Files
harvester-ui-extension/pkg/harvester/list/kubeovn.io.vpc.vue
T
marcelofukumoto 77b643540e fix: label the VPC group of subnets without a VPC as None (#1210)
Kube-OVN sets a subnet's VPC (ovn-cluster when none is given), but a
subnet it hasn't processed, such as one with an invalid provider, has
none. The VPC page grouped those under "Virtual Private Cloud:" with an
empty name, and its Create Subnet, Topology and actions buttons pointed
at no VPC.

Show None as the VPC name for that group and leave out its VPC buttons.

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
2026-10-29 17:00:00 +01:00

314 lines
7.6 KiB
Vue

<script>
import Loading from '@shell/components/Loading';
import ResourceTable from '@shell/components/ResourceTable';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers';
import { PROVIDER, PROTOCOL, CIDR_BLOCK } from '@pkg/harvester/config/table-headers';
import { HCI } from '../types';
import { VPC } from '../config/query-params';
import { ADD_ONS } from '../config/harvester-map';
import { allHash } from '@shell/utils/promise';
import MessageLink from '@shell/components/MessageLink';
import Banner from '@components/Banner/Banner.vue';
export default {
name: 'HarvesterVPC',
components: {
ResourceTable,
Loading,
MessageLink,
Banner
},
inheritAttrs: false,
props: {
schema: {
type: Object,
required: true,
}
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = await allHash({ addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }) });
this.enabledKubeOvnAddon = hash.addons.find((addon) => addon.name === ADD_ONS.KUBEOVN_OPERATOR)?.spec?.enabled === true;
if (this.enabledKubeOvnAddon) {
try {
await allHash({
rows: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SUBNET }),
vpcs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
});
this.$store.dispatch('type-map/configureType', { match: HCI.SUBNET, isCreatable: this.enabledKubeOvnAddon });
} catch (e) {
// eslint-disable-next-line no-console
console.error('Error fetching VPC and Subnet data:', e);
}
}
},
data() {
return {
HCI,
hasBothSchema: false,
enabledKubeOvnAddon: false,
to: `${ HCI.ADD_ONS }/kube-system/${ ADD_ONS.KUBEOVN_OPERATOR }?mode=edit`
};
},
computed: {
headers() {
return [
STATE,
NAME,
NAMESPACE,
CIDR_BLOCK,
PROTOCOL,
{
name: 'vlan',
labelKey: 'harvester.subnet.vlan.label',
value: 'spec.vlan',
sort: 'spec.vlan',
formatter: 'HarvesterVlan',
},
PROVIDER,
AGE
];
},
rows() {
return this.$store.getters[`harvester/all`](HCI.SUBNET) || [];
},
vpcWithoutSubnets() {
const vpcs = this.$store.getters[`harvester/all`](HCI.VPC) || [];
const out = vpcs.map((v) => {
const hasChild = v.status?.subnets?.length > 0 || false;
return {
...v,
hasChild
};
});
return out;
},
isSubnetCreatable() {
return (this.subnetSchema?.collectionMethods || []).includes('POST');
},
rowsWithFakeVpcs() {
const fakeRows = this.vpcWithoutSubnets.map((vpc) => {
return {
groupByLabel: vpc.id,
isFake: true,
mainRowKey: vpc.id,
nameDisplay: vpc.id,
groupByVpc: vpc.id,
availableActions: []
};
});
return [...this.rows, ...fakeRows];
},
createVPCLocation() {
const location = {
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
params: {
product: HARVESTER_PRODUCT,
resource: HCI.VPC,
},
};
return location;
},
vpcSchema() {
return this.$store.getters[`harvester/schemaFor`](HCI.VPC);
},
subnetSchema() {
return this.$store.getters[`harvester/schemaFor`](HCI.SUBNET);
},
},
methods: {
groupLabel(group) {
// Kube-OVN sets a subnet's VPC (ovn-cluster when none is given), so an empty one is a subnet it hasn't processed
return `${ this.t('harvester.vpc.label') }: ${ group.key || this.t('generic.none') }`;
},
slotName(vpc) {
return `main-row:${ vpc }`;
},
createSubnetLocation(group) {
const vpc = group.key;
const location = {
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
params: {
product: HARVESTER_PRODUCT,
resource: HCI.SUBNET,
},
};
location.query = { [VPC]: vpc };
return location;
},
viewTopology(group) {
const vpc = group.key;
const resource = this.$store.getters[`harvester/byId`](HCI.VPC, vpc);
if (resource && resource.goToDetail) {
resource.goToDetail();
}
},
showVpcAction(event, group) {
const vpc = group.key;
const resource = this.$store.getters[`harvester/byId`](HCI.VPC, vpc);
this.$store.commit(`action-menu/show`, {
resources: [resource],
elem: event.target
});
},
},
typeDisplay() {
return this.t('harvester.vpc.label');
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else-if="!enabledKubeOvnAddon">
<Banner color="warning">
<MessageLink
:to="to"
prefix-label="harvester.vpc.noAddonEnabled.prefix"
middle-label="harvester.vpc.noAddonEnabled.middle"
suffix-label="harvester.vpc.noAddonEnabled.suffix"
/>
</Banner>
</div>
<div v-else>
<Masthead
:schema="vpcSchema"
:type-display="t('harvester.vpc.label')"
:resource="HCI.VPC"
:create-location="createVPCLocation"
:create-button-label="t('harvester.clusterNetwork.create.button.label')"
/>
<ResourceTable
:rows="rowsWithFakeVpcs"
:headers="headers"
:schema="subnetSchema"
:groupable="true"
group-by="groupByVpc"
>
<template #header-middle>
<div />
</template>
<template #group-by="{group}">
<div class="group-bar">
<div class="group-tab">
<span>
{{ groupLabel(group) }}
</span>
</div>
<div
v-if="group.key"
class="right"
>
<router-link
v-if="isSubnetCreatable"
class="btn btn-sm role-secondary mr-5"
:to="createSubnetLocation(group)"
>
{{ t('harvester.vpc.createSubnet') }}
</router-link>
<button
type="button"
class="btn btn-sm role-secondary mr-5"
@click="viewTopology(group)"
>
<i class="icon icon-globe mr-5" />
{{ t('harvester.vpc.viewTopology') }}
</button>
<button
type="button"
class="btn btn-sm role-multi-action actions mr-10"
@click="showVpcAction($event, group)"
>
<i class="icon icon-actions" />
</button>
</div>
</div>
</template>
<template
v-for="(vpc) in vpcWithoutSubnets"
:key="vpc.id"
v-slot:[slotName(vpc.id)]
>
<tr
v-show="!vpc.hasChild"
class="main-row"
>
<td
class="empty text-center"
colspan="12"
>
{{ t('harvester.vpc.noChild') }}
</td>
</tr>
</template>
</ResourceTable>
</div>
</template>
<style lang="scss" scoped>
.state {
display: flex;
justify-content: space-between;
.icon-warning {
margin-top: 2px;
}
}
.group-bar {
display: flex;
flex-direction: row;
justify-content: space-between;
.right {
margin-top: 5px;
margin-bottom: 3px;
}
.group-tab {
&, &::after {
height: 50px;
}
&::after {
right: -20px;
}
SPAN {
color: var(--body-text) !important;
}
}
}
</style>