mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-07-01 22:32:20 +00:00
feat: add Host Networks tab list and edit pages (#920)
* feat: add Host Network tab Signed-off-by: Andy Lee <andy.lee@suse.com> * feat: add Host Network edit page Signed-off-by: Andy Lee <andy.lee@suse.com> * feat: add node selector tab Signed-off-by: Andy Lee <andy.lee@suse.com> * refactor: copilot review Signed-off-by: Andy Lee <andy.lee@suse.com> * fix: lint Signed-off-by: Andy Lee <andy.lee@suse.com> * fix: copilot review Signed-off-by: Andy Lee <andy.lee@suse.com> * refactor: add warning message if addon is not enabled or already hasone Signed-off-by: Andy Lee <andy.lee@suse.com> * refactor: some wordings in en-us.yaml Signed-off-by: Andy Lee <andy.lee@suse.com> --------- Signed-off-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
parent
661ab995f6
commit
5985913f5e
186
pkg/harvester/components/HarvesterNodeSelector.vue
Normal file
186
pkg/harvester/components/HarvesterNodeSelector.vue
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
<script>
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import MatchExpressions from '@shell/components/form/MatchExpressions';
|
||||||
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import { convert, simplify, matching as selectorMatching } from '@shell/utils/selector';
|
||||||
|
import throttle from 'lodash/throttle';
|
||||||
|
import { NODE } from '@shell/config/types';
|
||||||
|
import { NAME, AGE } from '@shell/config/table-headers';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterNodeSelector',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Banner,
|
||||||
|
MatchExpressions,
|
||||||
|
ResourceTable,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
this.updateMatchingResources();
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return {
|
||||||
|
matchingResources: {
|
||||||
|
matched: 0,
|
||||||
|
matches: [],
|
||||||
|
none: true,
|
||||||
|
sample: null,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
tableHeaders: [
|
||||||
|
NAME,
|
||||||
|
{
|
||||||
|
name: 'host-ip',
|
||||||
|
labelKey: 'tableHeaders.hostIp',
|
||||||
|
search: ['internalIp'],
|
||||||
|
value: 'internalIp',
|
||||||
|
sort: ['internalIp'],
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cpuManager',
|
||||||
|
labelKey: 'harvester.tableHeaders.cpuManager',
|
||||||
|
value: 'id',
|
||||||
|
formatter: 'HarvesterCPUPinning',
|
||||||
|
width: 150,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'diskState',
|
||||||
|
labelKey: 'tableHeaders.diskState',
|
||||||
|
value: 'diskState',
|
||||||
|
formatter: 'HarvesterDiskState',
|
||||||
|
width: 130,
|
||||||
|
},
|
||||||
|
AGE,
|
||||||
|
],
|
||||||
|
inStore,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler: 'updateMatchingResources',
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
allResourcesInScope() {
|
||||||
|
this.updateMatchingResources();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
schema() {
|
||||||
|
return this.$store.getters[`${ this.inStore }/schemaFor`](NODE);
|
||||||
|
},
|
||||||
|
|
||||||
|
selectorExpressions: {
|
||||||
|
get() {
|
||||||
|
return convert(
|
||||||
|
this.value.matchLabels || {},
|
||||||
|
this.value.matchExpressions || []
|
||||||
|
);
|
||||||
|
},
|
||||||
|
set(selectorExpressions) {
|
||||||
|
const { matchLabels, matchExpressions } = simplify(selectorExpressions);
|
||||||
|
|
||||||
|
this.value['matchLabels'] = matchLabels;
|
||||||
|
this.value['matchExpressions'] = matchExpressions;
|
||||||
|
this.updateMatchingResources();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
allNodes() {
|
||||||
|
return this.$store.getters[`${ this.inStore }/all`](NODE) || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
allResourcesInScope() {
|
||||||
|
return this.allNodes.length;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateMatchingResources: throttle(function() {
|
||||||
|
const expressions = this.selectorExpressions;
|
||||||
|
const allNodes = this.allNodes;
|
||||||
|
|
||||||
|
// Empty expressions with no key = no match
|
||||||
|
const hasValidExpression = expressions.length > 0 && expressions.every((e) => !!e.key);
|
||||||
|
|
||||||
|
if (!hasValidExpression) {
|
||||||
|
this.matchingResources = {
|
||||||
|
matched: 0,
|
||||||
|
matches: [],
|
||||||
|
none: true,
|
||||||
|
sample: null,
|
||||||
|
total: allNodes.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = selectorMatching(allNodes, expressions, 'metadata.labels');
|
||||||
|
|
||||||
|
this.matchingResources = {
|
||||||
|
matched: matches.length,
|
||||||
|
matches,
|
||||||
|
none: matches.length === 0,
|
||||||
|
sample: matches[0]?.nameDisplay || null,
|
||||||
|
total: allNodes.length,
|
||||||
|
};
|
||||||
|
}, 100, { trailing: true })
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<MatchExpressions
|
||||||
|
v-model:value="selectorExpressions"
|
||||||
|
:mode="mode"
|
||||||
|
:show-remove="false"
|
||||||
|
:type="'node'"
|
||||||
|
:target-resources="allResourcesInScope"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<Banner :color="(matchingResources.none ? 'warning' : 'success')">
|
||||||
|
<span v-clean-html="t('generic.selectors.matchingResources.matchesSome', matchingResources)" />
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<ResourceTable
|
||||||
|
:rows="matchingResources.matches"
|
||||||
|
:headers="tableHeaders"
|
||||||
|
key-field="id"
|
||||||
|
:table-actions="false"
|
||||||
|
:row-actions="false"
|
||||||
|
:schema="schema"
|
||||||
|
:groupable="false"
|
||||||
|
:search="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -581,6 +581,7 @@ export function init($plugin, store) {
|
|||||||
[
|
[
|
||||||
HCI.CLUSTER_NETWORK,
|
HCI.CLUSTER_NETWORK,
|
||||||
HCI.NETWORK_ATTACHMENT,
|
HCI.NETWORK_ATTACHMENT,
|
||||||
|
HCI.HOST_NETWORK_CONFIG,
|
||||||
HCI.VPC,
|
HCI.VPC,
|
||||||
NETWORK_POLICY,
|
NETWORK_POLICY,
|
||||||
HCI.LB,
|
HCI.LB,
|
||||||
@ -1141,4 +1142,24 @@ export function init($plugin, store) {
|
|||||||
ifHaveType: HCI.IP_POOL,
|
ifHaveType: HCI.IP_POOL,
|
||||||
});
|
});
|
||||||
headers(HCI.IP_POOL, IP_POOL_HEADERS);
|
headers(HCI.IP_POOL, IP_POOL_HEADERS);
|
||||||
|
|
||||||
|
configureType(HCI.HOST_NETWORK_CONFIG, {
|
||||||
|
location: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.HOST_NETWORK_CONFIG }
|
||||||
|
},
|
||||||
|
canYaml: false,
|
||||||
|
});
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.hostNetworkConfig.label',
|
||||||
|
name: HCI.HOST_NETWORK_CONFIG,
|
||||||
|
namespaced: false,
|
||||||
|
weight: 183,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.HOST_NETWORK_CONFIG }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.HOST_NETWORK_CONFIG,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
419
pkg/harvester/edit/network.harvesterhci.io.hostnetworkconfig.vue
Normal file
419
pkg/harvester/edit/network.harvesterhci.io.hostnetworkconfig.vue
Normal file
@ -0,0 +1,419 @@
|
|||||||
|
<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 InfoBox from '@shell/components/InfoBox';
|
||||||
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import HarvesterNodeSelector from '../components/HarvesterNodeSelector';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { set } from '@shell/utils/object';
|
||||||
|
import { NODE } from '@shell/config/types';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import { ADD_ONS } from '../config/harvester-map';
|
||||||
|
|
||||||
|
const MODE_DHCP = 'dhcp';
|
||||||
|
const MODE_STATIC = 'static';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterHostNetworkConfigEditPage',
|
||||||
|
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
CruResource,
|
||||||
|
NameNsDescription,
|
||||||
|
ResourceTabs,
|
||||||
|
Tab,
|
||||||
|
InfoBox,
|
||||||
|
MessageLink,
|
||||||
|
LabeledSelect,
|
||||||
|
LabeledInput,
|
||||||
|
RadioGroup,
|
||||||
|
Checkbox,
|
||||||
|
HarvesterNodeSelector,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await allHash({
|
||||||
|
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
||||||
|
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
|
||||||
|
hostNetworkConfigs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HOST_NETWORK_CONFIG }),
|
||||||
|
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const networkMode = this.value?.spec?.mode || MODE_DHCP;
|
||||||
|
const ips = { ...(this.value?.spec?.ips || {}) };
|
||||||
|
|
||||||
|
if (!this.value.spec) {
|
||||||
|
set(this.value, 'spec', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
networkMode,
|
||||||
|
ips,
|
||||||
|
hasNodeSelector: !!this.value?.spec?.nodeSelector,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
modeOptions() {
|
||||||
|
return [
|
||||||
|
{ label: 'DHCP', value: MODE_DHCP },
|
||||||
|
{ label: 'Static', value: MODE_STATIC },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
clusterNetworkOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||||
|
|
||||||
|
return clusterNetworks.map((n) => {
|
||||||
|
const disabled = !n.isReady;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: disabled ? `${ n.id } (${ this.t('generic.notReady') })` : n.id,
|
||||||
|
value: n.id,
|
||||||
|
disabled,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
nodes() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return this.$store.getters[`${ inStore }/all`](NODE) || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
isStaticMode() {
|
||||||
|
return this.networkMode === MODE_STATIC;
|
||||||
|
},
|
||||||
|
|
||||||
|
underlay: {
|
||||||
|
get() {
|
||||||
|
return !!this.value?.spec?.underlay;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
set(this.value, 'spec.underlay', val);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
vlanID: {
|
||||||
|
get() {
|
||||||
|
return this.value?.spec?.vlanID;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
set(this.value, 'spec.vlanID', val);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
clusterNetwork: {
|
||||||
|
get() {
|
||||||
|
return this.value?.spec?.clusterNetwork;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
set(this.value, 'spec.clusterNetwork', val);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
underlayConflict() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const all = this.$store.getters[`${ inStore }/all`](HCI.HOST_NETWORK_CONFIG) || [];
|
||||||
|
const currentId = this.value?.id;
|
||||||
|
|
||||||
|
return all.find((c) => c.id !== currentId && c.spec?.underlay === true) || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
kubeovnEnabled() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS) || [];
|
||||||
|
|
||||||
|
return addons.find((a) => a.name === ADD_ONS.KUBEOVN_OPERATOR)?.spec?.enabled === true;
|
||||||
|
},
|
||||||
|
|
||||||
|
underlayDisabled() {
|
||||||
|
return !this.kubeovnEnabled || !!this.underlayConflict;
|
||||||
|
},
|
||||||
|
|
||||||
|
kubeovnAddonTo() {
|
||||||
|
return {
|
||||||
|
name: 'c-cluster-product-resource-namespace-id',
|
||||||
|
params: {
|
||||||
|
cluster: this.$route.params.cluster,
|
||||||
|
product: this.$store.getters['productId'],
|
||||||
|
resource: HCI.ADD_ONS,
|
||||||
|
namespace: 'kube-system',
|
||||||
|
id: ADD_ONS.KUBEOVN_OPERATOR,
|
||||||
|
},
|
||||||
|
query: { mode: 'edit' },
|
||||||
|
hash: '#basic',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
networkMode(neu) {
|
||||||
|
set(this.value, 'spec.mode', neu);
|
||||||
|
|
||||||
|
if (neu !== MODE_STATIC) {
|
||||||
|
if (this.value?.spec?.ips !== undefined) {
|
||||||
|
delete this.value.spec.ips;
|
||||||
|
}
|
||||||
|
this.ips = {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.updateBeforeSave);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateBeforeSave() {
|
||||||
|
set(this.value, 'spec.mode', this.networkMode);
|
||||||
|
|
||||||
|
if (this.isStaticMode) {
|
||||||
|
set(this.value, 'spec.ips', { ...this.ips });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateIp(nodeName, val) {
|
||||||
|
this.ips = { ...this.ips, [nodeName]: val };
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeSelector() {
|
||||||
|
set(this.value.spec, 'nodeSelector', {
|
||||||
|
matchExpressions: [{
|
||||||
|
key: '', operator: 'In', values: []
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
this.hasNodeSelector = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
removeNodeSelector() {
|
||||||
|
delete this.value.spec.nodeSelector;
|
||||||
|
this.hasNodeSelector = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:mode="mode"
|
||||||
|
:resource="value"
|
||||||
|
:errors="errors"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
@finish="save"
|
||||||
|
@cancel="done"
|
||||||
|
@error="e => errors = e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:namespaced="false"
|
||||||
|
description-key="spec.description"
|
||||||
|
@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('harvester.hostNetworkConfig.tabs.mode')"
|
||||||
|
:weight="99"
|
||||||
|
class="bordered-table"
|
||||||
|
>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="networkMode"
|
||||||
|
name="hostNetworkConfigMode"
|
||||||
|
:options="modeOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:row="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="clusterNetwork"
|
||||||
|
:label="t('harvester.network.clusterNetwork.label')"
|
||||||
|
:options="clusterNetworkOptions"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value.number="vlanID"
|
||||||
|
type="number"
|
||||||
|
required
|
||||||
|
:min="2"
|
||||||
|
:max="4094"
|
||||||
|
placeholder="e.g. 2 ~ 4094"
|
||||||
|
:label="t('harvester.hostNetworkConfig.vlanID.label')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<Checkbox
|
||||||
|
v-model:value="underlay"
|
||||||
|
:label="t('harvester.hostNetworkConfig.underlay.label')"
|
||||||
|
:tooltip="t('harvester.hostNetworkConfig.underlay.tooltip')"
|
||||||
|
:mode="mode"
|
||||||
|
:disabled="underlayDisabled"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="!kubeovnEnabled"
|
||||||
|
class="underlay-conflict-warning"
|
||||||
|
>
|
||||||
|
<i class="icon icon-warning" />
|
||||||
|
<MessageLink
|
||||||
|
:to="kubeovnAddonTo"
|
||||||
|
prefix-label="harvester.hostNetworkConfig.underlay.noKubeovn.prefix"
|
||||||
|
middle-label="harvester.hostNetworkConfig.underlay.noKubeovn.middle"
|
||||||
|
suffix-label="harvester.hostNetworkConfig.underlay.noKubeovn.suffix"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="underlayConflict"
|
||||||
|
class="underlay-conflict-warning"
|
||||||
|
>
|
||||||
|
<i class="icon icon-warning" />
|
||||||
|
{{ t('harvester.hostNetworkConfig.underlay.conflict', { name: underlayConflict.nameDisplay || underlayConflict.id }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="isStaticMode">
|
||||||
|
<hr class="section-divider" />
|
||||||
|
<div
|
||||||
|
v-for="node in nodes"
|
||||||
|
:key="node.id"
|
||||||
|
class="row mb-10 ips-row"
|
||||||
|
>
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabeledInput
|
||||||
|
:value="node.nameDisplay || node.id"
|
||||||
|
:label="t('harvester.hostNetworkConfig.ips.nodeLabel')"
|
||||||
|
mode="view"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-5">
|
||||||
|
<LabeledInput
|
||||||
|
:value="ips[node.id]"
|
||||||
|
:label="t('harvester.hostNetworkConfig.ips.label')"
|
||||||
|
:placeholder="t('harvester.hostNetworkConfig.ips.placeholder')"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
@update:value="updateIp(node.id, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
name="nodeSelector"
|
||||||
|
:label="t('harvester.hostNetworkConfig.tabs.nodeSelector')"
|
||||||
|
:weight="98"
|
||||||
|
>
|
||||||
|
<template v-if="hasNodeSelector">
|
||||||
|
<InfoBox class="node-selector-box">
|
||||||
|
<button
|
||||||
|
v-if="!isView"
|
||||||
|
type="button"
|
||||||
|
class="role-link btn btn-sm remove"
|
||||||
|
:aria-label="t('generic.remove')"
|
||||||
|
@click="removeNodeSelector"
|
||||||
|
>
|
||||||
|
<i class="icon icon-x" />
|
||||||
|
</button>
|
||||||
|
<HarvesterNodeSelector
|
||||||
|
class="mt-20"
|
||||||
|
:value="value.spec.nodeSelector"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</InfoBox>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn role-secondary"
|
||||||
|
:disabled="isView"
|
||||||
|
@click="addNodeSelector"
|
||||||
|
>
|
||||||
|
{{ t('harvester.hostNetworkConfig.nodeSelector.addButton') }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.section-divider {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
margin: 10px 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-selector-box {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.remove {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.underlay-conflict-warning {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--warning);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
23
pkg/harvester/formatters/HarvesterBoolean.vue
Normal file
23
pkg/harvester/formatters/HarvesterBoolean.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterBooleanFormatter',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span v-if="value">
|
||||||
|
<i class="icon icon-checkmark" />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-muted"
|
||||||
|
>
|
||||||
|
—
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
29
pkg/harvester/formatters/HarvesterHostNetworkConfigMode.vue
Normal file
29
pkg/harvester/formatters/HarvesterHostNetworkConfigMode.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterHostNetworkConfigModeFormatter',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
displayMode() {
|
||||||
|
if (this.value?.toLowerCase() === 'dhcp') {
|
||||||
|
return 'DHCP';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.value?.toLowerCase() === 'static') {
|
||||||
|
return 'Static';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span>{{ displayMode }}</span>
|
||||||
|
</template>
|
||||||
@ -295,6 +295,12 @@ harvester:
|
|||||||
harvesterIpAddress:
|
harvesterIpAddress:
|
||||||
customIpTooltip: "Custom IP (set via annotation)"
|
customIpTooltip: "Custom IP (set via annotation)"
|
||||||
tableHeaders:
|
tableHeaders:
|
||||||
|
hostNetworkConfig:
|
||||||
|
underlay: Underlay
|
||||||
|
underlayTooltip: Allow this interface to act as the underlay for VM overlay networks.
|
||||||
|
vlanID: VLAN ID
|
||||||
|
mode: Mode
|
||||||
|
clusterNetwork: Cluster Network
|
||||||
imageEncryption: Encryption
|
imageEncryption: Encryption
|
||||||
size: Size
|
size: Size
|
||||||
virtualSize: Virtual Size
|
virtualSize: Virtual Size
|
||||||
@ -1871,6 +1877,32 @@ harvester:
|
|||||||
addLabel: Add CIDR
|
addLabel: Add CIDR
|
||||||
range:
|
range:
|
||||||
addLabel: Add Range
|
addLabel: Add Range
|
||||||
|
hostNetworkConfig:
|
||||||
|
label: Host Networks
|
||||||
|
mode:
|
||||||
|
label: Mode
|
||||||
|
tabs:
|
||||||
|
mode: Mode
|
||||||
|
nodeSelector: Node Selector
|
||||||
|
nodeSelector:
|
||||||
|
addButton: Add Node Selector
|
||||||
|
underlay:
|
||||||
|
label: Underlay
|
||||||
|
tooltip: Allow this interface to act as the underlay for VM overlay networks.
|
||||||
|
conflict: '`{name}` host network config already has underlay enabled. Only one underlay is allowed in the cluster.'
|
||||||
|
noKubeovn:
|
||||||
|
prefix: The kubeovn-operator add-on is not enabled. Click
|
||||||
|
middle: here
|
||||||
|
suffix: to enable the add-on for overlay networking.
|
||||||
|
vlanID:
|
||||||
|
label: VLAN ID
|
||||||
|
ipRange:
|
||||||
|
label: IP Range ({node})
|
||||||
|
placeholder: e.g. 192.168.1.10/24
|
||||||
|
ips:
|
||||||
|
nodeLabel: Node
|
||||||
|
label: IP
|
||||||
|
placeholder: 'e.g. 192.168.1.10/24'
|
||||||
|
|
||||||
service:
|
service:
|
||||||
healthCheckPort:
|
healthCheckPort:
|
||||||
@ -2194,6 +2226,11 @@ typeLabel:
|
|||||||
one { Cluster Network }
|
one { Cluster Network }
|
||||||
other { Cluster Networks }
|
other { Cluster Networks }
|
||||||
}
|
}
|
||||||
|
network.harvesterhci.io.hostnetworkconfig: |-
|
||||||
|
{count, plural,
|
||||||
|
one { Host Network }
|
||||||
|
other { Host Networks }
|
||||||
|
}
|
||||||
harvesterhci.io.addon: |-
|
harvesterhci.io.addon: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
one { Add-on }
|
one { Add-on }
|
||||||
|
|||||||
@ -0,0 +1,86 @@
|
|||||||
|
<script>
|
||||||
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import { STATE, NAME as NAME_COL, AGE } from '@shell/config/table-headers';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
|
const UNDERLAY = {
|
||||||
|
name: 'underlay',
|
||||||
|
labelKey: 'harvester.tableHeaders.hostNetworkConfig.underlay',
|
||||||
|
tooltip: 'harvester.tableHeaders.hostNetworkConfig.underlayTooltip',
|
||||||
|
value: 'spec.underlay',
|
||||||
|
sort: 'spec.underlay',
|
||||||
|
formatter: 'HarvesterBoolean',
|
||||||
|
};
|
||||||
|
|
||||||
|
const VLAN_ID = {
|
||||||
|
name: 'vlanID',
|
||||||
|
labelKey: 'harvester.tableHeaders.hostNetworkConfig.vlanID',
|
||||||
|
value: 'spec.vlanID',
|
||||||
|
sort: 'spec.vlanID',
|
||||||
|
};
|
||||||
|
|
||||||
|
const MODE = {
|
||||||
|
name: 'mode',
|
||||||
|
labelKey: 'harvester.tableHeaders.hostNetworkConfig.mode',
|
||||||
|
value: 'spec.mode',
|
||||||
|
sort: 'spec.mode',
|
||||||
|
formatter: 'HarvesterHostNetworkConfigMode',
|
||||||
|
};
|
||||||
|
|
||||||
|
const CLUSTER_NETWORK = {
|
||||||
|
name: 'clusterNetwork',
|
||||||
|
labelKey: 'harvester.tableHeaders.hostNetworkConfig.clusterNetwork',
|
||||||
|
value: 'spec.clusterNetwork',
|
||||||
|
sort: 'spec.clusterNetwork',
|
||||||
|
align: 'center',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterListHostNetworkConfig',
|
||||||
|
components: { ResourceTable, Loading },
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HOST_NETWORK_CONFIG });
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { rows: [] };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
schema() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return this.$store.getters[`${ inStore }/schemaFor`](HCI.HOST_NETWORK_CONFIG);
|
||||||
|
},
|
||||||
|
headers() {
|
||||||
|
return [
|
||||||
|
STATE,
|
||||||
|
NAME_COL,
|
||||||
|
UNDERLAY,
|
||||||
|
VLAN_ID,
|
||||||
|
MODE,
|
||||||
|
CLUSTER_NETWORK,
|
||||||
|
AGE,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<ResourceTable
|
||||||
|
v-else
|
||||||
|
v-bind="$attrs"
|
||||||
|
:headers="headers"
|
||||||
|
:rows="rows"
|
||||||
|
:schema="schema"
|
||||||
|
key-field="_key"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@ -16,6 +16,7 @@ export const HCI = {
|
|||||||
RESTORE: 'harvesterhci.io.virtualmachinerestore',
|
RESTORE: 'harvesterhci.io.virtualmachinerestore',
|
||||||
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
|
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
|
||||||
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
|
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
|
||||||
|
HOST_NETWORK_CONFIG: 'network.harvesterhci.io.hostnetworkconfig',
|
||||||
SUBNET: 'kubeovn.io.subnet',
|
SUBNET: 'kubeovn.io.subnet',
|
||||||
VPC: 'kubeovn.io.vpc',
|
VPC: 'kubeovn.io.vpc',
|
||||||
IP: 'kubeovn.io.ip',
|
IP: 'kubeovn.io.ip',
|
||||||
@ -62,6 +63,7 @@ export const HCI = {
|
|||||||
VMIMPORT_SOURCE_OVA: 'migration.harvesterhci.io.ovasource',
|
VMIMPORT_SOURCE_OVA: 'migration.harvesterhci.io.ovasource',
|
||||||
VMIMPORT: 'migration.harvesterhci.io.virtualmachineimport',
|
VMIMPORT: 'migration.harvesterhci.io.virtualmachineimport',
|
||||||
MIGRATION: 'migration.harvesterhci.io',
|
MIGRATION: 'migration.harvesterhci.io',
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';
|
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user