mirror of https://github.com/rancher/dashboard.git
469 lines
12 KiB
Vue
469 lines
12 KiB
Vue
<script>
|
|
import { CAPI } from '@shell/config/types';
|
|
import AsyncButton from '@shell/components/AsyncButton';
|
|
import { downloadFile } from '@shell/utils/download';
|
|
import { filterOnlyKubernetesClusters, filterHiddenLocalCluster } from '@shell/utils/cluster';
|
|
import { sortBy } from '@shell/utils/sort';
|
|
import { Checkbox } from '@components/Form/Checkbox';
|
|
|
|
export default {
|
|
layout: 'plain',
|
|
components: { AsyncButton, Checkbox },
|
|
async fetch() {
|
|
const provClusters = await this.$store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER });
|
|
const readyClusters = provClusters.filter(c => c.mgmt?.isReady);
|
|
const clusterForCounts = filterHiddenLocalCluster(filterOnlyKubernetesClusters(readyClusters), this.$store);
|
|
const finalCounts = [];
|
|
const promises = [];
|
|
let topTenForResponseTime = [];
|
|
|
|
clusterForCounts.forEach((cluster, i) => {
|
|
finalCounts.push({
|
|
id: cluster.id,
|
|
capiId: cluster.mgmt?.id,
|
|
counts: null,
|
|
isTableVisible: i === 0
|
|
});
|
|
promises.push(this.$store.dispatch('management/request', { url: `/k8s/clusters/${ cluster.mgmt?.id }/v1/counts` }));
|
|
});
|
|
|
|
const countsPerCluster = await Promise.all(promises);
|
|
|
|
countsPerCluster.forEach((clusterCount, index) => {
|
|
const counts = clusterCount.data?.[0]?.counts;
|
|
|
|
if (counts) {
|
|
const sanitizedCount = [];
|
|
|
|
Object.keys(counts).forEach((key) => {
|
|
sanitizedCount[key] = counts[key].summary?.count;
|
|
sanitizedCount.push({
|
|
resource: key,
|
|
count: counts[key].summary?.count || 0,
|
|
});
|
|
});
|
|
|
|
const sortedCount = sortBy(sanitizedCount, 'count:desc');
|
|
|
|
topTenForResponseTime = topTenForResponseTime.concat(sortedCount);
|
|
topTenForResponseTime = sortBy(topTenForResponseTime, 'count:desc');
|
|
topTenForResponseTime = topTenForResponseTime.splice(0, 10);
|
|
|
|
topTenForResponseTime.forEach((item, i) => {
|
|
topTenForResponseTime[i].id = finalCounts[index].id;
|
|
topTenForResponseTime[i].capiId = finalCounts[index].capiId;
|
|
});
|
|
|
|
finalCounts[index].counts = sortedCount;
|
|
}
|
|
});
|
|
|
|
this.topTenForResponseTime = topTenForResponseTime;
|
|
this.finalCounts = finalCounts;
|
|
},
|
|
|
|
data() {
|
|
const systemInformation = {
|
|
browserUserAgent: {
|
|
label: this.t('about.diagnostic.systemInformation.browserUserAgent'),
|
|
value: window?.navigator?.userAgent
|
|
},
|
|
browserLanguage: {
|
|
label: this.t('about.diagnostic.systemInformation.browserLanguage'),
|
|
value: window?.navigator?.language
|
|
},
|
|
cookieEnabled: {
|
|
label: this.t('about.diagnostic.systemInformation.cookieEnabled'),
|
|
value: window?.navigator?.cookieEnabled
|
|
},
|
|
hardwareConcurrency: {
|
|
label: this.t('about.diagnostic.systemInformation.hardwareConcurrency'),
|
|
value: window?.navigator?.hardwareConcurrency
|
|
},
|
|
};
|
|
|
|
if (window?.navigator?.userAgentData?.platform) {
|
|
systemInformation.os = {
|
|
label: this.t('about.diagnostic.systemInformation.os'),
|
|
value: window?.navigator?.userAgentData?.platform
|
|
};
|
|
}
|
|
|
|
if (window?.navigator?.deviceMemory) {
|
|
systemInformation.deviceMemory = {
|
|
label: this.t('about.diagnostic.systemInformation.deviceMemory'),
|
|
value: window?.navigator?.deviceMemory
|
|
};
|
|
}
|
|
|
|
if (window?.performance?.memory?.jsHeapSizeLimit) {
|
|
systemInformation.memJsHeapLimit = {
|
|
label: this.t('about.diagnostic.systemInformation.memJsHeapLimit'),
|
|
value: window?.performance?.memory?.jsHeapSizeLimit
|
|
};
|
|
}
|
|
|
|
if (window?.performance?.memory?.totalJSHeapSize) {
|
|
systemInformation.memTotalJsHeapSize = {
|
|
label: this.t('about.diagnostic.systemInformation.memTotalJsHeapSize'),
|
|
value: window?.performance?.memory?.totalJSHeapSize
|
|
};
|
|
}
|
|
|
|
if (window?.performance?.memory?.usedJSHeapSize) {
|
|
systemInformation.memUsedJsHeapSize = {
|
|
label: this.t('about.diagnostic.systemInformation.memUsedJsHeapSize'),
|
|
value: window?.performance?.memory?.usedJSHeapSize
|
|
};
|
|
}
|
|
|
|
// scroll logs container to the bottom
|
|
this.scrollLogsToBottom();
|
|
|
|
return {
|
|
systemInformation,
|
|
topTenForResponseTime: null,
|
|
finalCounts: null,
|
|
includeResponseTimes: true,
|
|
storeMapping: this.$store?._modules?.root?.state,
|
|
latestLogs: console.logs // eslint-disable-line no-console
|
|
};
|
|
},
|
|
watch: {
|
|
latestLogs() {
|
|
this.scrollLogsToBottom();
|
|
}
|
|
},
|
|
computed: {
|
|
name() {
|
|
return this.data;
|
|
}
|
|
},
|
|
methods: {
|
|
scrollLogsToBottom() {
|
|
this.$nextTick(() => {
|
|
const logsContainer = document.querySelector('.logs-container');
|
|
|
|
logsContainer.scrollTop = logsContainer.scrollHeight;
|
|
});
|
|
},
|
|
generateKey(data) {
|
|
const randomize = Math.random() * 10000;
|
|
|
|
return `key_${ randomize }_${ data }`;
|
|
},
|
|
async downloadData(btnCb) {
|
|
const date = new Date().toLocaleDateString();
|
|
const time = new Date().toLocaleTimeString();
|
|
const fileName = `rancher-diagnostic-data-${ date }-${ time.replaceAll(':', '_') }.json`;
|
|
const data = {
|
|
systemInformation: this.systemInformation,
|
|
logs: this.latestLogs,
|
|
storeMapping: this.parseStoreData(this.storeMapping),
|
|
resourceCounts: this.finalCounts
|
|
};
|
|
|
|
if (this.includeResponseTimes) {
|
|
const responseTimes = await this.gatherResponseTimes();
|
|
|
|
data.responseTimes = responseTimes;
|
|
}
|
|
|
|
downloadFile(fileName, JSON.stringify(data), 'application/json')
|
|
.then(() => btnCb(true))
|
|
.catch(() => btnCb(false));
|
|
},
|
|
toggleTable(area) {
|
|
const itemIndex = this.finalCounts.findIndex(item => item.id === area);
|
|
|
|
this.finalCounts[itemIndex].isTableVisible = !this.finalCounts[itemIndex].isTableVisible;
|
|
},
|
|
async gatherResponseTimes() {
|
|
return await Promise.all(this.topTenForResponseTime.map((item) => {
|
|
const t = Date.now();
|
|
|
|
return this.$store.dispatch('management/request', { url: `/k8s/clusters/${ item.capiId }/v1/${ item.resource }` })
|
|
.then(() => ({
|
|
outcome: 'success', item, durationMs: Date.now() - t
|
|
}))
|
|
.catch(() => ({
|
|
outcome: 'error', item, durationMs: Date.now() - t
|
|
}));
|
|
})).then((responseTimes) => {
|
|
return responseTimes;
|
|
});
|
|
},
|
|
parseStoreData(rootStore) {
|
|
// clear potencial sensitive data
|
|
const disallowedDataKeys = [
|
|
'aws',
|
|
'digitalocean',
|
|
'linode',
|
|
];
|
|
|
|
const clearListsKeys = [
|
|
'management',
|
|
'rancher',
|
|
'cluster',
|
|
'harvester',
|
|
'epinio',
|
|
'elemental',
|
|
];
|
|
|
|
const cleanRootStore = {};
|
|
|
|
Object.entries(rootStore).forEach(([storeKey, store]) => {
|
|
if (disallowedDataKeys.includes(storeKey)) {
|
|
// Ignore any root store in the disallowed list
|
|
return;
|
|
}
|
|
|
|
// Remove all `list` keys, for example `management.types['management.cattle.io.cluster'].list`
|
|
|
|
if (!clearListsKeys.includes(storeKey)) {
|
|
// This is only done for some store namespaces, if not just save the raw entry
|
|
cleanRootStore[storeKey] = store;
|
|
|
|
return;
|
|
}
|
|
|
|
// if there's no types property to clear, just save the raw entry
|
|
if (!Object.keys(store.types || {}).length) {
|
|
cleanRootStore[storeKey] = store;
|
|
|
|
return;
|
|
}
|
|
|
|
// Save the root entry with empty types prop
|
|
cleanRootStore[storeKey] = {
|
|
...store,
|
|
types: {},
|
|
};
|
|
|
|
// Save the root entries type's entries without the list property
|
|
Object.entries(store.types).forEach(([type, entry]) => {
|
|
const { list, ...otherProps } = entry;
|
|
|
|
cleanRootStore[storeKey].types[type] = {
|
|
...otherProps,
|
|
count: list.length,
|
|
};
|
|
});
|
|
});
|
|
|
|
return cleanRootStore;
|
|
}
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<div class="title-block mt-20 mb-40">
|
|
<h1
|
|
v-t="'about.diagnostic.title'"
|
|
class="mt-20 mb-40"
|
|
/>
|
|
<div>
|
|
<Checkbox
|
|
v-model="includeResponseTimes"
|
|
v-tooltip.left="t('about.diagnostic.checkboxTooltip')"
|
|
:label="t('about.diagnostic.checkboxLabel')"
|
|
/>
|
|
<AsyncButton
|
|
mode="diagnostic"
|
|
@click="downloadData"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="mb-40">
|
|
<h2 class="mb-20">
|
|
{{ t('about.diagnostic.systemInformation.subtitle') }}
|
|
</h2>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Type</th>
|
|
<th>Value</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(item, objKey) in systemInformation" :key="objKey">
|
|
<td>{{ item.label }}</td>
|
|
<td>{{ item.value }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="mb-40">
|
|
<h2 class="mb-20">
|
|
{{ t('about.diagnostic.logs.subtitle') }}
|
|
</h2>
|
|
<ul class="logs-container">
|
|
<li
|
|
v-for="logEntry in latestLogs"
|
|
:key="generateKey(logEntry.timestamp)"
|
|
:class="logEntry.type"
|
|
>
|
|
<span class="log-entry-type">{{ logEntry.type }} :: </span>
|
|
<span
|
|
v-for="(arg, i) in logEntry.data"
|
|
:key="i"
|
|
>{{ arg }}</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="mb-40">
|
|
<h2 class="mb-20">
|
|
{{ t('about.diagnostic.resourceCounts.subtitle') }}
|
|
</h2>
|
|
<div class="resources-count-container">
|
|
<table
|
|
v-for="cluster in finalCounts"
|
|
:key="cluster.id"
|
|
class="full-width"
|
|
>
|
|
<thead @click="toggleTable(cluster.id)">
|
|
<th colspan="2">
|
|
<div>
|
|
<span>{{ cluster.id }}</span>
|
|
<i
|
|
class="icon"
|
|
:class="{
|
|
'icon-chevron-down': !cluster.isTableVisible,
|
|
'icon-chevron-up': cluster.isTableVisible
|
|
}"
|
|
></i>
|
|
</div>
|
|
</th>
|
|
</thead>
|
|
<tbody v-show="cluster.isTableVisible">
|
|
<tr v-for="item in cluster.counts" :key="item.resource">
|
|
<td>{{ item.resource }}</td>
|
|
<td>{{ item.count }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.title-block {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
|
|
h1 {
|
|
margin: 0 !important;
|
|
}
|
|
|
|
div {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
}
|
|
|
|
table {
|
|
border-collapse: collapse;
|
|
overflow: hidden;
|
|
border-radius: var(--border-radius);
|
|
|
|
&.full-width {
|
|
border-radius: 0 !important;
|
|
min-width: 60%;
|
|
|
|
thead {
|
|
cursor: pointer;
|
|
|
|
th div {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
}
|
|
}
|
|
|
|
tr > td:first-of-type {
|
|
width: 20%;
|
|
}
|
|
|
|
th, td {
|
|
border: 1px solid var(--border);
|
|
padding: 8px 5px;
|
|
min-width: 150px;
|
|
text-align: left;
|
|
}
|
|
|
|
th {
|
|
background-color: var(--sortable-table-top-divider);
|
|
border-bottom: 1px solid var(--sortable-table-top-divider);
|
|
}
|
|
|
|
a {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.os {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
}
|
|
|
|
.logs-container {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
background-color: #FFF;
|
|
border: 1px solid var(--body-text);
|
|
height: 300px;
|
|
overflow-x: hidden;
|
|
overflow-y: auto;
|
|
|
|
li {
|
|
font-family: $mono-font;
|
|
font-size: 12px;
|
|
margin: 0;
|
|
padding: 10px 20px;
|
|
border-bottom: 1px solid #ccc;
|
|
|
|
&.log {
|
|
color: #000;
|
|
}
|
|
|
|
&.warn {
|
|
background-color: LightGoldenRodYellow;
|
|
color: SaddleBrown;
|
|
}
|
|
|
|
&.info {
|
|
background-color: Azure;
|
|
color: blue;
|
|
}
|
|
|
|
&.error {
|
|
background-color: MistyRose;
|
|
color: red;
|
|
}
|
|
|
|
&:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.log-entry-type {
|
|
font-weight: bold;
|
|
}
|
|
}
|
|
}
|
|
|
|
.resources-count-container {
|
|
table:nth-child(even) {
|
|
th {
|
|
background-color: rgb(239, 239, 239);
|
|
color: #000;
|
|
}
|
|
}
|
|
}
|
|
</style>
|