mirror of https://github.com/rancher/dashboard.git
880 lines
24 KiB
Vue
880 lines
24 KiB
Vue
<script>
|
|
import DashboardMetrics from '@shell/components/DashboardMetrics';
|
|
import { mapGetters } from 'vuex';
|
|
import {
|
|
CAPI,
|
|
ENDPOINTS,
|
|
EVENT,
|
|
NAMESPACE,
|
|
INGRESS,
|
|
MANAGEMENT,
|
|
METRIC,
|
|
NODE,
|
|
SERVICE,
|
|
PV,
|
|
WORKLOAD_TYPES,
|
|
COUNT,
|
|
CATALOG,
|
|
SECRET
|
|
} from '@shell/config/types';
|
|
import { setPromiseResult } from '@shell/utils/promise';
|
|
import AlertTable from '@shell/components/AlertTable';
|
|
import { Banner } from '@components/Banner';
|
|
import { parseSi, createMemoryValues } from '@shell/utils/units';
|
|
import {
|
|
NAME,
|
|
ROLES,
|
|
STATE,
|
|
} from '@shell/config/table-headers';
|
|
|
|
import { monitoringStatus, canViewGrafanaLink } from '@shell/utils/monitoring';
|
|
import Tabbed from '@shell/components/Tabbed';
|
|
import Tab from '@shell/components/Tabbed/Tab';
|
|
import { allDashboardsExist } from '@shell/utils/grafana';
|
|
import EtcdInfoBanner from '@shell/components/EtcdInfoBanner';
|
|
import metricPoller from '@shell/mixins/metric-poller';
|
|
import ResourceSummary, { resourceCounts } from '@shell/components/ResourceSummary';
|
|
import HardwareResourceGauge from '@shell/components/HardwareResourceGauge';
|
|
import { isEmpty } from '@shell/utils/object';
|
|
import ConfigBadge from './ConfigBadge';
|
|
import EventsTable from './EventsTable';
|
|
import { fetchClusterResources } from './explorer-utils';
|
|
import SimpleBox from '@shell/components/SimpleBox';
|
|
import { ExtensionPoint, CardLocation } from '@shell/core/types';
|
|
import { getApplicableExtensionEnhancements } from '@shell/core/plugin-helpers';
|
|
import Certificates from '@shell/components/Certificates';
|
|
import { NAME as EXPLORER } from '@shell/config/product/explorer';
|
|
import TabTitle from '@shell/components/TabTitle';
|
|
import { STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
|
|
|
|
export const RESOURCES = [NAMESPACE, INGRESS, PV, WORKLOAD_TYPES.DEPLOYMENT, WORKLOAD_TYPES.STATEFUL_SET, WORKLOAD_TYPES.JOB, WORKLOAD_TYPES.DAEMON_SET, SERVICE];
|
|
|
|
const CLUSTER_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/rancher-cluster-nodes-1/rancher-cluster-nodes?orgId=1';
|
|
const CLUSTER_METRICS_SUMMARY_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/rancher-cluster-1/rancher-cluster?orgId=1';
|
|
const K8S_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/rancher-k8s-components-nodes-1/rancher-kubernetes-components-nodes?orgId=1';
|
|
const K8S_METRICS_SUMMARY_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/rancher-k8s-components-1/rancher-kubernetes-components?orgId=1';
|
|
const ETCD_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/rancher-etcd-nodes-1/rancher-etcd-nodes?orgId=1';
|
|
const ETCD_METRICS_SUMMARY_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/rancher-etcd-1/rancher-etcd?orgId=1';
|
|
|
|
const CLUSTER_COMPONENTS = [
|
|
'etcd',
|
|
'scheduler',
|
|
'controller-manager',
|
|
];
|
|
|
|
export default {
|
|
name: 'ClusterExplorerIndexPage',
|
|
|
|
components: {
|
|
EtcdInfoBanner,
|
|
DashboardMetrics,
|
|
HardwareResourceGauge,
|
|
ResourceSummary,
|
|
Tab,
|
|
Tabbed,
|
|
AlertTable,
|
|
Banner,
|
|
ConfigBadge,
|
|
EventsTable,
|
|
SimpleBox,
|
|
Certificates,
|
|
TabTitle
|
|
},
|
|
|
|
mixins: [metricPoller],
|
|
|
|
fetch() {
|
|
fetchClusterResources(this.$store, NODE);
|
|
|
|
if (this.currentCluster) {
|
|
setPromiseResult(
|
|
allDashboardsExist(this.$store, this.currentCluster.id, [CLUSTER_METRICS_DETAIL_URL, CLUSTER_METRICS_SUMMARY_URL]),
|
|
this,
|
|
'showClusterMetrics',
|
|
`Determine cluster metrics`
|
|
);
|
|
setPromiseResult(
|
|
allDashboardsExist(this.$store, this.currentCluster.id, [K8S_METRICS_DETAIL_URL, K8S_METRICS_SUMMARY_URL]),
|
|
this,
|
|
'showK8sMetrics',
|
|
`Determine k8s metrics`
|
|
);
|
|
setPromiseResult(
|
|
allDashboardsExist(this.$store, this.currentCluster.id, [ETCD_METRICS_DETAIL_URL, ETCD_METRICS_SUMMARY_URL]),
|
|
this,
|
|
'showEtcdMetrics',
|
|
`Determine etcd metrics`
|
|
);
|
|
|
|
// It's not enough to check that the grafana links are working for the current user; embedded cluster-level dashboards should only be shown if the user can view the grafana endpoint
|
|
// https://github.com/rancher/dashboard/issues/9792
|
|
setPromiseResult(canViewGrafanaLink(this.$store), this, 'canViewMetrics', 'Determine Grafana Permission');
|
|
|
|
if (this.currentCluster.isLocal && this.$store.getters['management/schemaFor'](MANAGEMENT.NODE)) {
|
|
this.$store.dispatch('management/findAll', { type: MANAGEMENT.NODE });
|
|
}
|
|
|
|
this.canViewAgents = this.$store.getters['cluster/canList'](WORKLOAD_TYPES.DEPLOYMENT) && this.$store.getters['cluster/canList'](WORKLOAD_TYPES.STATEFUL_SET);
|
|
|
|
if (this.canViewAgents) {
|
|
this.loadAgents();
|
|
}
|
|
}
|
|
},
|
|
|
|
data() {
|
|
const clusterCounts = this.$store.getters[`cluster/all`](COUNT);
|
|
const nodeHeaders = [
|
|
STATE,
|
|
NAME,
|
|
ROLES,
|
|
];
|
|
|
|
return {
|
|
nodeHeaders,
|
|
constraints: [],
|
|
cattleDeployment: 'loading',
|
|
fleetDeployment: 'loading',
|
|
fleetStatefulSet: 'loading',
|
|
canViewAgents: false,
|
|
disconnected: false,
|
|
events: [],
|
|
nodeMetrics: [],
|
|
showClusterMetrics: false,
|
|
showK8sMetrics: false,
|
|
showEtcdMetrics: false,
|
|
canViewMetrics: false,
|
|
CLUSTER_METRICS_DETAIL_URL,
|
|
CLUSTER_METRICS_SUMMARY_URL,
|
|
K8S_METRICS_DETAIL_URL,
|
|
K8S_METRICS_SUMMARY_URL,
|
|
ETCD_METRICS_DETAIL_URL,
|
|
ETCD_METRICS_SUMMARY_URL,
|
|
STATES_ENUM,
|
|
clusterCounts,
|
|
selectedTab: 'cluster-events',
|
|
extensionCards: getApplicableExtensionEnhancements(this, ExtensionPoint.CARD, CardLocation.CLUSTER_DASHBOARD_CARD, this.$route),
|
|
};
|
|
},
|
|
|
|
beforeDestroy() {
|
|
// Remove the data and stop watching resources that were fetched in this page
|
|
// Events in particular can lead to change messages having to be processed when we are no longer interested in events
|
|
this.$store.dispatch('cluster/forgetType', EVENT);
|
|
this.$store.dispatch('cluster/forgetType', NODE);
|
|
this.$store.dispatch('cluster/forgetType', ENDPOINTS); // Used by AlertTable to get alerts when v2 monitoring is installed
|
|
this.$store.dispatch('cluster/forgetType', METRIC.NODE);
|
|
this.$store.dispatch('cluster/forgetType', MANAGEMENT.NODE);
|
|
this.$store.dispatch('cluster/forgetType', WORKLOAD_TYPES.DEPLOYMENT);
|
|
|
|
clearInterval(this.interval);
|
|
},
|
|
|
|
computed: {
|
|
...mapGetters(['currentCluster']),
|
|
...monitoringStatus(),
|
|
|
|
nodes() {
|
|
return this.$store.getters['cluster/all'](NODE);
|
|
},
|
|
|
|
mgmtNodes() {
|
|
return this.$store.getters['management/all'](MANAGEMENT.CLUSTER);
|
|
},
|
|
|
|
displayProvider() {
|
|
const other = 'other';
|
|
|
|
let provider = this.currentCluster?.status?.provider || other;
|
|
|
|
if (provider === 'rke.windows') {
|
|
provider = 'rkeWindows';
|
|
}
|
|
|
|
if (!this.$store.getters['i18n/exists'](`cluster.provider.${ provider }`)) {
|
|
provider = 'other';
|
|
}
|
|
|
|
return this.t(`cluster.provider.${ provider }`);
|
|
},
|
|
|
|
isHarvesterCluster() {
|
|
return this.currentCluster?.isHarvester;
|
|
},
|
|
|
|
isRKE() {
|
|
return ['rke', 'rke.windows', 'rke2', 'rke2.windows'].includes((this.currentCluster.status.provider || '').toLowerCase());
|
|
},
|
|
|
|
accessibleResources() {
|
|
// This is a list of IDs for allowed resources counts.
|
|
const defaultAllowedResources = Object.keys(this.clusterCounts?.[0]?.counts || {}).filter((typeId) => {
|
|
return this.$store.getters['type-map/isIgnored']({ id: typeId });
|
|
});
|
|
|
|
// Merge with RESOURCES list
|
|
const allowedResources = [...new Set([...defaultAllowedResources, ...RESOURCES])];
|
|
|
|
return allowedResources.filter((resource) => this.$store.getters['cluster/schemaFor'](resource));
|
|
},
|
|
|
|
clusterServices() {
|
|
const services = [];
|
|
|
|
CLUSTER_COMPONENTS.forEach((cs) => {
|
|
services.push({
|
|
name: cs,
|
|
status: this.getComponentStatus(cs),
|
|
labelKey: `clusterIndexPage.sections.componentStatus.${ cs }`,
|
|
});
|
|
});
|
|
|
|
if (this.canViewAgents) {
|
|
if (!this.currentCluster.isLocal) {
|
|
services.push({
|
|
name: 'cattle',
|
|
status: this.cattleStatus,
|
|
labelKey: 'clusterIndexPage.sections.componentStatus.cattle',
|
|
});
|
|
}
|
|
|
|
services.push({
|
|
name: 'fleet',
|
|
status: this.fleetStatus,
|
|
labelKey: 'clusterIndexPage.sections.componentStatus.fleet',
|
|
});
|
|
}
|
|
|
|
return services;
|
|
},
|
|
|
|
cattleStatus() {
|
|
const resource = this.cattleDeployment;
|
|
|
|
if (resource === 'loading') {
|
|
return STATES_ENUM.IN_PROGRESS;
|
|
}
|
|
|
|
if (!resource || this.disconnected || resource.status.conditions?.find((c) => c.status !== 'True') || resource.metadata.state?.error) {
|
|
return STATES_ENUM.UNHEALTHY;
|
|
}
|
|
|
|
if (resource.spec.replicas !== resource.status.readyReplicas || resource.status.unavailableReplicas > 0) {
|
|
return STATES_ENUM.WARNING;
|
|
}
|
|
|
|
return STATES_ENUM.HEALTHY;
|
|
},
|
|
|
|
fleetStatus() {
|
|
const resources = [this.fleetStatefulSet];
|
|
|
|
if (this.currentCluster.isLocal) {
|
|
resources.push(this.fleetDeployment);
|
|
}
|
|
|
|
if (resources.find((r) => r === 'loading')) {
|
|
return STATES_ENUM.IN_PROGRESS;
|
|
}
|
|
|
|
for (const resource of resources) {
|
|
if (!resource || resource.status.conditions?.find((c) => c.status !== 'True') || resource.metadata.state?.error) {
|
|
return STATES_ENUM.UNHEALTHY;
|
|
}
|
|
}
|
|
|
|
for (const resource of resources) {
|
|
if (resource.spec.replicas !== resource.status.readyReplicas || resource.status.unavailableReplicas > 0) {
|
|
return STATES_ENUM.WARNING;
|
|
}
|
|
}
|
|
|
|
return STATES_ENUM.HEALTHY;
|
|
},
|
|
|
|
totalCountGaugeInput() {
|
|
const totalInput = {
|
|
name: this.t('clusterIndexPage.resourceGauge.totalResources'),
|
|
total: 0,
|
|
useful: 0,
|
|
warningCount: 0,
|
|
errorCount: 0
|
|
};
|
|
|
|
this.accessibleResources.forEach((resource) => {
|
|
const counts = resourceCounts(this.$store, resource);
|
|
|
|
Object.entries(counts).forEach((entry) => {
|
|
totalInput[entry[0]] += entry[1];
|
|
});
|
|
});
|
|
|
|
return totalInput;
|
|
},
|
|
|
|
hasStats() {
|
|
return this.currentCluster?.status?.allocatable && this.currentCluster?.status?.requested;
|
|
},
|
|
|
|
cpuReserved() {
|
|
const total = parseSi(this.currentCluster?.status?.allocatable?.cpu);
|
|
|
|
return {
|
|
total,
|
|
useful: parseSi(this.currentCluster?.status?.requested?.cpu),
|
|
units: this.t('clusterIndexPage.hardwareResourceGauge.units.cores', { count: total })
|
|
};
|
|
},
|
|
|
|
podsUsed() {
|
|
return {
|
|
total: parseSi(this.currentCluster?.status?.allocatable?.pods || '0'),
|
|
useful: parseSi(this.currentCluster?.status?.requested?.pods || '0'),
|
|
};
|
|
},
|
|
|
|
ramReserved() {
|
|
return createMemoryValues(this.currentCluster?.status?.allocatable?.memory, this.currentCluster?.status?.requested?.memory);
|
|
},
|
|
|
|
metricAggregations() {
|
|
let checkNodes = this.nodes;
|
|
|
|
// Special case local cluster
|
|
if (this.currentCluster.isLocal) {
|
|
const nodeNames = this.nodes.reduce((acc, n) => {
|
|
acc[n.id] = n;
|
|
|
|
return acc;
|
|
}, {});
|
|
|
|
checkNodes = this.mgmtNodes.filter((n) => {
|
|
const nodeName = n.metadata?.labels?.['management.cattle.io/nodename'] || n.id;
|
|
|
|
return !!nodeNames[nodeName];
|
|
});
|
|
}
|
|
|
|
const someNonWorkerRoles = checkNodes.some((node) => node.hasARole && !node.isWorker);
|
|
const metrics = this.nodeMetrics.filter((nodeMetrics) => {
|
|
const node = this.nodes.find((nd) => nd.id === nodeMetrics.id);
|
|
|
|
return node && (!someNonWorkerRoles || node.isWorker);
|
|
});
|
|
const initialAggregation = {
|
|
cpu: 0,
|
|
memory: 0
|
|
};
|
|
|
|
if (isEmpty(metrics)) {
|
|
return null;
|
|
}
|
|
|
|
return metrics.reduce((agg, metric) => {
|
|
agg.cpu += parseSi(metric.usage.cpu);
|
|
agg.memory += parseSi(metric.usage.memory);
|
|
|
|
return agg;
|
|
}, initialAggregation);
|
|
},
|
|
|
|
cpuUsed() {
|
|
const total = parseSi(this.currentCluster?.status?.capacity?.cpu);
|
|
|
|
return {
|
|
total,
|
|
useful: this.metricAggregations?.cpu,
|
|
units: this.t('clusterIndexPage.hardwareResourceGauge.units.cores', { count: total })
|
|
};
|
|
},
|
|
|
|
ramUsed() {
|
|
return createMemoryValues(this.currentCluster?.status?.capacity?.memory, this.metricAggregations?.memory);
|
|
},
|
|
|
|
hasMonitoring() {
|
|
return !!this.clusterCounts?.[0]?.counts?.[CATALOG.APP]?.namespaces?.['cattle-monitoring-system'];
|
|
},
|
|
|
|
canAccessNodes() {
|
|
return !!this.clusterCounts?.[0]?.counts?.[NODE];
|
|
},
|
|
|
|
canAccessDeployments() {
|
|
return !!this.clusterCounts?.[0]?.counts?.[WORKLOAD_TYPES.DEPLOYMENT];
|
|
},
|
|
|
|
hasMetricsTabs() {
|
|
return this.canViewMetrics && ( this.showClusterMetrics || this.showK8sMetrics || this.showEtcdMetrics);
|
|
},
|
|
|
|
hasBadge() {
|
|
return !!this.currentCluster?.badge;
|
|
},
|
|
|
|
hasDescription() {
|
|
return !!this.currentCluster?.spec?.description;
|
|
},
|
|
|
|
allEventsLink() {
|
|
return {
|
|
name: 'c-cluster-product-resource',
|
|
params: {
|
|
product: EXPLORER,
|
|
resource: EVENT,
|
|
}
|
|
};
|
|
},
|
|
|
|
allSecretsLink() {
|
|
return {
|
|
name: 'c-cluster-product-resource',
|
|
params: {
|
|
product: EXPLORER,
|
|
resource: SECRET,
|
|
}
|
|
};
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
loadAgents() {
|
|
if (this.currentCluster.isLocal) {
|
|
this.setAgentResource('fleetDeployment', WORKLOAD_TYPES.DEPLOYMENT, 'cattle-fleet-system/fleet-controller');
|
|
this.setAgentResource('fleetStatefulSet', WORKLOAD_TYPES.STATEFUL_SET, 'cattle-fleet-local-system/fleet-agent');
|
|
} else {
|
|
this.setAgentResource('fleetStatefulSet', WORKLOAD_TYPES.STATEFUL_SET, 'cattle-fleet-system/fleet-agent');
|
|
this.setAgentResource('cattleDeployment', WORKLOAD_TYPES.DEPLOYMENT, 'cattle-system/cattle-cluster-agent');
|
|
|
|
// Scaling Up/Down cattle deployment causes web sockets disconnection;
|
|
this.interval = setInterval(() => {
|
|
this.disconnected = !!this.$store.getters['cluster/inError']({ type: NODE });
|
|
}, 1000);
|
|
}
|
|
},
|
|
|
|
async setAgentResource(agent, type, id) {
|
|
try {
|
|
this[agent] = await this.$store.dispatch('cluster/find', { type, id });
|
|
} catch (err) {
|
|
this[agent] = null;
|
|
}
|
|
},
|
|
|
|
getComponentStatus(field) {
|
|
const matching = (this.currentCluster?.status?.componentStatuses || []).filter((s) => s.name.startsWith(field));
|
|
|
|
// If there's no matching component status, it's "healthy"
|
|
if ( !matching.length ) {
|
|
return STATES_ENUM.HEALTHY;
|
|
}
|
|
|
|
const count = matching.reduce((acc, status) => {
|
|
const conditions = status.conditions.find((c) => c.status !== 'True');
|
|
|
|
return !conditions ? acc : acc + 1;
|
|
}, 0);
|
|
|
|
if (count > 0) {
|
|
return STATES_ENUM.UNHEALTHY;
|
|
}
|
|
|
|
return STATES_ENUM.HEALTHY;
|
|
},
|
|
|
|
showActions() {
|
|
this.$store.commit('action-menu/show', {
|
|
resources: this.currentCluster,
|
|
elem: this.$refs['cluster-actions'],
|
|
});
|
|
},
|
|
|
|
// Used by metric-poller mixin
|
|
async loadMetrics() {
|
|
this.nodeMetrics = await fetchClusterResources(this.$store, METRIC.NODE, { force: true } );
|
|
},
|
|
|
|
// Events/Alerts tab changed
|
|
tabChange(neu) {
|
|
this.selectedTab = neu?.selectedName;
|
|
},
|
|
|
|
async goToHarvesterCluster() {
|
|
try {
|
|
const provClusters = await this.$store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER });
|
|
const provCluster = provClusters.find((p) => p.mgmt.id === this.currentCluster.id);
|
|
|
|
await provCluster.goToHarvesterCluster();
|
|
} catch {
|
|
}
|
|
}
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<section class="dashboard">
|
|
<header>
|
|
<div class="title">
|
|
<h1>
|
|
<TabTitle>
|
|
{{ t('clusterIndexPage.header') }}
|
|
</TabTitle>
|
|
</h1>
|
|
<div>
|
|
<span v-if="hasDescription">{{ currentCluster.spec.description }}</span>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<div
|
|
class="cluster-dashboard-glance"
|
|
>
|
|
<div>
|
|
<label>{{ t('glance.provider') }}: </label>
|
|
<span v-if="isHarvesterCluster">
|
|
<a
|
|
role="button"
|
|
@click="goToHarvesterCluster"
|
|
>
|
|
{{ displayProvider }}
|
|
</a>
|
|
</span>
|
|
<span v-else>
|
|
{{ displayProvider }}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<label>{{ t('glance.version') }}: </label>
|
|
<span>{{ currentCluster.kubernetesVersionBase }}</span>
|
|
<span
|
|
v-if="currentCluster.kubernetesVersionExtension"
|
|
style="font-size: 0.75em"
|
|
>{{ currentCluster.kubernetesVersionExtension }}</span>
|
|
</div>
|
|
<div>
|
|
<label>{{ t('glance.created') }}: </label>
|
|
<span><LiveDate
|
|
:value="currentCluster.metadata.creationTimestamp"
|
|
:add-suffix="true"
|
|
:show-tooltip="true"
|
|
/></span>
|
|
</div>
|
|
<div :style="{'flex':1}" />
|
|
<div v-if="!monitoringStatus.v2">
|
|
<router-link
|
|
:to="{name: 'c-cluster-explorer-tools'}"
|
|
class="monitoring-install"
|
|
>
|
|
<i class="icon icon-gear" />
|
|
<span>{{ t('glance.installMonitoring') }}</span>
|
|
</router-link>
|
|
</div>
|
|
<ConfigBadge
|
|
v-if="currentCluster.canUpdate"
|
|
:cluster="currentCluster"
|
|
/>
|
|
</div>
|
|
|
|
<div class="resource-gauges">
|
|
<ResourceSummary :spoofed-counts="totalCountGaugeInput" />
|
|
<ResourceSummary
|
|
v-if="canAccessNodes"
|
|
resource="node"
|
|
/>
|
|
<ResourceSummary
|
|
v-if="canAccessDeployments"
|
|
resource="apps.deployment"
|
|
/>
|
|
</div>
|
|
|
|
<!-- extension cards -->
|
|
<div
|
|
v-if="extensionCards.length"
|
|
class="extension-card-container mt-20"
|
|
>
|
|
<SimpleBox
|
|
v-for="item, i in extensionCards"
|
|
:key="`extensionCards${i}`"
|
|
class="extension-card"
|
|
:style="item.style"
|
|
>
|
|
<h3>
|
|
{{ item.label }}
|
|
</h3>
|
|
<component
|
|
:is="item.component"
|
|
:resource="currentCluster"
|
|
/>
|
|
</SimpleBox>
|
|
</div>
|
|
|
|
<h3
|
|
v-if="hasStats"
|
|
class="mt-40"
|
|
>
|
|
{{ t('clusterIndexPage.sections.capacity.label') }}
|
|
</h3>
|
|
<div
|
|
v-if="hasStats"
|
|
class="hardware-resource-gauges"
|
|
>
|
|
<HardwareResourceGauge
|
|
:name="t('clusterIndexPage.hardwareResourceGauge.pods')"
|
|
:used="podsUsed"
|
|
/>
|
|
<HardwareResourceGauge
|
|
:name="t('clusterIndexPage.hardwareResourceGauge.cores')"
|
|
:reserved="cpuReserved"
|
|
:used="cpuUsed"
|
|
:units="cpuReserved.units"
|
|
/>
|
|
<HardwareResourceGauge
|
|
:name="t('clusterIndexPage.hardwareResourceGauge.ram')"
|
|
:reserved="ramReserved"
|
|
:used="ramUsed"
|
|
:units="ramReserved.units"
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="clusterServices">
|
|
<div
|
|
v-for="service in clusterServices"
|
|
:key="service.name"
|
|
class="k8s-service-status"
|
|
:class="{[service.status]: true }"
|
|
:data-testid="`k8s-service-${ service.name }`"
|
|
>
|
|
<i
|
|
v-if="service.status === STATES_ENUM.IN_PROGRESS"
|
|
class="icon icon-spinner icon-spin"
|
|
/>
|
|
<i
|
|
v-else-if="service.status === STATES_ENUM.HEALTHY"
|
|
class="icon icon-checkmark"
|
|
/>
|
|
<i
|
|
v-else
|
|
class="icon icon-warning"
|
|
/>
|
|
<div class="label">
|
|
{{ t(service.labelKey) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-30">
|
|
<Tabbed @changed="tabChange">
|
|
<Tab
|
|
name="cluster-events"
|
|
:label="t('clusterIndexPage.sections.events.label')"
|
|
:weight="2"
|
|
>
|
|
<span class="events-table-link">
|
|
<router-link :to="allEventsLink">
|
|
<span>{{ t('glance.eventsTable') }}</span>
|
|
</router-link>
|
|
</span>
|
|
<EventsTable />
|
|
</Tab>
|
|
<Tab
|
|
v-if="hasMonitoring"
|
|
name="cluster-alerts"
|
|
:label="t('clusterIndexPage.sections.alerts.label')"
|
|
:weight="1"
|
|
>
|
|
<AlertTable v-if="selectedTab === 'cluster-alerts'" />
|
|
</Tab>
|
|
<Tab
|
|
name="cluster-certs"
|
|
:label="t('clusterIndexPage.sections.certs.label')"
|
|
:weight="1"
|
|
>
|
|
<span class="events-table-link">
|
|
<router-link :to="allSecretsLink">
|
|
<span>{{ t('glance.secretsTable') }}</span>
|
|
</router-link>
|
|
</span>
|
|
<Certificates v-if="selectedTab === 'cluster-certs'" />
|
|
</Tab>
|
|
</Tabbed>
|
|
</div>
|
|
<Tabbed
|
|
v-if="hasMetricsTabs"
|
|
default-tab="cluster-metrics"
|
|
:use-hash="false"
|
|
class="mt-30"
|
|
>
|
|
<Tab
|
|
v-if="showClusterMetrics"
|
|
name="cluster-metrics"
|
|
:label="t('clusterIndexPage.sections.clusterMetrics.label')"
|
|
:weight="2"
|
|
>
|
|
<template #default="props">
|
|
<DashboardMetrics
|
|
v-if="props.active"
|
|
:detail-url="CLUSTER_METRICS_DETAIL_URL"
|
|
:summary-url="CLUSTER_METRICS_SUMMARY_URL"
|
|
graph-height="825px"
|
|
/>
|
|
</template>
|
|
</Tab>
|
|
<Tab
|
|
v-if="showK8sMetrics"
|
|
name="k8s-metrics"
|
|
:label="t('clusterIndexPage.sections.k8sMetrics.label')"
|
|
:weight="1"
|
|
>
|
|
<template #default="props">
|
|
<DashboardMetrics
|
|
v-if="props.active"
|
|
:detail-url="K8S_METRICS_DETAIL_URL"
|
|
:summary-url="K8S_METRICS_SUMMARY_URL"
|
|
graph-height="550px"
|
|
/>
|
|
</template>
|
|
</Tab>
|
|
<Tab
|
|
v-if="showEtcdMetrics"
|
|
name="etcd-metrics"
|
|
:label="t('clusterIndexPage.sections.etcdMetrics.label')"
|
|
:weight="0"
|
|
>
|
|
<template #default="props">
|
|
<DashboardMetrics
|
|
v-if="props.active"
|
|
class="etcd-metrics"
|
|
:detail-url="ETCD_METRICS_DETAIL_URL"
|
|
:summary-url="ETCD_METRICS_SUMMARY_URL"
|
|
graph-height="550px"
|
|
>
|
|
<EtcdInfoBanner />
|
|
</DashboardMetrics>
|
|
</template>
|
|
</Tab>
|
|
</Tabbed>
|
|
</section>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.extension-card-container {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(calc((100%/3) - 40px), 1fr));
|
|
grid-column-gap: 15px;
|
|
grid-row-gap: 20px;
|
|
}
|
|
|
|
@media only screen and (max-width: map-get($breakpoints, "--viewport-9")) {
|
|
.extension-card-container {
|
|
grid-template-columns: 1fr !important;
|
|
}
|
|
}
|
|
|
|
.cluster-dashboard-glance {
|
|
border-top: 1px solid var(--border);
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 20px 0px;
|
|
display: flex;
|
|
|
|
&>*:not(:nth-last-child(-n+2)) {
|
|
margin-right: 40px;
|
|
|
|
& SPAN {
|
|
font-weight: bold
|
|
}
|
|
}
|
|
}
|
|
|
|
.title h1 {
|
|
margin: 0;
|
|
}
|
|
|
|
.actions-span {
|
|
align-self: center;
|
|
}
|
|
|
|
.events {
|
|
margin-top: 30px;
|
|
}
|
|
|
|
.graph-options {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.etcd-metrics ::v-deep .external-link {
|
|
top: -107px;
|
|
}
|
|
|
|
.cluster-tools-tip {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.monitoring-install {
|
|
display: flex;
|
|
margin-left: 10px;
|
|
|
|
> I {
|
|
line-height: inherit;
|
|
margin-right: 4px;
|
|
}
|
|
|
|
&:focus {
|
|
outline: 0;
|
|
}
|
|
}
|
|
|
|
.events-table-link {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.k8s-service-status {
|
|
align-items: center;
|
|
display: inline-flex;
|
|
border: 1px solid;
|
|
border-color: var(--border);
|
|
margin-top: 20px;
|
|
|
|
.label {
|
|
border-left: 1px solid var(--border);
|
|
}
|
|
|
|
&:not(:last-child) {
|
|
margin-right: 20px;
|
|
}
|
|
|
|
> div {
|
|
padding: 5px 20px;
|
|
}
|
|
|
|
> I {
|
|
text-align: center;
|
|
padding: 5px 10px;
|
|
}
|
|
|
|
&.unhealthy {
|
|
border-color: var(--error-border);
|
|
|
|
> I {
|
|
color: var(--error)
|
|
}
|
|
}
|
|
|
|
&.warning {
|
|
> I {
|
|
color: var(--warning)
|
|
}
|
|
}
|
|
|
|
&.healthy {
|
|
> I {
|
|
color: var(--success)
|
|
}
|
|
}
|
|
}
|
|
</style>
|