dashboard/pages/c/_cluster/index.vue

385 lines
10 KiB
Vue

<script>
import { isEmpty } from 'lodash';
import { get } from '@/utils/object';
import InfoBoxCluster from '@/components/InfoBoxCluster';
import InfoBox from '@/components/InfoBox';
import SortableTable from '@/components/SortableTable';
import DetailTop from '@/components/DetailTop';
import ClusterDisplayProvider from '@/components/ClusterDisplayProvider';
import {
MESSAGE,
NAME,
NAMESPACE_NAME,
REASON,
ROLES,
STATE,
} from '@/config/table-headers';
import { DESCRIPTION } from '@/config/labels-annotations';
import { findAllConstraints } from '@/utils/gatekeeper/util';
import {
MANAGEMENT,
EVENT,
NODE,
METRIC,
EXTERNAL,
GATEKEEPER,
SYSTEM_PROJECT_LABEL,
} from '@/config/types';
import { allHash } from '@/utils/promise';
import Poller from '@/utils/poller';
const METRICS_POLL_RATE_MS = 30000;
const MAX_FAILURES = 2;
export default {
components: {
DetailTop,
ClusterDisplayProvider,
InfoBox,
InfoBoxCluster,
SortableTable
},
async asyncData(ctx) {
const { route, store } = ctx;
const id = get(route, 'params.cluster');
let gatekeeper = null;
let gatekeeperEnabled = false;
const projects = await store.dispatch('clusterExternal/findAll', { type: EXTERNAL.PROJECT });
const targetSystemProject = projects.find(( proj ) => {
const labels = proj.metadata?.labels || {};
if ( labels[SYSTEM_PROJECT_LABEL] === 'true' ) {
return true;
}
});
if (!isEmpty(targetSystemProject)) {
const systemNamespace = targetSystemProject.metadata.name;
try {
gatekeeper = await store.dispatch('clusterExternal/find', {
type: EXTERNAL.APP,
id: `${ systemNamespace }/${ GATEKEEPER.APP_ID }`,
});
if (!isEmpty(gatekeeper)) {
gatekeeperEnabled = true;
}
} catch (err) {
gatekeeperEnabled = false;
}
}
const cluster = await store.dispatch('management/find', { type: MANAGEMENT.CLUSTER, id });
return {
constraints: [],
events: [],
nodeMetrics: [],
haveNodes: !!store.getters['cluster/schemaFor'](NODE),
haveNodeTemplates: !!store.getters['management/schemaFor'](MANAGEMENT.NODE_TEMPLATE),
haveNodePools: !!store.getters['management/schemaFor'](MANAGEMENT.NODE_POOL),
nodePools: [],
nodeTemplates: [],
nodes: [],
cluster,
gatekeeperEnabled,
};
},
data() {
const constraintHeaders = [
NAME,
{
name: 'violations',
label: 'Violations',
value: 'status.totalViolations',
sort: 'status.totalViolations',
width: 65
},
STATE,
];
const reason = { ...REASON, ...{ canBeVariable: true } };
const message = { ...MESSAGE, ...{ canBeVariable: true } };
const eventHeaders = [
reason,
{
name: 'object',
label: 'Object',
value: 'displayInvolvedObject',
sort: ['involvedObject.kind', 'involvedObject.name'],
canBeVariable: true,
formatter: 'LinkDetail',
},
message,
{
align: 'center',
name: 'date',
label: 'Date',
value: 'lastTimestamp',
sort: 'lastTimestamp',
formatter: 'LiveDate',
formatterOpts: { addSuffix: true },
width: 125
},
];
const nodeHeaders = [
NAMESPACE_NAME,
ROLES,
STATE,
];
return {
metricPoller: new Poller(this.loadMetrics, METRICS_POLL_RATE_MS, MAX_FAILURES),
gatekeeperEnabled: false,
constraintHeaders,
eventHeaders,
nodeHeaders,
};
},
computed: {
detailTopColumns() {
const out = [];
if ( this.haveNodeTemplates && this.haveNodePools ) {
out.push({
title: this.$store.getters['i18n/t']('infoBoxCluster.provider'),
name: 'cluster-provider',
});
}
out.push({
title: this.$store.getters['i18n/t']('infoBoxCluster.version'),
content: this.cluster.kubernetesVersion
});
if ( this.haveNodes ) {
out.push({
title: this.$store.getters['i18n/t']('infoBoxCluster.nodes.total.label'),
content: ( this.nodes || [] ).length
});
}
out.push({
title: this.$store.getters['i18n/t']('infoBoxCluster.created'),
name: 'live-date',
});
return out;
},
filteredNodes() {
const allNodes = this.nodes || [];
return allNodes.filter(node => !node.state.includes('healthy') && !node.state.includes('active'));
},
filteredConstraints() {
const allConstraints = this.constraints || [];
return allConstraints.filter(constraint => constraint?.status?.totalViolations > 0);
},
},
mounted() {
this.metricPoller.start();
},
async created() {
const hash = {
nodes: this.fetchClusterResources(NODE),
events: this.fetchClusterResources(EVENT),
constraints: this.fetchConstraints(),
};
if ( this.haveNodeTemplates ) {
hash.nodeTemplates = this.$store.dispatch('management/findAll', { type: MANAGEMENT.NODE_TEMPLATE });
}
if ( this.haveNodePools ) {
hash.nodePools = this.$store.dispatch('management/findAll', { type: MANAGEMENT.NODE_POOL });
}
const res = await allHash(hash);
for ( const k in res ) {
this[k] = res[k];
}
},
methods: {
showActions() {
this.$store.commit('action-menu/show', {
resources: this.cluster,
elem: this.$refs['cluster-actions'],
});
},
async fetchConstraints() {
try {
const rawConstraints = await findAllConstraints(this.$store);
return rawConstraints
.flat()
.map((constraint) => {
constraint.description = constraint.metadata.annotations[DESCRIPTION];
return constraint;
});
} catch (err) {
console.error(`Failed to fetch constraints:`, err); // eslint-disable-line no-console
return [];
}
},
async fetchClusterResources(type, opt = {}) {
const schema = this.$store.getters['cluster/schemaFor'](type);
if (schema) {
try {
const resources = await this.$store.dispatch('cluster/findAll', { type, opt });
return resources;
} catch (err) {
console.error(`Failed fetching cluster resource ${ type } with error:`, err); // eslint-disable-line no-console
return [];
}
}
return [];
},
async loadMetrics() {
this.nodeMetrics = await this.fetchClusterResources(METRIC.NODE, { force: true } );
},
},
beforeRouteLeave(to, from, next) {
this.metricPoller.stop();
next();
}
};
</script>
<template>
<section>
<header>
<h1>
<t k="clusterIndexPage.header" :name="cluster.nameDisplay" />
</h1>
<div class="actions">
<button
ref="cluster-actions"
type="button"
class="btn btn-sm role-multi-action actions"
aria-haspopup="true"
aria-expanded="false"
@click="showActions"
>
<i class="icon icon-actions" />
</button>
</div>
</header>
<DetailTop :columns="detailTopColumns" class="mb-20">
<template v-slot:cluster-provider>
<ClusterDisplayProvider :cluster="cluster" :node-templates="nodeTemplates" :node-pools="nodePools" />
</template>
<template v-slot:live-date>
<LiveDate :value="cluster.metadata.creationTimestamp" :add-suffix="true" />
</template>
</DetailTop>
<InfoBoxCluster
:cluster="cluster"
:metrics="nodeMetrics"
:nodes="nodes"
:node-templates="nodeTemplates"
:node-pools="nodePools"
/>
<div class="row">
<div class="col span-6 equal-height">
<InfoBox>
<h5>
<t k="clusterIndexPage.sections.nodes.label" />
</h5>
<div class="row mt-10">
<SortableTable
:rows="filteredNodes"
:headers="nodeHeaders"
:search="false"
:table-actions="false"
:row-actions="false"
no-rows-key="clusterIndexPage.sections.nodes.noRows"
key-field="id"
/>
</div>
</InfoBox>
</div>
<div class="col span-6 equal-height">
<InfoBox>
<h5>
<t k="clusterIndexPage.sections.gatekeeper.label" />
</h5>
<div v-if="gatekeeperEnabled">
<div class="row mt-10">
<SortableTable
:rows="filteredConstraints"
:headers="constraintHeaders"
:search="false"
:table-actions="false"
:row-actions="false"
key-field="id"
no-rows-key="clusterIndexPage.sections.gatekeeper.noRows"
/>
</div>
</div>
<div v-else>
<hr class="mt-35 mb-10" />
<div class="mt-35 mb-35 text-center">
<div>
<t k="clusterIndexPage.sections.gatekeeper.disabled" />
</div>
<n-link
:to="{ name: 'c-cluster-gatekeeper' }"
role="link"
type="button"
class="btn role-link"
>
<a>
<t k="clusterIndexPage.sections.gatekeeper.buttonText" />
</a>
</n-link>
</div>
</div>
</InfoBox>
</div>
</div>
<div class="row">
<div class="col span-12">
<InfoBox>
<h5>
<t k="clusterIndexPage.sections.events.label" />
</h5>
<div class="row mt-10">
<SortableTable
:rows="events"
:headers="eventHeaders"
key-field="id"
:search="false"
:table-actions="false"
:row-actions="false"
:paging="true"
:rows-per-page="10"
default-sort-by="date"
/>
</div>
</InfoBox>
</div>
</div>
</section>
</template>