dashboard/shell/components/form/ResourceTabs/index.vue

341 lines
8.8 KiB
Vue

<script>
/*
Tab component for resource CRU pages featuring:
Labels and Annotation tabs with content filtered by create-edit-view mixin
*/
import Tabbed from '@shell/components/Tabbed';
import Tab from '@shell/components/Tabbed/Tab';
import CreateEditView from '@shell/mixins/create-edit-view';
import Conditions from '@shell/components/form/Conditions';
import { EVENT, NAMESPACE } from '@shell/config/types';
import PaginatedResourceTable from '@shell/components/PaginatedResourceTable.vue';
import { _VIEW } from '@shell/config/query-params';
import RelatedResources from '@shell/components/RelatedResources';
import { isConditionReadyAndWaiting } from '@shell/plugins/dashboard-store/resource-class';
import { PaginationParamFilter } from '@shell/types/store/pagination.types';
import { MESSAGE, REASON } from '@shell/config/table-headers';
import { STEVE_EVENT_FIRST_SEEN, STEVE_EVENT_LAST_SEEN, STEVE_EVENT_TYPE, STEVE_NAME_COL } from '@shell/config/pagination-table-headers';
import { headerFromSchemaColString } from '@shell/store/type-map.utils';
import { useIndicateUseCounts } from '@shell/components/form/ResourceTabs/composable';
export default {
name: 'ResourceTabs',
components: {
Tabbed,
Tab,
Conditions,
PaginatedResourceTable,
RelatedResources,
},
mixins: [CreateEditView],
props: {
// resource instance
value: {
type: Object,
default: () => {
return {};
}
},
// create-edit-view mode
mode: {
type: String,
default: _VIEW,
},
defaultTab: {
type: String,
default: null,
},
needConditions: {
type: Boolean,
default: true
},
needEvents: {
type: Boolean,
default: true
},
needRelated: {
type: Boolean,
default: true
},
extensionParams: {
type: Object,
default: null
},
useHash: {
type: Boolean,
default: true
}
},
setup(props) {
if (props.mode === _VIEW) {
useIndicateUseCounts();
}
},
data() {
const inStore = this.$store.getters['currentStore'](EVENT);
const eventSchema = this.$store.getters[`${ inStore }/schemaFor`](EVENT); // @TODO be smarter about which resources actually ever have events
const paginationHeaders = eventSchema ? [
STEVE_EVENT_LAST_SEEN,
STEVE_EVENT_TYPE,
REASON,
headerFromSchemaColString('Subobject', eventSchema, this.$store.getters, true),
headerFromSchemaColString('Source', eventSchema, this.$store.getters, true),
MESSAGE,
STEVE_EVENT_FIRST_SEEN,
headerFromSchemaColString('Count', eventSchema, this.$store.getters, true),
STEVE_NAME_COL,
] : [];
return {
eventSchema,
EVENT,
selectedTab: this.defaultTab,
inStore,
showConditions: false,
paginationHeaders
};
},
beforeUnmount() {
this.$store.dispatch('cluster/forgetType', EVENT);
},
fetch() {
// By this stage the `value` should be set. Taking a chance that this is true
// The alternative is have an expensive watch on the `value` and trigger there (as well)
this.setShowConditions();
},
computed: {
isNamespace() {
return this.value?.type === NAMESPACE;
},
showEvents() {
return this.isView && this.needEvents && this.eventSchema;
},
showRelated() {
return this.isView && this.needRelated;
},
eventHeaders() {
return [
{
name: 'type',
label: this.t('tableHeaders.type'),
value: 'eventType',
sort: 'eventType',
},
{
name: 'reason',
label: this.t('tableHeaders.reason'),
value: 'reason',
sort: 'reason',
},
{
name: 'date',
label: this.t('tableHeaders.updated'),
value: 'date',
sort: 'date:desc',
formatter: 'LiveDate',
formatterOpts: { addSuffix: true },
width: 125
},
{
name: 'message',
label: this.t('tableHeaders.message'),
value: 'message',
sort: 'message',
},
];
},
conditionsHaveIssues() {
if (this.showConditions) {
return this.value.status?.conditions?.filter((cond) => !isConditionReadyAndWaiting(cond)).some((cond) => cond.error);
}
return false;
},
children() {
return this.$slots?.default?.() || [];
}
},
methods: {
/**
* Conditions come from a resource's `status`. They are used by both core resources like workloads as well as those from CRDs
* - Workloads
* - Nodes
* - Fleet git repo
* - Cluster (provisioning)
*
* Check here if the resource type contains conditions via the schema resourceFields
*/
async setShowConditions() {
if (this.isView && this.needConditions && !!this.value?.type && !!this.schema?.fetchResourceFields) {
await this.schema.fetchResourceFields();
this.showConditions = this.$store.getters[`${ this.inStore }/pathExistsInSchema`](this.value.type, 'status.conditions');
}
},
/**
* Filter out hidden repos from list of all repos
*/
filterEventsLocal(rows) {
return rows.filter((event) => {
return this.isNamespace ? event.metadata?.namespace === this.value?.metadata?.name : event.involvedObject?.uid === this.value?.metadata?.uid;
});
},
/**
* Filter out hidden repos via api
*
* pagination: PaginationArgs
* returns: PaginationArgs
*/
filterEventsApi(pagination) {
if (!pagination.filters) {
pagination.filters = [];
}
// Determine the field and value based on type
const field = this.isNamespace ? 'metadata.namespace' : 'involvedObject.uid';
const value = this.isNamespace ? this.value.metadata.name : this.value.metadata.uid;
// Check if a filter for this field already exists
const existing = pagination.filters.find((f) => f.fields.some((ff) => ff.field === field));
// Create the required filter
const required = PaginationParamFilter.createSingleField({
field,
exact: true,
value,
equals: true
});
// Merge or add the filter
if (!!existing) {
Object.assign(existing, required);
} else {
pagination.filters.push(required);
}
return pagination;
}
}
};
</script>
<template>
<Tabbed
class="resource-tabs"
:class="{[mode]: true}"
v-bind="$attrs"
:default-tab="defaultTab"
:resource="value"
:use-hash="useHash"
@changed="tabChange"
>
<slot />
<Tab
v-if="showConditions"
label-key="resourceTabs.conditions.tab"
name="conditions"
:weight="-1"
:display-alert-icon="conditionsHaveIssues"
>
<Conditions :value="value" />
</Tab>
<Tab
v-if="showEvents"
:label="isNamespace ? t('resourceTabs.events.namespaceTab') : t('resourceTabs.events.tab')"
name="events"
:weight="-2"
>
<!-- Caption for namespace pages -->
<div
v-if="isNamespace"
v-clean-html="t('resourceTabs.events.namespaceCaption', { namespace: value.metadata.name }, true)"
class="tab-caption"
/>
<!-- namespaced: false given we don't want the default handling of namespaced resource (apply header filter) -->
<PaginatedResourceTable
:schema="eventSchema"
:local-filter="filterEventsLocal"
:api-filter="filterEventsApi"
:use-query-params-for-simple-filtering="false"
:headers="eventHeaders"
:paginationHeaders="paginationHeaders"
:namespaced="false"
/>
</Tab>
<Tab
v-if="showRelated"
name="related"
label-key="resourceTabs.related.tab"
:weight="-3"
>
<h3 v-t="'resourceTabs.related.from'" />
<RelatedResources
:ignore-types="[value.type]"
:value="value"
direction="from"
/>
<h3
v-t="'resourceTabs.related.to'"
class="mt-20"
/>
<RelatedResources
:ignore-types="[value.type]"
:value="value"
direction="to"
/>
</Tab>
</Tabbed>
</template>
<style lang="scss" scoped>
.resource-tabs {
// For the time being we're only targeting detail pages for the new styling. Remove this if we want this style to apply to all pages.
&.view {
:deep() .tabs.horizontal {
border: none;
}
:deep() .tabs.horizontal + .tab-container {
border: none;
border-top: 1px solid var(--border);
padding: 0;
padding-top: 24px;
}
}
}
/* Caption for namespace events tab */
.tab-caption {
align-items: center;
font-size: 16px;
margin-bottom: 24px;
.namespace-name {
display: inline;
font-weight: bold;
margin-right: 0 4px;
white-space: nowrap;
}
}
</style>