Add pkg/harvester components + shell portings - 1

Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
Francesco Torchia
2024-10-23 17:00:46 +02:00
parent f8408469f7
commit 4f2688f6ab
265 changed files with 42316 additions and 82 deletions
@@ -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">
&mdash;
</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">/&nbsp;{{ 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">/&nbsp;{{ 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">/&nbsp;{{ 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">/&nbsp;{{ 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">
/&nbsp;{{ 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">
/&nbsp;{{ 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>