mirror of https://github.com/rancher/dashboard.git
341 lines
8.8 KiB
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>
|