feat: add overlay networks and underlay networks resources page (#938)

* feat: allow user to select kube-system ns

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: add NICs tab when creating overlay network

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: only show NIC tab if ns is kube-system

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: add NAT & Internet tabs

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: add provider networks / vlan edit pages

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: add edit pages

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: review comments

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: copilot review

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
Andy Lee
2026-07-01 16:33:20 +08:00
committed by GitHub
parent 9a675da756
commit b9334bafb7
13 changed files with 1627 additions and 13 deletions
@@ -0,0 +1,190 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../types';
export default {
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
ResourceTabs,
Tab,
LabeledInput,
LabeledSelect,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
return {
eip: this.value?.spec?.eip || '',
externalPort: this.value?.spec?.externalPort || '',
internalIp: this.value?.spec?.internalIp || '',
internalPort: this.value?.spec?.internalPort || '',
protocol: this.value?.spec?.protocol || '',
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({ eips: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.IPTABLES_EIP }) });
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
eipOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const eips = this.$store.getters[`${ inStore }/all`](HCI.IPTABLES_EIP) || [];
return eips.map((eip) => ({
label: eip.id,
value: eip.id,
}));
},
protocolOptions() {
return [
{ label: 'TCP', value: 'tcp' },
{ label: 'UDP', value: 'udp' },
];
},
},
methods: {
updateBeforeSave() {
if (!this.value.spec) {
this.value.spec = {};
}
this.value.spec.eip = this.eip;
this.value.spec.externalPort = this.externalPort;
this.value.spec.internalIp = this.internalIp;
this.value.spec.internalPort = this.internalPort;
this.value.spec.protocol = this.protocol;
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="basic"
:label="t('generic.basic')"
:weight="99"
>
<div class="mt-20">
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="eip"
class="mb-20"
:options="eipOptions"
:mode="mode"
:label="t('harvester.dnat.eip.label')"
:placeholder="t('harvester.dnat.eip.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="externalPort"
class="mb-20"
:mode="mode"
:label="t('harvester.dnat.externalPort.label')"
:placeholder="t('harvester.dnat.externalPort.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="internalIp"
class="mb-20"
:mode="mode"
:label="t('harvester.dnat.internalIp.label')"
:placeholder="t('harvester.dnat.internalIp.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="internalPort"
class="mb-20"
:mode="mode"
:label="t('harvester.dnat.internalPort.label')"
:placeholder="t('harvester.dnat.internalPort.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="protocol"
class="mb-20"
:options="protocolOptions"
:mode="mode"
:label="t('harvester.dnat.protocol.label')"
:placeholder="t('harvester.dnat.protocol.placeholder')"
required
/>
</div>
</div>
</div>
</Tab>
</ResourceTabs>
</CruResource>
</template>
@@ -0,0 +1,168 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../types';
export default {
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
ResourceTabs,
Tab,
LabeledInput,
LabeledSelect,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
return {
natGwDp: this.value?.spec?.natGwDp || '',
externalSubnet: this.value?.spec?.externalSubnet || '',
v4ip: this.value?.spec?.v4ip || '',
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
natGateways: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC_NAT_GATEWAY }),
subnets: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SUBNET }),
});
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
natGatewayOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const natGateways = this.$store.getters[`${ inStore }/all`](HCI.VPC_NAT_GATEWAY) || [];
return natGateways.map((gw) => ({
label: gw.id,
value: gw.id,
}));
},
subnetOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const subnets = this.$store.getters[`${ inStore }/all`](HCI.SUBNET) || [];
return subnets.map((subnet) => ({
label: subnet.id,
value: subnet.id,
}));
},
},
methods: {
updateBeforeSave() {
if (!this.value.spec) {
this.value.spec = {};
}
this.value.spec.natGwDp = this.natGwDp;
this.value.spec.externalSubnet = this.externalSubnet;
this.value.spec.v4ip = this.v4ip;
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="basic"
:label="t('generic.basic')"
:weight="99"
>
<div class="mt-20">
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="natGwDp"
class="mb-20"
:options="natGatewayOptions"
:mode="mode"
:label="t('harvester.externalIP.natGateway.label')"
:placeholder="t('harvester.externalIP.natGateway.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="externalSubnet"
class="mb-20"
:options="subnetOptions"
:label="t('harvester.externalIP.externalSubnet.label')"
:placeholder="t('harvester.externalIP.externalSubnet.placeholder')"
:mode="mode"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="v4ip"
class="mb-20"
:label="t('harvester.externalIP.v4ip.label')"
:placeholder="t('harvester.externalIP.v4ip.placeholder')"
:mode="mode"
required
/>
</div>
</div>
</div>
</Tab>
</ResourceTabs>
</CruResource>
</template>
@@ -0,0 +1,140 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../types';
export default {
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
ResourceTabs,
Tab,
LabeledInput,
LabeledSelect,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
return {
eip: this.value?.spec?.eip || '',
internalCIDR: this.value?.spec?.internalCIDR || '',
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({ eips: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.IPTABLES_EIP }) });
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
eipOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const eips = this.$store.getters[`${ inStore }/all`](HCI.IPTABLES_EIP) || [];
return eips.map((eip) => ({
label: eip.id,
value: eip.id,
}));
},
},
methods: {
updateBeforeSave() {
if (!this.value.spec) {
this.value.spec = {};
}
this.value.spec.eip = this.eip;
this.value.spec.internalCIDR = this.internalCIDR;
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="basic"
:label="t('generic.basic')"
:weight="99"
>
<div class="mt-20">
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="eip"
class="mb-20"
:options="eipOptions"
:mode="mode"
:label="t('harvester.snat.eip.label')"
:placeholder="t('harvester.snat.eip.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="internalCIDR"
class="mb-20"
:mode="mode"
:label="t('harvester.snat.internalCIDR.label')"
:placeholder="t('harvester.snat.internalCIDR.placeholder')"
required
/>
</div>
</div>
</div>
</Tab>
</ResourceTabs>
</CruResource>
</template>
@@ -0,0 +1,384 @@
<script>
import CruResource from '@shell/components/CruResource';
import ArrayList from '@shell/components/form/ArrayList';
import InfoBox from '@shell/components/InfoBox';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import ArrayListSelect from '@shell/components/form/ArrayListSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { NODE } from '@shell/config/types';
import { HCI } from '../types';
export default {
emits: ['update:value'],
components: {
CruResource,
ArrayList,
InfoBox,
NameNsDescription,
ResourceTabs,
Tab,
LabeledSelect,
ArrayListSelect,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
return {
defaultInterface: this.value?.spec?.defaultInterface || '',
excludedNodes: this.value?.spec?.excludeNodes || [],
customInterfaces: this.value?.spec?.customInterfaces || [],
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
linkMonitors: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.LINK_MONITOR }),
});
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
nodes() {
const inStore = this.$store.getters['currentProduct'].inStore;
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
return nodes.filter((n) => n.isEtcd !== 'true');
},
nics() {
const inStore = this.$store.getters['currentProduct'].inStore;
const linkMonitor = this.$store.getters[`${ inStore }/byId`](HCI.LINK_MONITOR, 'nic') || {};
const linkStatus = linkMonitor?.status?.linkStatus || {};
const nodes = this.nodes.map((n) => n.id);
const out = [];
// Collect all nics from all nodes
Object.keys(linkStatus).map((nodeName) => {
if (nodes.includes(nodeName)) {
const nics = linkStatus[nodeName] || [];
nics.map((nic) => {
out.push({
...nic,
nodeName,
});
});
}
});
return out;
},
nicOptions() {
const out = [];
const seen = new Set();
(this.nics || []).forEach((nic) => {
if (!seen.has(nic.name)) {
seen.add(nic.name);
out.push({
label: nic.name,
value: nic.name,
});
}
});
return out.sort((a, b) => a.label.localeCompare(b.label));
},
nodeOptions() {
return this.nodes.map((node) => ({
label: node.id,
value: node.id,
}));
},
},
methods: {
removeCustomInterface(index) {
this.customInterfaces.splice(index, 1);
},
updateBeforeSave() {
if (!this.value.spec) {
this.value.spec = {};
}
this.value.spec.defaultInterface = this.defaultInterface;
this.value.spec.excludeNodes = this.excludedNodes;
this.value.spec.customInterfaces = (this.customInterfaces || [])
.filter((item) => item?.interface || (item?.nodes || []).length)
.map((item) => ({
interface: item.interface || '',
nodes: (item.nodes || []).filter((node) => !!node),
}))
.filter((item) => item.interface && item.nodes.length > 0);
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="Interfaces"
label="Interfaces"
:weight="99"
>
<LabeledSelect
v-model:value="defaultInterface"
class="mb-20"
required
:options="nicOptions"
:mode="mode"
:label="t('harvester.providerNetwork.defaultInterface.label')"
:placeholder="t('harvester.providerNetwork.defaultInterface.placeholder')"
/>
<hr class="section-divider" />
<ArrayList
v-model:value="customInterfaces"
class="mb-20 custom-interface-list"
:mode="mode"
:title="t('harvester.providerNetwork.customInterfaces.label')"
:protip="false"
:remove-allowed="false"
:initial-empty-row="true"
:default-add-value="{ interface: '', nodes: [] }"
>
<template #add="{ add }">
<div class="custom-interface-primary-add">
<button
type="button"
class="btn role-primary"
:disabled="mode === 'view'"
@click="add"
>
{{ t('harvester.providerNetwork.customInterfaces.addLabel') }}
</button>
</div>
</template>
<template #column-headers>
<div class="row custom-interface-header">
<div class="col span-6">
{{ t('harvester.providerNetwork.customInterfaces.interface.label') }}
</div>
<div class="col span-6">
{{ t('harvester.providerNetwork.customInterfaces.nodes.label') }}
</div>
</div>
</template>
<template #columns="scope">
<InfoBox class="custom-interface-box">
<button
v-if="mode !== 'view'"
type="button"
class="role-link btn btn-sm remove"
@click="removeCustomInterface(scope.i)"
>
<i class="icon icon-x" />
</button>
<div class="custom-interface-content">
<div class="row custom-interface-row interface-row">
<div class="col span-12 interface-col">
<h3 class="mb-10">
{{ t('harvester.providerNetwork.customInterfaces.interface.label') }}
</h3>
<LabeledSelect
v-model:value="scope.row.value.interface"
class="mb-20"
:label="''"
:options="nicOptions"
:mode="mode"
:placeholder="t('harvester.providerNetwork.customInterfaces.interface.placeholder')"
/>
</div>
</div>
<div class="row custom-interface-row nodes-row">
<div class="col span-12">
<ArrayListSelect
v-model:value="scope.row.value.nodes"
:options="nodeOptions"
:mode="mode"
:disabled="mode === 'view'"
:enable-default-add-value="false"
:array-list-props="{
addLabel: t('harvester.providerNetwork.customInterfaces.nodes.addLabel'),
initialEmptyRow: true,
title: t('harvester.providerNetwork.customInterfaces.nodes.label'),
required: false,
protip: false,
}"
:select-props="{
placeholder: t('harvester.providerNetwork.customInterfaces.nodes.placeholder'),
disabled: mode === 'view',
}"
>
<template #add="{ add }">
<div class="custom-interface-add">
<button
type="button"
class="btn role-tertiary add"
:disabled="mode === 'view'"
@click="add"
>
{{ t('harvester.providerNetwork.customInterfaces.nodes.addLabel') }}
</button>
</div>
</template>
</ArrayListSelect>
</div>
</div>
</div>
</InfoBox>
</template>
</ArrayList>
</Tab>
<Tab
name="excludedNodes"
:label="t('harvester.providerNetwork.excludedNodes.label')"
:weight="98"
>
<div class="row">
<div class="col span-12">
<ArrayListSelect
v-model:value="excludedNodes"
:options="nodeOptions"
:disabled="mode === 'view'"
:mode="mode"
:enable-default-add-value="false"
:array-list-props="{
addLabel: t('harvester.providerNetwork.excludedNodes.addLabel'),
initialEmptyRow: true,
required: false,
protip: false,
}"
:select-props="{
placeholder: t('harvester.providerNetwork.excludedNodes.placeholder'),
disabled: mode === 'view',
}"
/>
</div>
</div>
</Tab>
</ResourceTabs>
</CruResource>
</template>
<style lang="scss" scoped>
.section-divider {
border: none;
border-top: 1px solid var(--border);
margin: 10px 0 20px;
}
.custom-interface-header {
margin-bottom: 10px;
font-weight: 600;
}
.custom-interface-row {
align-items: flex-start;
}
.interface-row {
width: calc(100% - 90px);
margin-bottom: 10px;
}
.nodes-row {
align-items: flex-start;
}
.custom-interface-add {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
:deep(.nodes-row .array-list-select .box) {
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
}
:deep(.nodes-row .array-list-select .box .remove) {
align-self: center;
}
.custom-interface-primary-add {
max-width: 100%;
}
.custom-interface-box {
position: relative;
width: 100%;
padding: 20px;
margin-bottom: 5px;
}
:deep(.custom-interface-list .box) {
grid-template-columns: 1fr;
}
.remove {
position: absolute;
top: 10px;
right: 10px;
z-index: 1;
padding: 0;
}
</style>
+23 -2
View File
@@ -64,8 +64,9 @@ export default {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = {
vpc: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
nad: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }),
vpc: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
nad: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }),
vlans: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN }),
};
await allHash(hash);
@@ -135,6 +136,16 @@ export default {
natOutgoingDisabled() {
// Disable the NAT Outgoing option when the subnet belongs to the ovn-cluster VPC and its name is join or ovn-default.
return this.value?.spec?.vpc === 'ovn-cluster' && ['join', 'ovn-default'].includes(this.value?.metadata?.name);
},
vlanOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vlans = this.$store.getters[`${ inStore }/all`](HCI.VLAN) || [];
return vlans.map((vlan) => ({
label: vlan.id,
value: vlan.id,
}));
}
},
@@ -270,6 +281,16 @@ export default {
:mode="mode"
/>
</div>
<div class="col span-6">
<LabeledSelect
v-model:value="value.spec.vlan"
class="mb-20"
:options="vlanOptions"
:placeholder="t('harvester.subnet.vlan.placeholder')"
:label="t('harvester.subnet.vlan.label')"
:mode="mode"
/>
</div>
</div>
<div class="row mt-20">
<div class="col span-6">
+146
View File
@@ -0,0 +1,146 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../types';
export default {
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
ResourceTabs,
Tab,
LabeledInput,
LabeledSelect,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
return {
vlanId: this.value?.spec?.id || '',
provider: this.value?.spec?.provider || '',
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({ providerNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.PROVIDER_NETWORK }) });
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
providerNetworks() {
const inStore = this.$store.getters['currentProduct'].inStore;
const providerNetworks = this.$store.getters[`${ inStore }/all`](HCI.PROVIDER_NETWORK) || [];
return providerNetworks.map((pn) => ({
label: pn.id,
value: pn.id,
}));
},
},
methods: {
updateBeforeSave() {
if (!this.value.spec) {
this.value.spec = {};
}
if (this.vlanId !== '') {
this.value.spec.id = Number(this.vlanId);
}
this.value.spec.provider = this.provider;
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="basic"
:label="t('generic.basic')"
:weight="99"
>
<div class="mt-20">
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value.number="vlanId"
class="mb-20"
type="number"
:min="1"
:max="4094"
:label="t('harvester.vlan.id.label')"
:placeholder="t('harvester.vlan.id.placeholder')"
:mode="mode"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="provider"
class="mb-20"
:options="providerNetworks"
:mode="mode"
:label="t('harvester.vlan.provider.label')"
:placeholder="t('harvester.vlan.provider.placeholder')"
required
/>
</div>
</div>
</div>
</Tab>
</ResourceTabs>
</CruResource>
</template>
@@ -0,0 +1,246 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import ArrayListSelect from '@shell/components/form/ArrayListSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI as HCI_ANNOTATIONS } from '@pkg/config/labels-annotations';
import { HCI } from '../types';
export default {
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
ResourceTabs,
Tab,
LabeledInput,
LabeledSelect,
ArrayListSelect,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
const internalTenantNetwork = this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.CNI_NETWORKS] || '';
return {
internalTenantNetwork,
vpc: this.value?.spec?.vpc || '',
subnet: this.value?.spec?.subnet || '',
lanIp: this.value?.spec?.lanIp || '',
externalSubnets: this.value?.spec?.externalSubnets || [],
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
vpcs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
subnets: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SUBNET }),
vmNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.NETWORK_ATTACHMENT }),
});
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
vpcOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vpcs = this.$store.getters[`${ inStore }/all`](HCI.VPC) || [];
return vpcs.map((vpc) => ({
label: vpc.id,
value: vpc.id,
}));
},
subnetOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const subnets = this.$store.getters[`${ inStore }/all`](HCI.SUBNET) || [];
return subnets.map((subnet) => ({
label: subnet.id,
value: subnet.id,
}));
},
vmNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vmNetworks = this.$store.getters[`${ inStore }/all`](HCI.NETWORK_ATTACHMENT) || [];
return vmNetworks.map((network) => ({
label: network.id,
value: network.id,
}));
},
},
methods: {
updateBeforeSave() {
if (!this.value.spec) {
this.value.spec = {};
}
if (!this.value.metadata) {
this.value.metadata = {};
}
if (!this.value.metadata.annotations) {
this.value.metadata.annotations = {};
}
this.value.spec.vpc = this.vpc;
this.value.spec.subnet = this.subnet;
this.value.spec.lanIp = this.lanIp;
this.value.spec.externalSubnets = (this.externalSubnets || []).filter((subnet) => !!subnet);
if (this.internalTenantNetwork) {
this.value.metadata.annotations[HCI_ANNOTATIONS.CNI_NETWORKS] = this.internalTenantNetwork;
} else {
delete this.value.metadata.annotations[HCI_ANNOTATIONS.CNI_NETWORKS];
}
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="basic"
:label="t('generic.basic')"
:weight="99"
>
<div class="mt-20">
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="internalTenantNetwork"
class="mb-20"
required
:options="vmNetworkOptions"
:mode="mode"
:label="t('harvester.natGateway.internalTenantNetwork.label')"
:placeholder="t('harvester.natGateway.internalTenantNetwork.placeholder')"
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="vpc"
class="mb-20"
:options="vpcOptions"
:mode="mode"
:label="t('harvester.natGateway.vpc.label')"
:placeholder="t('harvester.natGateway.vpc.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="subnet"
class="mb-20"
:options="subnetOptions"
:mode="mode"
:label="t('harvester.natGateway.subnet.label')"
:placeholder="t('harvester.natGateway.subnet.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="lanIp"
class="mb-20"
:mode="mode"
:label="t('harvester.natGateway.lanIp.label')"
:placeholder="t('harvester.natGateway.lanIp.placeholder')"
required
/>
</div>
</div>
</div>
</Tab>
<Tab
name="externalSubnets"
:label="t('harvester.natGateway.externalSubnets.label')"
:weight="98"
>
<div class="mt-20">
<div class="row">
<div class="col span-12">
<ArrayListSelect
v-model:value="externalSubnets"
:mode="mode"
:disabled="mode === 'view'"
required
:options="subnetOptions"
:enable-default-add-value="false"
:array-list-props="{
addLabel: t('harvester.natGateway.externalSubnets.addLabel'),
title: t('harvester.natGateway.subnet.label'),
initialEmptyRow: true,
required: true,
protip: false,
}"
:select-props="{
placeholder: t('harvester.natGateway.externalSubnets.placeholder'),
disabled: mode === 'view',
}"
/>
</div>
</div>
</div>
</Tab>
</ResourceTabs>
</CruResource>
</template>