mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-01 03:36:04 +08:00
Add pkg/harvester components + shell portings - 1
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
<script>
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import Tags from '../../components/DiskTags';
|
||||
import ArrayList from '@shell/components/form/ArrayList';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
|
||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import Loading from '@shell/components/Loading';
|
||||
|
||||
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
||||
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
|
||||
import { STORAGE_CLASS, LONGHORN } from '@shell/config/types';
|
||||
import { CSI_DRIVER } from '../../types';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { clone } from '@shell/utils/object';
|
||||
|
||||
const LONGHORN_DRIVER = 'driver.longhorn.io';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterStorage',
|
||||
|
||||
components: {
|
||||
ArrayList,
|
||||
CruResource,
|
||||
LabeledSelect,
|
||||
LabeledInput,
|
||||
NameNsDescription,
|
||||
RadioGroup,
|
||||
Tab,
|
||||
Tabbed,
|
||||
Loading,
|
||||
Tags,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
data() {
|
||||
const reclaimPolicyOptions = [{
|
||||
label: this.t('storageClass.customize.reclaimPolicy.delete'),
|
||||
value: 'Delete'
|
||||
}, {
|
||||
label: this.t('storageClass.customize.reclaimPolicy.retain'),
|
||||
value: 'Retain'
|
||||
}];
|
||||
|
||||
const allowVolumeExpansionOptions = [
|
||||
{
|
||||
label: this.t('generic.enabled'),
|
||||
value: true
|
||||
},
|
||||
{
|
||||
label: this.t('generic.disabled'),
|
||||
value: false
|
||||
}
|
||||
];
|
||||
|
||||
const volumeBindingModeOptions = [
|
||||
{
|
||||
label: this.t('storageClass.customize.volumeBindingMode.now'),
|
||||
value: 'Immediate'
|
||||
},
|
||||
{
|
||||
label: this.t('harvester.storage.customize.volumeBindingMode.later'),
|
||||
value: 'WaitForFirstConsumer'
|
||||
}
|
||||
];
|
||||
|
||||
const allowedTopologies = clone(this.value.allowedTopologies?.[0]?.matchLabelExpressions || []);
|
||||
|
||||
this.$set(this.value, 'parameters', this.value.parameters || {});
|
||||
this.$set(this.value, 'provisioner', this.value.provisioner || LONGHORN_DRIVER);
|
||||
this.$set(this.value, 'allowVolumeExpansion', this.value.allowVolumeExpansion || allowVolumeExpansionOptions[0].value);
|
||||
this.$set(this.value, 'reclaimPolicy', this.value.reclaimPolicy || reclaimPolicyOptions[0].value);
|
||||
this.$set(this.value, 'volumeBindingMode', this.value.volumeBindingMode || volumeBindingModeOptions[0].value);
|
||||
|
||||
return {
|
||||
reclaimPolicyOptions,
|
||||
allowVolumeExpansionOptions,
|
||||
volumeBindingModeOptions,
|
||||
mountOptions: [],
|
||||
provisioner: LONGHORN_DRIVER,
|
||||
STORAGE_CLASS,
|
||||
allowedTopologies,
|
||||
defaultAddValue: {
|
||||
key: '',
|
||||
values: [],
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = {
|
||||
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
||||
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
||||
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }),
|
||||
};
|
||||
|
||||
await allHash(hash);
|
||||
},
|
||||
|
||||
computed: {
|
||||
showUnsupportedStorage: mapFeature(UNSUPPORTED_STORAGE_DRIVERS),
|
||||
|
||||
inStore() {
|
||||
return this.$store.getters['currentProduct'].inStore;
|
||||
},
|
||||
|
||||
modeOverride() {
|
||||
return this.isCreate ? _CREATE : _VIEW;
|
||||
},
|
||||
|
||||
provisionerWatch() {
|
||||
return this.value.provisioner;
|
||||
},
|
||||
|
||||
provisioners() {
|
||||
const csiDrivers = this.$store.getters[`${ this.inStore }/all`](CSI_DRIVER) || [];
|
||||
const format = { [LONGHORN_DRIVER]: 'storageClass.longhorn.title' };
|
||||
|
||||
return csiDrivers.map((provisioner) => {
|
||||
return {
|
||||
label: format[provisioner.name] || provisioner.name,
|
||||
value: provisioner.name,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
schema() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return this.$store.getters[`${ inStore }/schemaFor`](STORAGE_CLASS);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
provisionerWatch() {
|
||||
this.$set(this.value, 'parameters', {});
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.registerBeforeHook(this.willSave, 'willSave');
|
||||
},
|
||||
|
||||
methods: {
|
||||
getComponent(name) {
|
||||
try {
|
||||
return require(`./provisioners/${ name }`).default;
|
||||
} catch {
|
||||
return require(`./provisioners/custom`).default;
|
||||
}
|
||||
},
|
||||
|
||||
updateProvisioner(provisioner) {
|
||||
this.$set(this.value, 'provisioner', provisioner);
|
||||
this.$set(this.value, 'allowVolumeExpansion', provisioner === LONGHORN_DRIVER);
|
||||
},
|
||||
|
||||
willSave() {
|
||||
Object.keys(this.value.parameters).forEach((key) => {
|
||||
if (this.value.parameters[key] === null || this.value.parameters[key] === '') {
|
||||
delete this.value.parameters[key];
|
||||
}
|
||||
});
|
||||
|
||||
this.formatAllowedTopoloties();
|
||||
},
|
||||
|
||||
formatAllowedTopoloties() {
|
||||
const neu = this.allowedTopologies;
|
||||
|
||||
if (!neu || neu.length === 0) {
|
||||
delete this.value.allowedTopologies;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const matchLabelExpressions = neu.filter(R => !!R.key.trim() && (R.values.length > 0 && !R.values.find(V => !V.trim())));
|
||||
|
||||
if (matchLabelExpressions.length > 0) {
|
||||
this.value.allowedTopologies = [{ matchLabelExpressions }];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" />
|
||||
<CruResource
|
||||
v-else
|
||||
:done-route="doneRoute"
|
||||
:mode="mode"
|
||||
:resource="value"
|
||||
:subtypes="[]"
|
||||
:validation-passed="true"
|
||||
:apply-hooks="applyHooks"
|
||||
:errors="errors"
|
||||
@error="e=>errors = e"
|
||||
@finish="save"
|
||||
@cancel="done"
|
||||
>
|
||||
<NameNsDescription
|
||||
:namespaced="false"
|
||||
:value="value"
|
||||
:mode="mode"
|
||||
:register-before-hook="registerBeforeHook"
|
||||
/>
|
||||
<LabeledSelect
|
||||
:value="value.provisioner"
|
||||
label="Provisioner"
|
||||
:options="provisioners"
|
||||
:localized-label="true"
|
||||
:mode="modeOverride"
|
||||
:searchable="true"
|
||||
:taggable="true"
|
||||
class="mb-20"
|
||||
@input="updateProvisioner($event)"
|
||||
/>
|
||||
<Tabbed :side-tabs="true">
|
||||
<Tab name="parameters" :label="t('storageClass.parameters.label')" :weight="2">
|
||||
<component
|
||||
:is="getComponent(value.provisioner)"
|
||||
:key="value.provisioner"
|
||||
:value="value"
|
||||
:mode="modeOverride"
|
||||
:real-mode="realMode"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab name="customize" :label="t('storageClass.customize.label')">
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<RadioGroup
|
||||
v-model="value.reclaimPolicy"
|
||||
name="reclaimPolicy"
|
||||
:label="t('storageClass.customize.reclaimPolicy.label')"
|
||||
:mode="modeOverride"
|
||||
:options="reclaimPolicyOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<RadioGroup
|
||||
v-model="value.allowVolumeExpansion"
|
||||
name="allowVolumeExpansion"
|
||||
:label="t('storageClass.customize.allowVolumeExpansion.label')"
|
||||
:mode="modeOverride"
|
||||
:options="allowVolumeExpansionOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<RadioGroup
|
||||
v-model="value.volumeBindingMode"
|
||||
name="volumeBindingMode"
|
||||
:label="t('storageClass.customize.volumeBindingMode.label')"
|
||||
:mode="modeOverride"
|
||||
:options="volumeBindingModeOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab
|
||||
name="allowedTopologies"
|
||||
:label="t('harvester.storage.allowedTopologies.title')"
|
||||
:weight="-1"
|
||||
:tooltip="t('harvester.storage.allowedTopologies.tooltip')"
|
||||
>
|
||||
<ArrayList
|
||||
v-model="allowedTopologies"
|
||||
:default-add-value="defaultAddValue"
|
||||
:initial-empty-row="true"
|
||||
:show-header="true"
|
||||
:mode="modeOverride"
|
||||
>
|
||||
<template v-slot:column-headers>
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col span-4 key">
|
||||
{{ t('generic.key') }}
|
||||
<span class="required">*</span>
|
||||
</div>
|
||||
<div class="col span-8 value">
|
||||
{{ t('generic.value') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:columns="scope">
|
||||
<div class="row custom-headers">
|
||||
<div class="col span-4 key">
|
||||
<LabeledInput
|
||||
v-model="scope.row.value.key"
|
||||
:required="true"
|
||||
:mode="modeOverride"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-8 value">
|
||||
<Tags
|
||||
v-model="scope.row.value.values"
|
||||
:add-label="t('generic.add')"
|
||||
:mode="modeOverride"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ArrayList>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</CruResource>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-headers {
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script>
|
||||
import KeyValue from '@shell/components/form/KeyValue';
|
||||
|
||||
export default {
|
||||
components: { KeyValue },
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<KeyValue
|
||||
v-model="value.parameters"
|
||||
:add-label="t('storageClass.custom.addLabel')"
|
||||
:read-allowed="false"
|
||||
:mode="mode"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,249 @@
|
||||
<script>
|
||||
import KeyValue from '@shell/components/form/KeyValue';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import RadioGroup from '@components/Form/Radio/RadioGroup';
|
||||
|
||||
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
||||
import { LONGHORN } from '@shell/config/types';
|
||||
import { clone } from '@shell/utils/object';
|
||||
import { uniq } from '@shell/utils/array';
|
||||
|
||||
const DEFAULT_PARAMETERS = [
|
||||
'numberOfReplicas',
|
||||
'staleReplicaTimeout',
|
||||
'diskSelector',
|
||||
'nodeSelector',
|
||||
'migratable',
|
||||
];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
KeyValue,
|
||||
LabeledSelect,
|
||||
LabeledInput,
|
||||
RadioGroup,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
realMode: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
if (this.realMode === _CREATE) {
|
||||
this.$set(this.value, 'parameters', {
|
||||
numberOfReplicas: '3',
|
||||
staleReplicaTimeout: '30',
|
||||
diskSelector: null,
|
||||
nodeSelector: null,
|
||||
migratable: 'true',
|
||||
});
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
|
||||
computed: {
|
||||
longhornNodes() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return this.$store.getters[`${ inStore }/all`](LONGHORN.NODES);
|
||||
},
|
||||
|
||||
nodeTags() {
|
||||
return (this.longhornNodes || []).reduce((sum, node) => {
|
||||
const tags = node.spec?.tags || [];
|
||||
|
||||
return uniq([...sum, ...tags]);
|
||||
}, []);
|
||||
},
|
||||
|
||||
diskTags() {
|
||||
return (this.longhornNodes || []).reduce((sum, node) => {
|
||||
const disks = node.spec?.disks;
|
||||
|
||||
const tagsOfNode = Object.keys(disks).reduce((sum, key) => {
|
||||
const tags = disks[key]?.tags || [];
|
||||
|
||||
return uniq([...sum, ...tags]);
|
||||
}, []);
|
||||
|
||||
return uniq([...sum, ...tagsOfNode]);
|
||||
}, []);
|
||||
},
|
||||
|
||||
isView() {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
|
||||
migratableOptions() {
|
||||
return [{
|
||||
label: this.t('generic.yes'),
|
||||
value: 'true'
|
||||
}, {
|
||||
label: this.t('generic.no'),
|
||||
value: 'false'
|
||||
}];
|
||||
},
|
||||
|
||||
parameters: {
|
||||
get() {
|
||||
const parameters = clone(this.value?.parameters) || {};
|
||||
|
||||
DEFAULT_PARAMETERS.map((key) => {
|
||||
delete parameters[key];
|
||||
});
|
||||
|
||||
return parameters;
|
||||
},
|
||||
|
||||
set(value) {
|
||||
Object.assign(this.value.parameters, value);
|
||||
}
|
||||
},
|
||||
|
||||
nodeSelector: {
|
||||
get() {
|
||||
const nodeSelector = this.value?.parameters?.nodeSelector;
|
||||
|
||||
if ((nodeSelector || '').includes(',')) {
|
||||
return nodeSelector.split(',');
|
||||
} else if (nodeSelector) {
|
||||
return [nodeSelector];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
set(value) {
|
||||
this.value.parameters.nodeSelector = (value || []).join(',');
|
||||
}
|
||||
},
|
||||
|
||||
diskSelector: {
|
||||
get() {
|
||||
const diskSelector = this.value?.parameters?.diskSelector;
|
||||
|
||||
if ((diskSelector || '').includes(',')) {
|
||||
return diskSelector.split(',');
|
||||
} else if (diskSelector) {
|
||||
return [diskSelector];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
set(value) {
|
||||
this.value.parameters.diskSelector = (value || []).join(',');
|
||||
}
|
||||
},
|
||||
|
||||
numberOfReplicas: {
|
||||
get() {
|
||||
return this.value?.parameters?.numberOfReplicas;
|
||||
},
|
||||
|
||||
set(value) {
|
||||
if (value >= 1 && value <= 3) {
|
||||
this.value.parameters.numberOfReplicas = String(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="numberOfReplicas"
|
||||
:label="t('harvester.storage.parameters.numberOfReplicas.label')"
|
||||
:required="true"
|
||||
:mode="mode"
|
||||
min="1"
|
||||
max="3"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="value.parameters.staleReplicaTimeout"
|
||||
:label="t('harvester.storage.parameters.staleReplicaTimeout.label')"
|
||||
:required="true"
|
||||
:mode="mode"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-10">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model="nodeSelector"
|
||||
:label="t('harvester.storage.parameters.nodeSelector.label')"
|
||||
:options="nodeTags"
|
||||
:taggable="true"
|
||||
:multiple="true"
|
||||
:mode="mode"
|
||||
>
|
||||
<template #no-options="{ searching }">
|
||||
<span v-if="!searching" class="text-muted">
|
||||
{{ t('harvester.storage.parameters.nodeSelector.no-options', null, true) }}
|
||||
</span>
|
||||
</template>
|
||||
</LabeledSelect>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model="diskSelector"
|
||||
:label="t('harvester.storage.parameters.diskSelector.label')"
|
||||
:options="diskTags"
|
||||
:taggable="true"
|
||||
:multiple="true"
|
||||
:mode="mode"
|
||||
>
|
||||
<template #no-options="{ searching }">
|
||||
<span v-if="!searching" class="text-muted">
|
||||
{{ t('harvester.storage.parameters.diskSelector.no-options', null, true) }}
|
||||
</span>
|
||||
</template>
|
||||
</LabeledSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-10">
|
||||
<div class="col span-6">
|
||||
<RadioGroup
|
||||
v-model="value.parameters.migratable"
|
||||
name="layer3NetworkMode"
|
||||
:label="t('harvester.storage.parameters.migratable.label')"
|
||||
:mode="mode"
|
||||
:options="migratableOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<KeyValue
|
||||
v-model="parameters"
|
||||
:add-label="t('storageClass.longhorn.addLabel')"
|
||||
:read-allowed="false"
|
||||
:mode="mode"
|
||||
class="mt-10"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.labeled-input.compact-input {
|
||||
padding: 7px 10px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user