Andy Lee b87f367583
fix: prevent VM detail page hang when static IP assigned (#1072)
Make syncStaticIpAnnotations idempotent so it no longer mutates VM
annotations when already in sync, breaking a reactive update loop that
froze the browser on the VM detail page. Also stack multiple IP
addresses vertically in the IP Address column.

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-31 16:55:25 +08:00

160 lines
3.9 KiB
Vue

<script>
import { get } from '@shell/utils/object';
import { isIpv4 } from '@shell/utils/string';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import CopyToClipboardText from '@shell/components/CopyToClipboardText';
import { HCI } from '../types';
import { MANAGEMENT_NETWORK } from '../mixins/harvester-vm';
import { OFF } from '../models/kubevirt.io.virtualmachine';
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: {
ips() {
if (this.staticAnnotationIP.length) {
return this.staticAnnotationIP;
}
if (this.vmiIp.length) {
return [...this.vmiIp, ...this.networkAnnotationIP]
.filter(Boolean)
.sort((a, b) => a.ip < b.ip ? -1 : 1);
}
return this.customAnnotationIP;
},
staticAnnotationIP() {
const annotations = this.row?.metadata?.annotations || {};
const staticIpPrefix = `${ HCI_ANNOTATIONS.STATIC_IP }/`;
return Object.entries(annotations)
.filter(([key, value]) => key.startsWith(staticIpPrefix) && !!value)
.map(([key, value]) => {
const nicName = key.slice(staticIpPrefix.length);
const ip = String(value).replace(/\/[\d\D]*/, '');
return {
ip,
name: nicName
};
})
.filter(({ ip }) => isIpv4(ip));
},
customAnnotationIP() {
const annotationIp = get(this.row, `metadata.annotations."${ HCI_ANNOTATIONS.CUSTOM_IP }"`);
if (annotationIp && isIpv4(annotationIp)) {
return [{
name: 'custom-ip', ip: annotationIp, isCustom: true
}];
}
return [];
},
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"
class="ip-list"
>
<div
v-for="{ ip, name, isCustom } in ips"
:key="`${ip}-${name}`"
class="ip-item"
>
<CopyToClipboardText
v-clean-tooltip="isCustom ? t('harvester.formatters.harvesterIpAddress.customIpTooltip') : name"
:text="ip"
:plain="isCustom"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.ip-list {
display: flex;
flex-direction: column;
gap: 6px;
}
</style>