mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-01 03:36:04 +08:00
Add pkg/harvester components + shell portings - 1
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
<script>
|
||||
import { HCI } from '../types';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
row: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const vmList = this.$store.getters['harvester/all'](HCI.VM) || [];
|
||||
|
||||
return { vmList };
|
||||
},
|
||||
|
||||
computed: {
|
||||
vm() {
|
||||
const vm = this.vmList.find( (V) => V.id === `${ this.row.metadata.namespace }/${ this.value }`);
|
||||
|
||||
return vm;
|
||||
},
|
||||
|
||||
to() {
|
||||
return this.vm?.detailLocation;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-link
|
||||
v-if="to"
|
||||
:to="to"
|
||||
>
|
||||
{{ value }}
|
||||
</router-link>
|
||||
|
||||
<span v-else>
|
||||
{{ value }}
|
||||
</span>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script>
|
||||
import { HCI } from '@pkg/harvester/config/labels-annotations';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
displayLabel() {
|
||||
return this.value?.[HCI.CLOUD_INIT] === 'user' ? this.t('harvester.cloudTemplate.userData') : this.t('harvester.cloudTemplate.networkData');
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span> {{ displayLabel }} </span>
|
||||
</template>
|
||||
@@ -0,0 +1,107 @@
|
||||
<script>
|
||||
import ProgressBarMulti from '@shell/components/ProgressBarMulti';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterBackupProgressBar',
|
||||
components: { ProgressBarMulti },
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
isEmpty() {
|
||||
return this.value?.percentage === undefined;
|
||||
},
|
||||
status() {
|
||||
switch (this.value?.percentage) {
|
||||
case 0:
|
||||
return 'starting';
|
||||
case 100:
|
||||
return 'complete';
|
||||
|
||||
default:
|
||||
return 'progress';
|
||||
}
|
||||
},
|
||||
|
||||
percentage() {
|
||||
const value = Number(this.value.percentage);
|
||||
let color = 'bg-success';
|
||||
|
||||
if (value === 0) {
|
||||
color = 'bg-secondary';
|
||||
} else if (value < 30) {
|
||||
color = 'bg-darker';
|
||||
} else if (value < 70) {
|
||||
color = 'bg-warning';
|
||||
}
|
||||
|
||||
return [{
|
||||
value,
|
||||
color
|
||||
}];
|
||||
},
|
||||
|
||||
tooltip() {
|
||||
if (!this.value?.details?.volumes?.length) {
|
||||
return null;
|
||||
}
|
||||
const title = this.t(`harvester.${ this.value.type }.progress.details`);
|
||||
const rows = this.value.details.volumes.map(v => `<br><b>${ v.volumeName }</b>: ${ v.progress || 0 }%`);
|
||||
|
||||
return rows.reduce((acc, r) => acc + r, `${ title }<br>`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isEmpty" class="empty">
|
||||
<span class="text-muted">
|
||||
—
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="status != 'complete'"
|
||||
class="parent"
|
||||
>
|
||||
<div class="progress-box">
|
||||
<ProgressBarMulti
|
||||
v-clean-tooltip="tooltip"
|
||||
:values="percentage"
|
||||
:min="0"
|
||||
:max="100"
|
||||
/>
|
||||
</div>
|
||||
<div class="text">
|
||||
{{ value.percentage || 0 }}%
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ t('generic.completed') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.parent {
|
||||
display: grid;
|
||||
grid-template-areas: "progress text";
|
||||
grid-template-columns: auto 80px;
|
||||
align-items: center;
|
||||
.progress {
|
||||
background-color: darken(#EBEEF5, 15%);
|
||||
width: 100%;
|
||||
}
|
||||
.progress-box {
|
||||
grid-area: progress;
|
||||
}
|
||||
.text {
|
||||
grid-area: text;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script>
|
||||
import { HCI } from '../types';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const harvesterSettings = await this.$store.dispatch('harvester/findAll', { type: HCI.SETTING });
|
||||
|
||||
this.harvesterSettings = harvesterSettings;
|
||||
},
|
||||
|
||||
data() {
|
||||
return { harvesterSettings: [] };
|
||||
},
|
||||
|
||||
computed: {
|
||||
isMatch() {
|
||||
const harvesterSettings = this.$store.getters['harvester/all'](HCI.SETTING) || [];
|
||||
const resource = harvesterSettings.find( V => V.id === 'backup-target');
|
||||
|
||||
let isMatch = false;
|
||||
|
||||
try {
|
||||
isMatch = this.value === resource?.parseValue?.endpoint;
|
||||
} catch (e) {}
|
||||
|
||||
return isMatch;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isMatch">
|
||||
{{ value }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ value }}
|
||||
<p v-if="value" class="text-error">
|
||||
{{ t('harvester.backup.matchTarget') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,128 @@
|
||||
<script>
|
||||
import ConsumptionGauge from '@shell/components/ConsumptionGauge';
|
||||
import { METRIC, NODE } from '@shell/config/types';
|
||||
import { parseSi } from '@shell/utils/units';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterCpuUsed',
|
||||
components: { ConsumptionGauge },
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
|
||||
resourceName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
showUsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
|
||||
computed: {
|
||||
metrics() {
|
||||
return this.$store.getters['harvester/byId'](METRIC.NODE, this.row.id);
|
||||
},
|
||||
|
||||
cpuTotal() {
|
||||
let out = 0;
|
||||
|
||||
if (this.metrics) {
|
||||
out = this.metrics.cpuCapacity;
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
cpuUnits() {
|
||||
return 'C';
|
||||
},
|
||||
|
||||
node() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const node = this.$store.getters[`${ inStore }/byId`](NODE, this.row.id);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
reserved() {
|
||||
if (this.metrics) {
|
||||
return this.node.cpuReserved;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
|
||||
used() {
|
||||
if (this.metrics) {
|
||||
return parseSi(this.metrics?.usage?.cpu || '0m');
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ConsumptionGauge
|
||||
:capacity="cpuTotal"
|
||||
:used="reserved"
|
||||
:units="cpuUnits"
|
||||
:resource-name="resourceName"
|
||||
>
|
||||
<template #title="{amountTemplateValues, formattedPercentage}">
|
||||
<span>
|
||||
{{ t('clusterIndexPage.hardwareResourceGauge.reserved') }}
|
||||
</span>
|
||||
<span class="precent-data">
|
||||
{{ t('node.detail.glance.consumptionGauge.amount', amountTemplateValues) }}
|
||||
<span class="ml-10 percentage">/ {{ formattedPercentage }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</ConsumptionGauge>
|
||||
<div
|
||||
v-if="showUsed"
|
||||
class="mt-10"
|
||||
>
|
||||
<ConsumptionGauge
|
||||
:capacity="cpuTotal"
|
||||
:used="used"
|
||||
:units="cpuUnits"
|
||||
>
|
||||
<template #title="{amountTemplateValues, formattedPercentage}">
|
||||
<span>
|
||||
{{ t('clusterIndexPage.hardwareResourceGauge.used') }}
|
||||
</span>
|
||||
<span class="precent-data">
|
||||
{{ t('node.detail.glance.consumptionGauge.amount', amountTemplateValues) }}
|
||||
<span class="ml-10 percentage">/ {{ formattedPercentage }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</ConsumptionGauge>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.precent-data {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,109 @@
|
||||
<script>
|
||||
import { BadgeState } from '@components/BadgeState';
|
||||
import { stateDisplay } from '@shell/plugins/dashboard-store/resource-class';
|
||||
|
||||
const ACTIVE = 'healthy';
|
||||
const WARNING = 'warning';
|
||||
|
||||
export default {
|
||||
components: { BadgeState },
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
diskState() {
|
||||
const longhornDisks = this.row?.longhornDisks || [];
|
||||
const out = longhornDisks.reduce((state, disk) => {
|
||||
if (disk?.readyCondition?.status !== 'True' || disk?.schedulableCondition?.status !== 'True') {
|
||||
state = WARNING;
|
||||
}
|
||||
|
||||
return state;
|
||||
}, ACTIVE);
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
stateDisplay() {
|
||||
return stateDisplay(this.diskState);
|
||||
},
|
||||
|
||||
stateBackground() {
|
||||
if ( this.diskState === ACTIVE ) {
|
||||
return 'bg-success';
|
||||
} else {
|
||||
return 'bg-warning';
|
||||
}
|
||||
},
|
||||
|
||||
errorMessage() {
|
||||
if (this.diskState !== ACTIVE) {
|
||||
return this.t('harvester.host.disk.error');
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
|
||||
warningMessage() {
|
||||
const blockDevices = this.row?.unProvisionedDisks || [];
|
||||
|
||||
const out = [];
|
||||
|
||||
blockDevices.map((b) => {
|
||||
if (b.metadata.state.error) {
|
||||
out.push(b.metadata.state.message);
|
||||
}
|
||||
});
|
||||
|
||||
return out;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="state">
|
||||
<BadgeState
|
||||
v-clean-tooltip="errorMessage"
|
||||
:color="stateBackground"
|
||||
:label="stateDisplay"
|
||||
/>
|
||||
<v-popover
|
||||
v-if="warningMessage.length"
|
||||
trigger="hover"
|
||||
offset="16"
|
||||
>
|
||||
<span class="tooltip-target ml-5">
|
||||
<i class="icon icon-warning icon-lg text-warning" />
|
||||
</span>
|
||||
|
||||
<template slot="popover">
|
||||
<p
|
||||
v-for="(message, index) in warningMessage"
|
||||
:key="message"
|
||||
>
|
||||
{{ index + 1 }}. {{ message }}
|
||||
</p>
|
||||
</template>
|
||||
</v-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.state {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
|
||||
.icon-warning {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script>
|
||||
import { NODE } from '@shell/config/types';
|
||||
import CopyToClipboardText from '@shell/components/CopyToClipboardText';
|
||||
|
||||
export default {
|
||||
components: { CopyToClipboardText },
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
node() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return this.$store.getters[`${ inStore }/byId`](NODE, this.value);
|
||||
},
|
||||
|
||||
ip() {
|
||||
return this.node?.internalIp;
|
||||
},
|
||||
|
||||
nameDisplay() {
|
||||
return this.node?.nameDisplay || '';
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CopyToClipboardText v-clean-tooltip="ip" :text="nameDisplay" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,103 @@
|
||||
<script>
|
||||
import { OFF } from '../models/kubevirt.io.virtualmachine';
|
||||
import { get } from '@shell/utils/object';
|
||||
import { isIpv4 } from '@shell/utils/string';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import { HCI } from '../types';
|
||||
import { MANAGEMENT_NETWORK } from '../mixins/harvester-vm';
|
||||
import CopyToClipboardText from '@shell/components/CopyToClipboardText';
|
||||
|
||||
export default {
|
||||
components: { CopyToClipboardText },
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
col: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return { inStore: 'harvester' };
|
||||
},
|
||||
|
||||
created() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
this.inStore = inStore;
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Return VM instance IP and VM annotation IP
|
||||
ips() {
|
||||
return [...this.vmiIp, ...this.networkAnnotationIP]
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.ip < b.ip ? -1 : 1);
|
||||
},
|
||||
|
||||
networkAnnotationIP() {
|
||||
if (this.row.actualState !== 'Running') { // TODO: Running
|
||||
return [];
|
||||
}
|
||||
|
||||
const annotationIp = get(this.row, `metadata.annotations."${ HCI_ANNOTATIONS.NETWORK_IPS }"`) || '[]';
|
||||
|
||||
// Obtain IP from VM annotation, remove the CIDR suffix number if CIDR Exist
|
||||
try {
|
||||
const out = JSON.parse(annotationIp);
|
||||
|
||||
return out.map( ip => ({
|
||||
ip: ip.replace(/\/[\d\D]*/, ''),
|
||||
name: ''
|
||||
}));
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
vmiIp() {
|
||||
const vmiResources = this.$store.getters[`${ this.inStore }/all`](HCI.VMI);
|
||||
const resource = vmiResources.find(VMI => VMI.id === this.value) || null;
|
||||
const networksName = this.row.networksName || [];
|
||||
const vmiNetworks = resource?.spec?.networks || [];
|
||||
|
||||
return (resource?.status?.interfaces || []).filter((intf) => {
|
||||
return isIpv4(intf.ipAddress) && networksName.includes(intf.name);
|
||||
}).map((intf) => {
|
||||
let name;
|
||||
const network = vmiNetworks.find(network => network.name === intf.name);
|
||||
|
||||
if (network && network.multus) {
|
||||
name = network.multus.networkName;
|
||||
} else if (network && network.pod) {
|
||||
name = MANAGEMENT_NETWORK;
|
||||
}
|
||||
|
||||
return {
|
||||
ip: intf.ipAddress,
|
||||
name
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
showIP() {
|
||||
return this.row.stateDisplay !== OFF;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="showIP">
|
||||
<span v-for="{ip, name} in ips" :key="ip">
|
||||
<CopyToClipboardText v-clean-tooltip="name" :text="ip" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script>
|
||||
import CopyToClipboardText from '@shell/components/CopyToClipboardText';
|
||||
import { isMaybeSecure } from '@shell/utils/url';
|
||||
|
||||
export default {
|
||||
components: { CopyToClipboardText },
|
||||
|
||||
props: {
|
||||
row: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
col: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
parsed() {
|
||||
const row = this.row || {};
|
||||
const listeners = row?.spec?.listeners || [];
|
||||
const out = [];
|
||||
const address = row.status?.address;
|
||||
|
||||
if (!address) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (listeners.length > 0) {
|
||||
listeners.forEach((p) => {
|
||||
let proxyUrl;
|
||||
|
||||
const hidePort = [80, 443].includes(p.port);
|
||||
|
||||
if (p?.protocol === 'TCP' && hidePort) {
|
||||
if (isMaybeSecure(p.port, p?.protocol)) {
|
||||
proxyUrl = `https://${ address }:${ p.port }`;
|
||||
} else {
|
||||
proxyUrl = `http://${ address }:${ p.port }`;
|
||||
}
|
||||
} else {
|
||||
proxyUrl = `http://${ address }:${ p.port }`;
|
||||
}
|
||||
|
||||
let label = address;
|
||||
|
||||
if (!hidePort) {
|
||||
label = `${ address }:${ p.port }`;
|
||||
}
|
||||
|
||||
const html = `<a href="${ proxyUrl }" target="_blank" rel="noopener noreferrer nofollow">${ label }</a>`;
|
||||
|
||||
out.push({ html, label });
|
||||
});
|
||||
} else {
|
||||
out.push({ html: address, label: address });
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-for="(port, index) in parsed"
|
||||
:key="index"
|
||||
>
|
||||
<CopyToClipboardText :text="port.label" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,146 @@
|
||||
<script>
|
||||
import ConsumptionGauge from '@shell/components/ConsumptionGauge';
|
||||
import { METRIC, NODE } from '@shell/config/types';
|
||||
import { formatSi, exponentNeeded, UNITS, parseSi } from '@shell/utils/units';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterMemoryUsed',
|
||||
components: { ConsumptionGauge },
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
|
||||
resourceName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
showUsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
|
||||
computed: {
|
||||
metrics() {
|
||||
return this.$store.getters['harvester/byId'](METRIC.NODE, this.row.id);
|
||||
},
|
||||
|
||||
memoryTotal() {
|
||||
let out = 0;
|
||||
|
||||
if (this.metrics) {
|
||||
out = this.metrics.memoryCapacity;
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
memoryUnits() {
|
||||
const exponent = exponentNeeded(this.memoryTotal, 1024);
|
||||
|
||||
return `${ UNITS[exponent] }iB`;
|
||||
},
|
||||
|
||||
node() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const node = this.$store.getters[`${ inStore }/byId`](NODE, this.row.id);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
reserved() {
|
||||
if (this.metrics) {
|
||||
return this.node.memoryReserved;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
|
||||
used() {
|
||||
if (this.metrics) {
|
||||
return parseSi(this.metrics?.usage?.memory || '0m', { increment: 1024 });
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
memoryFormatter(value) {
|
||||
const exponent = exponentNeeded(this.memoryTotal, 1024);
|
||||
|
||||
const formatOptions = {
|
||||
addSuffix: false,
|
||||
increment: 1024,
|
||||
minExponent: exponent
|
||||
};
|
||||
|
||||
return formatSi(value, formatOptions);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ConsumptionGauge
|
||||
:capacity="memoryTotal"
|
||||
:used="reserved"
|
||||
:units="memoryUnits"
|
||||
:number-formatter="memoryFormatter"
|
||||
:resource-name="resourceName"
|
||||
>
|
||||
<template #title="{amountTemplateValues, formattedPercentage}">
|
||||
<span>
|
||||
{{ t('clusterIndexPage.hardwareResourceGauge.reserved') }}
|
||||
</span>
|
||||
<span class="precent-data">
|
||||
{{ t('node.detail.glance.consumptionGauge.amount', amountTemplateValues) }}
|
||||
<span class="ml-10 percentage">/ {{ formattedPercentage }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</ConsumptionGauge>
|
||||
<div
|
||||
v-if="showUsed"
|
||||
class="mt-10"
|
||||
>
|
||||
<ConsumptionGauge
|
||||
:capacity="memoryTotal"
|
||||
:used="used"
|
||||
:units="memoryUnits"
|
||||
:number-formatter="memoryFormatter"
|
||||
>
|
||||
<template #title="{amountTemplateValues, formattedPercentage}">
|
||||
<span>
|
||||
{{ t('clusterIndexPage.hardwareResourceGauge.used') }}
|
||||
</span>
|
||||
<span class="precent-data">
|
||||
{{ t('node.detail.glance.consumptionGauge.amount', amountTemplateValues) }}
|
||||
<span class="ml-10 percentage">/ {{ formattedPercentage }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</ConsumptionGauge>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.precent-data {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script>
|
||||
import { HCI } from '../types';
|
||||
import { BadgeState } from '@components/BadgeState';
|
||||
|
||||
export default {
|
||||
components: { BadgeState },
|
||||
|
||||
props: {
|
||||
vmResource: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
showSuccess: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
vmiResource() {
|
||||
const vmiList = this.$store.getters['harvester/all'](HCI.VMI) || [];
|
||||
const vmi = vmiList.find( (VMI) => {
|
||||
return VMI?.metadata?.ownerReferences?.[0]?.uid === this.vmResource?.metadata?.uid;
|
||||
});
|
||||
|
||||
return vmi;
|
||||
},
|
||||
migrationState() {
|
||||
return this.vmiResource?.migrationState?.status || '';
|
||||
},
|
||||
migrationBackground() {
|
||||
return this.vmiResource?.migrationStateBackground || '';
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
migrationState(neu) {
|
||||
this.$emit('state-changed', neu);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="migrationState">
|
||||
<span v-if="!showSuccess">/</span>
|
||||
<BadgeState :label="migrationState" :color="migrationBackground" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.badge-state {
|
||||
padding: 5px 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 20px;
|
||||
|
||||
&.bg-info {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
&.bg-error {
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
&.bg-warning {
|
||||
border-color: var(--warning);
|
||||
}
|
||||
|
||||
// Successful states are de-emphasized by using [text-]color instead of background-color
|
||||
&.bg-success {
|
||||
color: var(--success);
|
||||
border-color: var(--success);
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.sortable-table TD .badge-state {
|
||||
@include clip;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
padding: 2px 10px 1px 10px;
|
||||
font-size: 1em;
|
||||
max-width: 200px;
|
||||
font-size: .85em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,191 @@
|
||||
<script>
|
||||
import ConsumptionGauge from '@shell/components/ConsumptionGauge';
|
||||
import { LONGHORN } from '@shell/config/types';
|
||||
import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterStorageUsed',
|
||||
components: { ConsumptionGauge },
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
|
||||
resourceName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
showReserved: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
|
||||
computed: {
|
||||
usage() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const longhornNode = this.$store.getters[`${ inStore }/byId`](LONGHORN.NODES, `longhorn-system/${ this.row.id }`) || {};
|
||||
|
||||
return longhornNode?.used || 0;
|
||||
},
|
||||
|
||||
reserved() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const longhornNode = this.$store.getters[`${ inStore }/byId`](LONGHORN.NODES, `longhorn-system/${ this.row.id }`);
|
||||
let reserved = 0;
|
||||
|
||||
const disks = longhornNode?.spec?.disks || {};
|
||||
|
||||
Object.values(disks).map((disk) => {
|
||||
if (disk.allowScheduling) {
|
||||
reserved += disk.storageReserved;
|
||||
}
|
||||
});
|
||||
|
||||
return reserved;
|
||||
},
|
||||
|
||||
total() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const longhornNode = this.$store.getters[`${ inStore }/byId`](LONGHORN.NODES, `longhorn-system/${ this.row.id }`);
|
||||
let out = 0;
|
||||
|
||||
const diskStatus = longhornNode?.status?.diskStatus || {};
|
||||
|
||||
Object.values(diskStatus).map((disk) => {
|
||||
if (disk?.storageMaximum) {
|
||||
out += disk.storageMaximum;
|
||||
}
|
||||
});
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
units() {
|
||||
const exponent = exponentNeeded(this.total, 1024);
|
||||
|
||||
return `${ UNITS[exponent] }iB`;
|
||||
},
|
||||
|
||||
used() {
|
||||
let out = this.formatter(this.usage || 0);
|
||||
|
||||
if (!Number.parseFloat(out) > 0) {
|
||||
out = this.formatter(this.usage || 0, { canRoundToZero: false });
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
formatReserved() {
|
||||
let out = this.formatter(this.reserved || 0);
|
||||
|
||||
if (!Number.parseFloat(out) > 0) {
|
||||
out = this.formatter(this.reserved || 0, { canRoundToZero: false });
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
usedAmountTemplateValues() {
|
||||
return {
|
||||
used: this.used,
|
||||
total: this.formatter(this.total || 0),
|
||||
unit: this.units,
|
||||
};
|
||||
},
|
||||
|
||||
reservedAmountTemplateValues() {
|
||||
return {
|
||||
used: this.formatReserved,
|
||||
total: this.formatter(this.total || 0),
|
||||
unit: this.units,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
formatter(value, format) {
|
||||
const minExponent = exponentNeeded(this.total, 1024);
|
||||
const formatOptions = {
|
||||
addSuffix: false,
|
||||
increment: 1024,
|
||||
minExponent,
|
||||
};
|
||||
|
||||
return formatSi(value, {
|
||||
...formatOptions,
|
||||
...format,
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="showReserved"
|
||||
>
|
||||
<ConsumptionGauge
|
||||
:capacity="total"
|
||||
:used="reserved"
|
||||
:units="units"
|
||||
:number-formatter="formatter"
|
||||
:resource-name="resourceName"
|
||||
>
|
||||
<template #title="{formattedPercentage}">
|
||||
<span>
|
||||
{{ t('clusterIndexPage.hardwareResourceGauge.reserved') }}
|
||||
</span>
|
||||
<span class="precent-data">
|
||||
{{ t('node.detail.glance.consumptionGauge.amount', reservedAmountTemplateValues) }}
|
||||
<span class="ml-10 percentage">
|
||||
/ {{ formattedPercentage }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</ConsumptionGauge>
|
||||
</div>
|
||||
<ConsumptionGauge
|
||||
:capacity="total"
|
||||
:used="usage"
|
||||
:units="units"
|
||||
:number-formatter="formatter"
|
||||
:resource-name="showReserved ? '' : resourceName"
|
||||
:class="{
|
||||
'mt-10': showReserved,
|
||||
}"
|
||||
>
|
||||
<template #title="{formattedPercentage}">
|
||||
<span>
|
||||
{{ t('node.detail.glance.consumptionGauge.used') }}
|
||||
</span>
|
||||
<span class="precent-data">
|
||||
{{ t('node.detail.glance.consumptionGauge.amount', usedAmountTemplateValues) }}
|
||||
<span class="ml-10 percentage">
|
||||
/ {{ formattedPercentage }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</ConsumptionGauge>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.precent-data {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<script>
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterVFAddress',
|
||||
|
||||
props: {
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return { showAll: false, limitedNumbers: 3 };
|
||||
},
|
||||
|
||||
computed: {
|
||||
allVFs() {
|
||||
return this.row.status?.vfAddresses || [];
|
||||
},
|
||||
|
||||
rows() {
|
||||
const out = this.allVFs.map((O) => {
|
||||
const [prefix, middle, suffix] = O.split(':');
|
||||
const q = `${ this.row.spec?.nodeName }-${ prefix }${ middle }${ suffix.replace('.', '') }`;
|
||||
const to = {
|
||||
name: `${ HARVESTER_PRODUCT }-c-cluster-resource`,
|
||||
params: { cluster: this.$store.getters['clusterId'], resource: this.row.childDevice },
|
||||
query: { q }
|
||||
};
|
||||
|
||||
return {
|
||||
to,
|
||||
name: O
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
visible: out.slice(0, this.limitedNumbers),
|
||||
invisible: out.slice(this.limitedNumbers)
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vfs">
|
||||
<span
|
||||
v-for="(vf, index) in rows.visible"
|
||||
:key="vf.name"
|
||||
>
|
||||
<router-link
|
||||
:to="vf.to"
|
||||
>
|
||||
{{ vf.name }}
|
||||
</router-link>
|
||||
|
||||
<span v-if="index < rows.visible.length - 1">, </span>
|
||||
</span>
|
||||
|
||||
<v-popover
|
||||
trigger="click"
|
||||
placement="top"
|
||||
>
|
||||
<span v-if="allVFs.length > limitedNumbers">
|
||||
, <a
|
||||
href="javascript:void(0)"
|
||||
class="show-more"
|
||||
@click.prevent="showAll = !showAll"
|
||||
>
|
||||
{{ t('harvester.sriov.showMore') }}
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<template v-slot:popover>
|
||||
<div class="vfs-popup">
|
||||
<div>
|
||||
<span
|
||||
v-for="(vf, index) in rows.invisible"
|
||||
:key="vf.name"
|
||||
>
|
||||
<router-link
|
||||
:to="vf.to"
|
||||
>
|
||||
{{ vf.name }}
|
||||
</router-link>
|
||||
|
||||
<span v-if="index < rows.invisible.length - 1">, </span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.vfs {
|
||||
min-width: 380px;
|
||||
|
||||
.show-more {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.popover .popover-inner .vfs-popup a {
|
||||
color: var(--link);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--body-text);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script>
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterVFsNum',
|
||||
|
||||
props: {
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
to() {
|
||||
return {
|
||||
name: `${ HARVESTER_PRODUCT }-c-cluster-resource`,
|
||||
params: { cluster: this.$store.getters['clusterId'], resource: this.row.childDevice },
|
||||
query: { parentSriov: this.row.id }
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vfs-num">
|
||||
<router-link
|
||||
:to="to"
|
||||
>
|
||||
{{ row.numVFs }}
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,113 @@
|
||||
<script>
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterVGpuDevices',
|
||||
|
||||
props: {
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return { showAll: false, limitedNumbers: 2 };
|
||||
},
|
||||
|
||||
computed: {
|
||||
allVGpuDevices() {
|
||||
return this.row.status?.vGPUDevices || [];
|
||||
},
|
||||
|
||||
rows() {
|
||||
const out = this.allVGpuDevices.map((device) => {
|
||||
const to = {
|
||||
name: `${ HARVESTER_PRODUCT }-c-cluster-resource`,
|
||||
params: { cluster: this.$store.getters['clusterId'], resource: this.row.childDevice },
|
||||
query: { q: device }
|
||||
};
|
||||
|
||||
return {
|
||||
to,
|
||||
name: device
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
visible: out.slice(0, this.limitedNumbers),
|
||||
invisible: out.slice(this.limitedNumbers)
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vgpudevices">
|
||||
<span
|
||||
v-for="(vgpu, index) in rows.visible"
|
||||
:key="vgpu.name"
|
||||
>
|
||||
<router-link
|
||||
:to="vgpu.to"
|
||||
>
|
||||
{{ vgpu.name }}
|
||||
</router-link>
|
||||
|
||||
<span v-if="index < rows.visible.length - 1">, </span>
|
||||
</span>
|
||||
|
||||
<v-popover
|
||||
trigger="click"
|
||||
placement="top"
|
||||
>
|
||||
<span v-if="allVGpuDevices.length > limitedNumbers">
|
||||
, <a
|
||||
href="javascript:void(0)"
|
||||
class="show-more"
|
||||
@click.prevent="showAll = !showAll"
|
||||
>
|
||||
{{ t('harvester.sriovgpu.showMore') }}
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<template v-slot:popover>
|
||||
<div class="vgpu-popup">
|
||||
<div>
|
||||
<span
|
||||
v-for="(vgpu, index) in rows.invisible"
|
||||
:key="vgpu.name"
|
||||
>
|
||||
<router-link
|
||||
:to="vgpu.to"
|
||||
>
|
||||
{{ vgpu.name }}
|
||||
</router-link>
|
||||
|
||||
<span v-if="index < rows.invisible.length - 1">, </span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vgpudevices {
|
||||
min-width: 380px;
|
||||
|
||||
.show-more {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.popover .popover-inner .vgpu-popup a {
|
||||
color: var(--link);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--body-text);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,175 @@
|
||||
<script>
|
||||
import VMState from '@shell/components/formatter/BadgeStateFormatter';
|
||||
import HarvesterMigrationState from './HarvesterMigrationState';
|
||||
|
||||
export default {
|
||||
components: { VMState, HarvesterMigrationState },
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
|
||||
allNodeNetwork: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
allClusterNetwork: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isMigrating: false,
|
||||
timer: null,
|
||||
};
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
warningMessage() {
|
||||
const out = [];
|
||||
|
||||
if (this.row?.actualState === 'VM error' && this.row.warningMessage?.message) {
|
||||
out.push(this.row.warningMessage?.message);
|
||||
}
|
||||
|
||||
if (this.row?.migrationMessage) {
|
||||
out.push(this.row?.migrationMessage.message);
|
||||
}
|
||||
|
||||
if (this.row.warningMessage?.message) {
|
||||
if (this.row.warningMessage?.pod) {
|
||||
const pod = this.row.warningMessage.pod;
|
||||
|
||||
if (pod.metadata?.state?.error && !/pod has unbound immediate PersistentVolumeClaims/.test(pod.metadata?.state?.message)) {
|
||||
out.push(pod.metadata?.state?.message);
|
||||
}
|
||||
} else {
|
||||
out.push(this.row.warningMessage?.message);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
migrationStateChanged(neu) {
|
||||
if (neu === 'Failed') {
|
||||
this.isMigrating = false;
|
||||
} else {
|
||||
this.isMigrating = !!neu;
|
||||
}
|
||||
},
|
||||
|
||||
showMessage(show) {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
if (this.$refs.popover) {
|
||||
if (show) {
|
||||
this.$refs.popover.show();
|
||||
} else {
|
||||
this.$refs.popover.hide();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
delayClose() {
|
||||
this.timer = setTimeout(() => {
|
||||
this.showMessage(false);
|
||||
}, 500);
|
||||
},
|
||||
|
||||
async dismiss() {
|
||||
try {
|
||||
await this.row.doAction('dismissInsufficientResourceQuota');
|
||||
} catch (err) {
|
||||
if (err?._status === 400 || err?._status === 503) {
|
||||
this.$store.dispatch('growl/error', {
|
||||
title: this.t('harvester.notification.title.error'),
|
||||
message: err?.errors[0]
|
||||
}, { root: true });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
canMiss(row, message) {
|
||||
return row.warningMessage?.message === message && row.warningMessage?.canDismiss;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<HarvesterMigrationState v-show="isMigrating" :vm-resource="row" @state-changed="migrationStateChanged" />
|
||||
<div v-show="!isMigrating" class="state">
|
||||
<VMState :row="row" />
|
||||
<v-popover
|
||||
v-if="warningMessage.length"
|
||||
ref="popover"
|
||||
trigger="manual"
|
||||
offset="16"
|
||||
>
|
||||
<span
|
||||
class="tooltip-target"
|
||||
@mouseenter="showMessage(true)"
|
||||
@mouseleave="delayClose()"
|
||||
>
|
||||
<i class="icon icon-warning icon-lg text-warning" />
|
||||
</span>
|
||||
|
||||
<template slot="popover">
|
||||
<div
|
||||
@mouseenter="showMessage(true)"
|
||||
@mouseleave="showMessage(false)"
|
||||
>
|
||||
<p v-for="(message, index) in warningMessage" :key="message">
|
||||
{{ index + 1 }}.
|
||||
<a
|
||||
v-if="canMiss(row, message)"
|
||||
class="text-link"
|
||||
role="button"
|
||||
@click="dismiss"
|
||||
>
|
||||
{{ t('harvester.upgradePage.dismissMessage') }}
|
||||
</a>
|
||||
{{ message }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</v-popover>
|
||||
</div>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.state {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.icon-warning {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script>
|
||||
import ProgressBarMulti from '@shell/components/ProgressBarMulti';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import VolumeState from '@shell/components/formatter/BadgeStateFormatter';
|
||||
|
||||
export default {
|
||||
components: { VolumeState, ProgressBarMulti },
|
||||
props: {
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
warningMessage() {
|
||||
return this.row.relatedPV?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_ERROR];
|
||||
},
|
||||
|
||||
rebuildStatus() {
|
||||
return this.row.longhornEngine?.status?.rebuildStatus;
|
||||
},
|
||||
|
||||
isRebuilding() {
|
||||
return this.rebuildStatus && Object.keys(this.rebuildStatus).length > 0;
|
||||
},
|
||||
|
||||
percentage() {
|
||||
if (!this.isRebuilding) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rebuildStatus = Object.values(this.rebuildStatus)?.[0];
|
||||
const value = rebuildStatus?.progress;
|
||||
|
||||
return [{
|
||||
value,
|
||||
color: 'bg-warning'
|
||||
}];
|
||||
},
|
||||
|
||||
rebuildingMessage() {
|
||||
return this.$store.getters['i18n/t']('harvester.volume.rebuildingMessage', { percentage: this.percentage[0].value });
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<div>
|
||||
<ProgressBarMulti
|
||||
v-if="isRebuilding"
|
||||
v-clean-tooltip="rebuildingMessage"
|
||||
:values="percentage"
|
||||
:min="0"
|
||||
:max="100"
|
||||
class="mb-10"
|
||||
/>
|
||||
</div>
|
||||
<div class="state">
|
||||
<VolumeState :row="row" />
|
||||
<v-popover
|
||||
v-if="!!warningMessage"
|
||||
trigger="hover"
|
||||
offset="16"
|
||||
>
|
||||
<span class="tooltip-target">
|
||||
<i class="icon icon-warning icon-lg text-warning" />
|
||||
</span>
|
||||
|
||||
<template slot="popover">
|
||||
<p class="warning-message">
|
||||
{{ warningMessage }}
|
||||
</p>
|
||||
</template>
|
||||
</v-popover>
|
||||
</div>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.state {
|
||||
display: flex;
|
||||
|
||||
.icon-warning {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
.warning-message:first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script>
|
||||
import BadgeStateFormatter from '@shell/components/formatter/BadgeStateFormatter';
|
||||
|
||||
export default {
|
||||
components: { BadgeStateFormatter },
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
col: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
|
||||
arbitrary: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="value !== 'N/A'" class="text">
|
||||
<BadgeStateFormatter class="ml-10" :arbitrary="arbitrary" :row="row" :value="value" />
|
||||
</div>
|
||||
<div v-else class="text">
|
||||
{{ value }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.text {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
row: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
to() {
|
||||
const to = this.row?.detailLocation;
|
||||
|
||||
to.hash = '#instances';
|
||||
|
||||
return to;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-link
|
||||
v-if="value.length > 0"
|
||||
:to="to"
|
||||
>
|
||||
{{ value.length }}
|
||||
</router-link>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script>
|
||||
import { PVC } from '@shell/config/types';
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
row: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
async fetch() {
|
||||
this.volumeList = await this.$store.dispatch('harvester/findAll', { type: PVC });
|
||||
},
|
||||
data() {
|
||||
return { volumeList: [] };
|
||||
},
|
||||
computed: {
|
||||
volume() {
|
||||
const namespace = this.row?.metadata?.namespace;
|
||||
const volume = this.volumeList.find( (V) => V.id === `${ namespace }/${ this.value }`) || {};
|
||||
|
||||
return volume;
|
||||
},
|
||||
to() {
|
||||
return this.volume?.detailLocation;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<router-link
|
||||
v-if="to"
|
||||
:to="to"
|
||||
>
|
||||
{{ value }}
|
||||
</router-link>
|
||||
<span v-else>
|
||||
{{ value }}
|
||||
</span>
|
||||
</template>
|
||||
@@ -0,0 +1,59 @@
|
||||
<script>
|
||||
import State from '@shell/components/formatter/BadgeStateFormatter';
|
||||
|
||||
export default {
|
||||
components: { State },
|
||||
|
||||
props: {
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
warningMessages() {
|
||||
return this.row.warningMessages;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<div class="state">
|
||||
<State
|
||||
:row="row"
|
||||
/>
|
||||
<v-popover
|
||||
v-if="warningMessages.length > 0"
|
||||
trigger="hover"
|
||||
offset="16"
|
||||
>
|
||||
<span class="tooltip-target">
|
||||
<i class="icon icon-warning icon-lg text-warning" />
|
||||
</span>
|
||||
|
||||
<template v-slot:popover>
|
||||
<p
|
||||
v-for="(message, index) in warningMessages"
|
||||
:key="message.text"
|
||||
>
|
||||
{{ index + 1 }}.
|
||||
{{ message.text }}
|
||||
</p>
|
||||
</template>
|
||||
</v-popover>
|
||||
</div>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.state {
|
||||
display: flex;
|
||||
|
||||
.icon-warning {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user