mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-01 03:36:04 +08:00
feat: support for subnets and VPCs from UI (#374)
Signed-off-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
@@ -6,14 +6,13 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { HCI } from '../types';
|
||||
import { NETWORK_TYPE } from '../config/types';
|
||||
|
||||
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
|
||||
const { L2VLAN, UNTAGGED, OVERLAY } = NETWORK_TYPE;
|
||||
|
||||
const AUTO = 'auto';
|
||||
const MANUAL = 'manual';
|
||||
@@ -44,13 +43,10 @@ export default {
|
||||
|
||||
data() {
|
||||
const config = JSON.parse(this.value.spec.config);
|
||||
|
||||
const annotations = this.value?.metadata?.annotations || {};
|
||||
const layer3Network = JSON.parse(annotations[HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE] || '{}');
|
||||
|
||||
if ((config.bridge || '').endsWith('-br')) {
|
||||
config.bridge = config.bridge.slice(0, -3);
|
||||
}
|
||||
|
||||
const type = this.value.vlanType || L2VLAN ;
|
||||
|
||||
return {
|
||||
@@ -78,6 +74,30 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
clusterBridge: {
|
||||
get() {
|
||||
if (!this.config.bridge) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// remove -br suffix if exists
|
||||
return this.config?.bridge?.endsWith('-br') ? this.config.bridge.slice(0, -3) : '';
|
||||
},
|
||||
|
||||
set(neu) {
|
||||
if (neu === '') {
|
||||
this.config.bridge = '';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!neu.endsWith('-br')) {
|
||||
this.config.bridge = `${ neu }-br`;
|
||||
} else {
|
||||
this.config.bridge = neu;
|
||||
}
|
||||
}
|
||||
},
|
||||
modeOptions() {
|
||||
return [{
|
||||
label: this.t('harvester.network.layer3Network.mode.auto'),
|
||||
@@ -88,6 +108,14 @@ export default {
|
||||
}];
|
||||
},
|
||||
|
||||
kubeovnVpcSubnetSupport() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet');
|
||||
},
|
||||
|
||||
longhornV2LVMSupport() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('longhornV2LVMSupport');
|
||||
},
|
||||
|
||||
clusterNetworkOptions() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||
@@ -103,8 +131,30 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
networkType() {
|
||||
return [L2VLAN, UNTAGGED];
|
||||
networkTypes() {
|
||||
const types = [L2VLAN, UNTAGGED];
|
||||
|
||||
if (this.kubeovnVpcSubnetSupport) {
|
||||
types.push(OVERLAY);
|
||||
}
|
||||
|
||||
return types;
|
||||
},
|
||||
|
||||
isL2VlanNetwork() {
|
||||
if (this.isView) {
|
||||
return this.value.vlanType === L2VLAN;
|
||||
}
|
||||
|
||||
return this.type === L2VLAN;
|
||||
},
|
||||
|
||||
isOverlayNetwork() {
|
||||
if (this.isView) {
|
||||
return this.value.vlanType === OVERLAY;
|
||||
}
|
||||
|
||||
return this.type === OVERLAY;
|
||||
},
|
||||
|
||||
isUntaggedNetwork() {
|
||||
@@ -116,36 +166,54 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
type(newType) {
|
||||
if (newType === OVERLAY) {
|
||||
this.config.type = 'kube-ovn';
|
||||
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
|
||||
this.config.server_socket = '/run/openvswitch/kube-ovn-daemon.sock';
|
||||
} else {
|
||||
this.config.type = 'bridge';
|
||||
this.config.promiscMode = true;
|
||||
this.config.ipam = {};
|
||||
this.config.bridge = '';
|
||||
delete this.config.provider;
|
||||
delete this.config.server_socket;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async saveNetwork(buttonCb) {
|
||||
const errors = [];
|
||||
|
||||
if (!this.config.vlan && !this.isUntaggedNetwork) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
|
||||
}
|
||||
|
||||
if (!this.config.bridge) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
|
||||
}
|
||||
|
||||
if (this.layer3Network.mode === MANUAL) {
|
||||
if (!this.layer3Network.gateway) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
|
||||
if (this.isL2VlanNetwork || this.isUntaggedNetwork) {
|
||||
if (!this.config.vlan && !this.isUntaggedNetwork) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
|
||||
}
|
||||
if (!this.layer3Network.cidr) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
|
||||
|
||||
if (!this.config.bridge) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
|
||||
}
|
||||
|
||||
if (this.layer3Network.mode === MANUAL) {
|
||||
if (!this.layer3Network.gateway) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
|
||||
}
|
||||
if (!this.layer3Network.cidr) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
buttonCb(false);
|
||||
this.errors = errors;
|
||||
|
||||
return false;
|
||||
}
|
||||
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
buttonCb(false);
|
||||
this.errors = errors;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
|
||||
|
||||
await this.save(buttonCb);
|
||||
},
|
||||
|
||||
@@ -169,14 +237,19 @@ export default {
|
||||
updateBeforeSave() {
|
||||
this.config.name = this.value.metadata.name;
|
||||
|
||||
if (this.isOverlayNetwork) {
|
||||
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
|
||||
delete this.config.bridge;
|
||||
delete this.config.promiscMode;
|
||||
delete this.config.vlan;
|
||||
delete this.config.ipam;
|
||||
}
|
||||
|
||||
if (this.isUntaggedNetwork) {
|
||||
delete this.config.vlan;
|
||||
}
|
||||
|
||||
this.value.spec.config = JSON.stringify({
|
||||
...this.config,
|
||||
bridge: `${ this.config.bridge }-br`,
|
||||
});
|
||||
this.value.spec.config = JSON.stringify({ ...this.config });
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -212,14 +285,15 @@ export default {
|
||||
<LabeledSelect
|
||||
v-model:value="type"
|
||||
class="mb-20"
|
||||
:options="networkType"
|
||||
:options="networkTypes"
|
||||
:mode="mode"
|
||||
:disabled="isEdit"
|
||||
:label="t('harvester.fields.type')"
|
||||
required
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-if="!isUntaggedNetwork"
|
||||
v-if="isL2VlanNetwork"
|
||||
v-model:value.number="config.vlan"
|
||||
class="mb-20"
|
||||
required
|
||||
@@ -229,25 +303,20 @@ export default {
|
||||
:mode="mode"
|
||||
@update:value="input"
|
||||
/>
|
||||
|
||||
<div class="row">
|
||||
<div
|
||||
class="col span-12"
|
||||
>
|
||||
<LabeledSelect
|
||||
v-model:value="config.bridge"
|
||||
class="mb-20"
|
||||
:label="t('harvester.network.clusterNetwork.label')"
|
||||
required
|
||||
:options="clusterNetworkOptions"
|
||||
:mode="mode"
|
||||
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<LabeledSelect
|
||||
v-if="!isOverlayNetwork"
|
||||
v-model:value="clusterBridge"
|
||||
class="mb-20"
|
||||
:label="t('harvester.network.clusterNetwork.label')"
|
||||
required
|
||||
:disabled="isEdit"
|
||||
:options="clusterNetworkOptions"
|
||||
:mode="mode"
|
||||
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="!isUntaggedNetwork"
|
||||
v-if="isL2VlanNetwork"
|
||||
name="layer3Network"
|
||||
:label="t('harvester.network.tabs.layer3Network')"
|
||||
:weight="98"
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
<script>
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import { NETWORK_PROTOCOL, NETWORK_TYPE } from '@pkg/harvester/config/types';
|
||||
import { set } from '@shell/utils/object';
|
||||
import ArrayList from '@shell/components/form/ArrayList';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { HCI } from '../../types';
|
||||
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
|
||||
|
||||
export default {
|
||||
name: 'EditSubnet',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
components: {
|
||||
CruResource,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
NameNsDescription,
|
||||
Tab,
|
||||
RadioGroup,
|
||||
ArrayList,
|
||||
ResourceTabs,
|
||||
Loading,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
created() {
|
||||
const vpc = this.$route.query.vpc || '';
|
||||
|
||||
set(this.value, 'spec', this.value.spec || {
|
||||
cidrBlock: '',
|
||||
protocol: NETWORK_PROTOCOL.IPv4,
|
||||
provider: '',
|
||||
vpc,
|
||||
gatewayIP: '',
|
||||
excludeIps: [],
|
||||
private: false
|
||||
});
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
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 }),
|
||||
};
|
||||
|
||||
await allHash(hash);
|
||||
},
|
||||
|
||||
computed: {
|
||||
showAllowSubnets() {
|
||||
return this.value?.spec?.private === true;
|
||||
},
|
||||
|
||||
doneLocationOverride() {
|
||||
return this.value.doneOverride;
|
||||
},
|
||||
|
||||
allowSubnetTooltip() {
|
||||
return this.t('harvester.subnet.allowSubnet.tooltip', null, true);
|
||||
},
|
||||
|
||||
excludeIPsTooltip() {
|
||||
return this.t('harvester.subnet.excludeIPs.tooltip', null, true);
|
||||
},
|
||||
|
||||
protocolOptions() {
|
||||
return Object.values(NETWORK_PROTOCOL);
|
||||
},
|
||||
|
||||
provider: {
|
||||
get() {
|
||||
const raw = this.value.spec.provider;
|
||||
|
||||
if (!raw) {
|
||||
return '';
|
||||
}
|
||||
const vmNet = raw.split('.')[0] || '';
|
||||
const ns = raw.split('.')[1] || '';
|
||||
|
||||
return `${ ns }/${ vmNet }`;
|
||||
},
|
||||
|
||||
set(value) {
|
||||
const ns = value.split('/')[0] || '';
|
||||
const vmNet = value.split('/')[1] || '';
|
||||
const provider = `${ vmNet }.${ ns }.ovn`;
|
||||
|
||||
set(this.value, 'spec.provider', provider);
|
||||
}
|
||||
},
|
||||
|
||||
providerOptions() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const vmNets = this.$store.getters[`${ inStore }/all`](NETWORK_ATTACHMENT) || [];
|
||||
|
||||
return vmNets.filter((net) => net.vlanType === NETWORK_TYPE.OVERLAY).map((n) => ({
|
||||
label: n.id,
|
||||
value: n.id,
|
||||
}));
|
||||
},
|
||||
|
||||
vpcOptions() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const vpcs = this.$store.getters[`${ inStore }/all`](HCI.VPC) || [];
|
||||
|
||||
return vpcs.map((n) => ({
|
||||
label: n.id,
|
||||
value: n.id,
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async saveSubnet(buttonCb) {
|
||||
const errors = [];
|
||||
const name = this.value?.metadata?.name;
|
||||
|
||||
try {
|
||||
if (!name) {
|
||||
errors.push(this.t('validation.required', { key: this.t('generic.name') }, true));
|
||||
} else if (!this.value?.spec?.cidrBlock) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.cidrBlock.label') }, true));
|
||||
} else if (!this.value?.spec?.provider) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.provider.label') }, true));
|
||||
} else if (this.value.spec.excludeIps.includes('')) {
|
||||
errors.push(this.t('harvester.validation.subnet.excludeIps'));
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
buttonCb(false);
|
||||
this.errors = errors;
|
||||
|
||||
return false;
|
||||
}
|
||||
await this.value.save();
|
||||
buttonCb(true);
|
||||
this.done();
|
||||
} catch (e) {
|
||||
this.errors = [e];
|
||||
buttonCb(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" />
|
||||
<CruResource
|
||||
v-else
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:apply-hooks="applyHooks"
|
||||
:errors="errors"
|
||||
@finish="saveSubnet"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
:mode="mode"
|
||||
:namespaced="false"
|
||||
@update:value="$emit('update:value', $event)"
|
||||
/>
|
||||
<ResourceTabs
|
||||
class="mt-15"
|
||||
:need-events="false"
|
||||
:need-related="false"
|
||||
:mode="mode"
|
||||
:side-tabs="true"
|
||||
>
|
||||
<Tab
|
||||
name="Basic"
|
||||
:label="t('generic.basic')"
|
||||
:weight="-1"
|
||||
class="bordered-table"
|
||||
>
|
||||
<div class="row mt-10">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="value.spec.cidrBlock"
|
||||
class="mb-20"
|
||||
required
|
||||
:placeholder="t('harvester.subnet.cidrBlock.placeholder')"
|
||||
:label="t('harvester.subnet.cidrBlock.label')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model:value="value.spec.protocol"
|
||||
:label="t('harvester.subnet.protocol.label')"
|
||||
:options="protocolOptions"
|
||||
required
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-10">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model:value="provider"
|
||||
:label="t('harvester.subnet.provider.label')"
|
||||
:options="providerOptions"
|
||||
:tooltip="t('harvester.subnet.provider.tooltip')"
|
||||
required
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model:value="value.spec.vpc"
|
||||
:label="t('harvester.subnet.vpc.label')"
|
||||
:options="vpcOptions"
|
||||
required
|
||||
:disabled="true"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="value.spec.gateway"
|
||||
class="mb-20"
|
||||
:placeholder="t('harvester.subnet.gateway.placeholder')"
|
||||
:label="t('harvester.subnet.gateway.label')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<RadioGroup
|
||||
v-model:value="value.spec.private"
|
||||
name="enabled"
|
||||
:options="[true, false]"
|
||||
:label="t('harvester.subnet.private.label')"
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
:mode="mode"
|
||||
tooltip-key="harvester.subnet.private.tooltip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ArrayList
|
||||
v-if="showAllowSubnets"
|
||||
v-model:value="value.spec.allowSubnets"
|
||||
:show-header="true"
|
||||
class="mt-20"
|
||||
:mode="mode"
|
||||
:add-label="t('harvester.subnet.allowSubnet.addSubnet')"
|
||||
>
|
||||
<template #column-headers>
|
||||
<div class="box">
|
||||
<h3 class="key">
|
||||
{{ t('harvester.subnet.allowSubnet.label') }}
|
||||
<i
|
||||
v-clean-tooltip="{content: allowSubnetTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||
v-stripped-aria-label="allowSubnetTooltip"
|
||||
class="icon icon-info"
|
||||
tabindex="0"
|
||||
/>
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
<template #columns="scope">
|
||||
<div class="key">
|
||||
<input
|
||||
v-model="scope.row.value"
|
||||
:placeholder="t('harvester.subnet.allowSubnet.placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArrayList>
|
||||
<ArrayList
|
||||
v-model:value="value.spec.excludeIps"
|
||||
:show-header="true"
|
||||
class="mt-20"
|
||||
:mode="mode"
|
||||
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
|
||||
>
|
||||
<template #column-headers>
|
||||
<div class="box">
|
||||
<h3 class="key">
|
||||
{{ t('harvester.setting.storageNetwork.exclude.label') }}
|
||||
<i
|
||||
v-clean-tooltip="{content: excludeIPsTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||
v-stripped-aria-label="excludeIPsTooltip"
|
||||
class="icon icon-info"
|
||||
tabindex="0"
|
||||
/>
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
<template #columns="scope">
|
||||
<div class="key">
|
||||
<input
|
||||
v-model="scope.row.value"
|
||||
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArrayList>
|
||||
</Tab>
|
||||
</ResourceTabs>
|
||||
</CruResource>
|
||||
</template>
|
||||
@@ -0,0 +1,162 @@
|
||||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
||||
import { removeAt } from '@shell/utils/array';
|
||||
|
||||
export default {
|
||||
name: 'StaticRoutes',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: _EDIT,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
const rows = (this.value || []).map((row) => {
|
||||
return {
|
||||
cidr: row.cidr || '',
|
||||
nextHopIP: row.nextHopIP || '',
|
||||
};
|
||||
});
|
||||
|
||||
return { rows };
|
||||
},
|
||||
|
||||
computed: {
|
||||
isView() {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
|
||||
showAdd() {
|
||||
return !this.isView;
|
||||
},
|
||||
|
||||
showRemove() {
|
||||
return !this.isView;
|
||||
},
|
||||
nextHopIPTooltip() {
|
||||
return this.t('harvester.vpc.staticRoutes.nextHopIP.tooltip');
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.queueUpdate = debounce(this.update, 100);
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
this.rows.push({
|
||||
cidr: '',
|
||||
nextHopIP: '',
|
||||
});
|
||||
this.queueUpdate();
|
||||
},
|
||||
|
||||
remove(idx) {
|
||||
removeAt(this.rows, idx);
|
||||
this.queueUpdate();
|
||||
},
|
||||
|
||||
update() {
|
||||
if (this.isView) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('update:value', this.rows);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="rows.length"
|
||||
class="static-route-row"
|
||||
>
|
||||
<div
|
||||
v-for="(row, idx) in rows"
|
||||
:key="idx"
|
||||
>
|
||||
<div class="pool-headers cidr">
|
||||
<span class="pool-cidr">
|
||||
<t k="harvester.vpc.staticRoutes.cidr.label" />
|
||||
</span>
|
||||
<span class="pool-nextHopIP">
|
||||
<t k="harvester.vpc.staticRoutes.nextHopIP.label" />
|
||||
<i
|
||||
v-clean-tooltip="{content: nextHopIPTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||
v-stripped-aria-label="nextHopIPTooltip"
|
||||
class="icon icon-info"
|
||||
tabindex="0"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="pool-row cidr">
|
||||
<div class="pool-cidr">
|
||||
<span v-if="isView">
|
||||
{{ row.cidr }}
|
||||
</span>
|
||||
<input
|
||||
v-else
|
||||
v-model="row.cidr"
|
||||
type="text"
|
||||
:placeholder="t('harvester.vpc.staticRoutes.cidr.placeholder')"
|
||||
@input="queueUpdate"
|
||||
/>
|
||||
</div>
|
||||
<div class="pool-nextHopIP">
|
||||
<span v-if="isView">
|
||||
{{ row.nextHopIP }}
|
||||
</span>
|
||||
<input
|
||||
v-else
|
||||
v-model="row.nextHopIP"
|
||||
type="text"
|
||||
:placeholder="t('harvester.vpc.staticRoutes.nextHopIP.placeholder')"
|
||||
@input="queueUpdate"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-if="showRemove"
|
||||
type="button"
|
||||
class="btn role-link pl-0"
|
||||
@click="remove(idx)"
|
||||
>
|
||||
<t k="generic.remove" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="showAdd"
|
||||
type="button"
|
||||
class="btn role-tertiary add"
|
||||
@click="add()"
|
||||
>
|
||||
<t k="generic.add" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pool-headers, .pool-row {
|
||||
display: grid;
|
||||
grid-column-gap: $column-gutter;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
|
||||
&.cidr {
|
||||
grid-template-columns: 40%+$column-gutter 40%+$column-gutter 15%;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,187 @@
|
||||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
||||
import { removeAt } from '@shell/utils/array';
|
||||
import { HCI } from '../../types';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
export default {
|
||||
name: 'VpcPeerings',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
components: { LabeledSelect },
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: _EDIT,
|
||||
},
|
||||
vpc: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
await this.$store.dispatch('harvester/findAll', { type: HCI.VPC });
|
||||
},
|
||||
|
||||
data() {
|
||||
const rows = (this.value || []).map((row) => {
|
||||
return {
|
||||
localConnectIP: row.localConnectIP || '',
|
||||
remoteVpc: row.remoteVpc || '',
|
||||
};
|
||||
});
|
||||
|
||||
return { rows };
|
||||
},
|
||||
|
||||
computed: {
|
||||
isView() {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
|
||||
isEdit() {
|
||||
return this.mode === _EDIT;
|
||||
},
|
||||
|
||||
showAdd() {
|
||||
return !this.isView;
|
||||
},
|
||||
|
||||
showRemove() {
|
||||
return !this.isView;
|
||||
},
|
||||
|
||||
remoteVpcOptions() {
|
||||
const allVpcs = this.$store.getters['harvester/all'](HCI.VPC) || [];
|
||||
|
||||
// filter self vpc if editing
|
||||
const vpcs = this.isEdit ? allVpcs.filter((v) => v.id !== this.vpc.id) : allVpcs;
|
||||
|
||||
return vpcs.map((n) => ({
|
||||
label: n.id,
|
||||
value: n.id,
|
||||
}));
|
||||
},
|
||||
localConnectIPTooltip() {
|
||||
return this.t('harvester.vpc.vpcPeerings.localConnectIP.tooltip');
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.queueUpdate = debounce(this.update, 100);
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
this.rows.push({
|
||||
localConnectIP: '',
|
||||
remoteVpc: '',
|
||||
});
|
||||
this.queueUpdate();
|
||||
},
|
||||
|
||||
remove(idx) {
|
||||
removeAt(this.rows, idx);
|
||||
this.queueUpdate();
|
||||
},
|
||||
|
||||
update() {
|
||||
if (this.isView) {
|
||||
return;
|
||||
}
|
||||
this.$emit('update:value', this.rows);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="rows.length"
|
||||
class="static-route-row"
|
||||
>
|
||||
<div
|
||||
v-for="(row, idx) in rows"
|
||||
:key="idx"
|
||||
>
|
||||
<div class="pool-headers localConnectIP">
|
||||
<span class="pool-localConnectIP">
|
||||
<t k="harvester.vpc.vpcPeerings.localConnectIP.label" />
|
||||
<i
|
||||
v-clean-tooltip="{content: localConnectIPTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||
v-stripped-aria-label="localConnectPTooltip"
|
||||
class="icon icon-info"
|
||||
tabindex="0"
|
||||
/>
|
||||
</span>
|
||||
<span class="pool-remoteVpc">
|
||||
<t k="harvester.vpc.vpcPeerings.remoteVpc.label" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="pool-row localConnectIP">
|
||||
<div class="pool-localConnectIP">
|
||||
<span v-if="isView">
|
||||
{{ row.localConnectIP }}
|
||||
</span>
|
||||
<input
|
||||
v-else
|
||||
v-model="row.localConnectIP"
|
||||
type="text"
|
||||
:placeholder="t('harvester.vpc.vpcPeerings.localConnectIP.placeholder')"
|
||||
@input="queueUpdate"
|
||||
/>
|
||||
</div>
|
||||
<div class="pool-remoteVpc">
|
||||
<span v-if="isView">
|
||||
{{ row.remoteVpc }}
|
||||
</span>
|
||||
<LabeledSelect
|
||||
v-model:value="row.remoteVpc"
|
||||
:options="remoteVpcOptions"
|
||||
:mode="mode"
|
||||
@update:value="queueUpdate"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-if="showRemove"
|
||||
type="button"
|
||||
class="btn role-link pl-0"
|
||||
@click="remove(idx)"
|
||||
>
|
||||
<t k="generic.remove" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="showAdd"
|
||||
type="button"
|
||||
class="btn role-tertiary add"
|
||||
@click="add()"
|
||||
>
|
||||
<t k="generic.add" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pool-headers, .pool-row {
|
||||
display: grid;
|
||||
grid-column-gap: $column-gutter;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
|
||||
&.localConnectIP {
|
||||
grid-template-columns: 40%+$column-gutter 40%+$column-gutter 15%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,120 @@
|
||||
<script>
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
|
||||
import StaticRoutes from './StaticRoutes';
|
||||
import VpcPeerings from './VpcPeerings';
|
||||
import { set } from '@shell/utils/object';
|
||||
|
||||
export default {
|
||||
name: 'EditVPC',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
components: {
|
||||
CruResource,
|
||||
NameNsDescription,
|
||||
Tab,
|
||||
StaticRoutes,
|
||||
ResourceTabs,
|
||||
Loading,
|
||||
VpcPeerings
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
data() {
|
||||
set(this.value, 'spec', this.value.spec || {
|
||||
staticRoutes: [],
|
||||
vpcPeerings: [],
|
||||
});
|
||||
|
||||
return { staticRoutes: [] };
|
||||
},
|
||||
|
||||
methods: {
|
||||
async saveVpc(buttonCb) {
|
||||
const errors = [];
|
||||
|
||||
try {
|
||||
const name = this.value?.metadata?.name;
|
||||
|
||||
if (!name) {
|
||||
errors.push(this.t('validation.required', { key: this.t('generic.name') }, true));
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
buttonCb(false);
|
||||
this.errors = errors;
|
||||
|
||||
return false;
|
||||
}
|
||||
await this.value.save();
|
||||
buttonCb(true);
|
||||
this.done();
|
||||
} catch (e) {
|
||||
this.errors = [e];
|
||||
buttonCb(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" />
|
||||
<CruResource
|
||||
v-else
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:apply-hooks="applyHooks"
|
||||
:errors="errors"
|
||||
@finish="saveVpc"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
:mode="mode"
|
||||
:namespaced="false"
|
||||
@update:value="$emit('update:value', $event)"
|
||||
/>
|
||||
<ResourceTabs
|
||||
class="mt-15"
|
||||
:mode="mode"
|
||||
:side-tabs="true"
|
||||
>
|
||||
<Tab
|
||||
name="staticRoutes"
|
||||
:label="t('harvester.vpc.staticRoutes.label')"
|
||||
:weight="-1"
|
||||
class="bordered-table"
|
||||
>
|
||||
<StaticRoutes
|
||||
v-model:value="value.spec.staticRoutes"
|
||||
class="col span-12"
|
||||
:mode="mode"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
name="vpcPeerings"
|
||||
:label="t('harvester.vpc.vpcPeerings.label')"
|
||||
:weight="-2"
|
||||
class="bordered-table"
|
||||
>
|
||||
<VpcPeerings
|
||||
v-model:value="value.spec.vpcPeerings"
|
||||
class="col span-12"
|
||||
:mode="mode"
|
||||
:vpc="value"
|
||||
/>
|
||||
</Tab>
|
||||
</ResourceTabs>
|
||||
</CruResource>
|
||||
</template>
|
||||
Reference in New Issue
Block a user