mirror of https://github.com/rancher/dashboard.git
305 lines
10 KiB
Vue
305 lines
10 KiB
Vue
<script>
|
|
import CreateEditView from '@/mixins/create-edit-view';
|
|
import CruResource from '@/components/CruResource';
|
|
import NameNsDescription from '@/components/form/NameNsDescription';
|
|
import ArrayList from '@/components/form/ArrayList';
|
|
import ArrayListGrouped from '@/components/form/ArrayListGrouped';
|
|
import LabeledSelect from '@/components/form/LabeledSelect';
|
|
import MatchExpressions from '@/components/form/MatchExpressions';
|
|
import Tab from '@/components/Tabbed/Tab';
|
|
import Tabbed from '@/components/Tabbed';
|
|
import NodeAffinity from '@/components/form/NodeAffinity';
|
|
import Checkbox from '@/components/form/Checkbox';
|
|
import uniq from 'lodash/uniq';
|
|
import UnitInput from '@/components/form/UnitInput';
|
|
import { NODE, PVC, STORAGE_CLASS } from '@/config/types';
|
|
import Loading from '@/components/Loading';
|
|
import { LONGHORN_PLUGIN, VOLUME_PLUGINS } from '@/models/persistentvolume';
|
|
import { _CREATE, _VIEW } from '@/config/query-params';
|
|
import { clone } from '@/utils/object';
|
|
import { parseSi } from '@/utils/units';
|
|
import { fetchFeatureFlag, UNSUPPORTED_STORAGE_DRIVERS } from '@/utils/feature-flag';
|
|
import InfoBox from '@/components/InfoBox';
|
|
|
|
export default {
|
|
name: 'PersistentVolume',
|
|
|
|
components: {
|
|
ArrayList,
|
|
ArrayListGrouped,
|
|
Checkbox,
|
|
CruResource,
|
|
InfoBox,
|
|
LabeledSelect,
|
|
Loading,
|
|
MatchExpressions,
|
|
NameNsDescription,
|
|
NodeAffinity,
|
|
Tab,
|
|
Tabbed,
|
|
UnitInput
|
|
},
|
|
|
|
mixins: [CreateEditView],
|
|
|
|
async fetch() {
|
|
const storageClasses = await this.$store.dispatch('cluster/findAll', { type: STORAGE_CLASS });
|
|
const pvcPromise = this.$store.dispatch('cluster/findAll', { type: PVC });
|
|
const featureFlagPromise = fetchFeatureFlag(this.$store, UNSUPPORTED_STORAGE_DRIVERS);
|
|
|
|
this.storageClassOptions = storageClasses.map(s => ({
|
|
label: s.name,
|
|
value: s.name
|
|
}));
|
|
this.storageClassOptions.unshift(this.NONE_OPTION);
|
|
await pvcPromise;
|
|
this.showUnsupportedStorage = await featureFlagPromise;
|
|
},
|
|
|
|
data() {
|
|
const NONE_OPTION = {
|
|
label: this.t('generic.none'),
|
|
value: ''
|
|
};
|
|
const defaultAccessModes = ['ReadWriteOnce'];
|
|
|
|
this.$set(this.value, 'spec', this.value.spec || {});
|
|
this.$set(this.value.spec, 'accessModes', this.value.spec.accessModes || defaultAccessModes);
|
|
this.$set(this.value.spec, 'capacity', this.value.spec.capacity || {});
|
|
this.$set(this.value.spec.capacity, 'storage', this.value.spec.capacity.storage || '10Gi');
|
|
this.$set(this.value.spec, 'storageClassName', this.value.spec.storageClassName || NONE_OPTION.value);
|
|
|
|
const foundPlugin = this.value.isLonghorn ? LONGHORN_PLUGIN : VOLUME_PLUGINS.find(plugin => this.value.spec[plugin.value]);
|
|
const plugin = (foundPlugin || VOLUME_PLUGINS[0]).value;
|
|
|
|
return {
|
|
storageClassOptions: [], plugin, NONE_OPTION, NODE, initialNodeAffinity: clone(this.value.spec.nodeAffinity), showUnsupportedStorage: false
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
readWriteOnce: {
|
|
get() {
|
|
return this.value.spec.accessModes.includes('ReadWriteOnce');
|
|
},
|
|
set(value) {
|
|
this.checkboxSetter('ReadWriteOnce', value);
|
|
}
|
|
},
|
|
readOnlyMany: {
|
|
get() {
|
|
return this.value.spec.accessModes.includes('ReadOnlyMany');
|
|
},
|
|
set(value) {
|
|
this.checkboxSetter('ReadOnlyMany', value);
|
|
}
|
|
},
|
|
readWriteMany: {
|
|
get() {
|
|
return this.value.spec.accessModes.includes('ReadWriteMany');
|
|
},
|
|
set(value) {
|
|
this.checkboxSetter('ReadWriteMany', value);
|
|
}
|
|
},
|
|
modeOverride() {
|
|
return this.isCreate ? _CREATE : _VIEW;
|
|
},
|
|
capacity: {
|
|
get() {
|
|
const bytes = parseSi(this.value.spec.capacity.storage);
|
|
|
|
return bytes / (1024 ** 3);
|
|
},
|
|
set(value) {
|
|
this.$set(this.value.spec.capacity, 'storage', `${ value }Gi`);
|
|
}
|
|
},
|
|
nodeSelectorTerms: {
|
|
get() {
|
|
return this.value.spec.nodeAffinity?.required?.nodeSelectorTerms || [];
|
|
},
|
|
set(value) {
|
|
if (value.length > 0) {
|
|
const defaultValue = { required: { nodeSelectorTerms: [] } };
|
|
|
|
if (!this.value.spec.nodeAffinity?.required?.nodeSelectorTerms) {
|
|
this.$set(this.value.spec, 'nodeAffinity', this.value.spec.nodeAffinity || defaultValue);
|
|
this.$set(this.value.spec.nodeAffinity, 'required', this.value.spec.nodeAffinity.required || defaultValue.required);
|
|
this.$set(this.value.spec.nodeAffinity.required, 'nodeSelectorTerms', this.value.spec.nodeAffinity.required.nodeSelectorTerms || defaultValue.required.nodeSelectorTerms);
|
|
}
|
|
|
|
this.$set(this.value.spec.nodeAffinity.required, 'nodeSelectorTerms', value);
|
|
} else {
|
|
this.$set(this.value.spec.nodeAffinity, 'nodeAffinity', undefined);
|
|
}
|
|
}
|
|
},
|
|
areNodeSelectorsRequired() {
|
|
return this.plugin === 'local';
|
|
},
|
|
plugins() {
|
|
return VOLUME_PLUGINS.filter(plugin => this.showUnsupportedStorage || plugin.supported);
|
|
}
|
|
},
|
|
|
|
created() {
|
|
this.registerBeforeHook(this.willSave, 'willSave');
|
|
},
|
|
|
|
methods: {
|
|
checkboxSetter(key, value) {
|
|
if (value) {
|
|
this.value.spec.accessModes.push(key);
|
|
this.$set(this.value, 'accessModes', uniq(this.value.spec.accessModes));
|
|
} else {
|
|
const indexOf = this.value.spec.accessModes.indexOf(key);
|
|
|
|
if (indexOf >= 0) {
|
|
this.value.spec.accessModes.splice(indexOf, 1);
|
|
}
|
|
}
|
|
},
|
|
getComponent(name) {
|
|
return require(`./plugins/${ name }`).default;
|
|
},
|
|
willSave() {
|
|
if (this.value.spec.storageClassName === this.NONE_OPTION.value) {
|
|
this.$set(this.value.spec, 'storageClassName', null);
|
|
}
|
|
|
|
if (!this.isCreate) {
|
|
this.$set(this.value.spec, 'nodeAffinity', this.initialNodeAffinity);
|
|
}
|
|
},
|
|
updatePlugin(value) {
|
|
const plugin = this.plugin === LONGHORN_PLUGIN.value ? 'csi' : this.plugin;
|
|
|
|
delete this.value.spec[plugin];
|
|
this.$set(this, 'plugin', value);
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<Loading v-if="$fetchState.pending" />
|
|
<CruResource
|
|
v-else
|
|
:done-route="doneRoute"
|
|
:mode="mode"
|
|
:resource="value"
|
|
:subtypes="[]"
|
|
:validation-passed="true"
|
|
:errors="errors"
|
|
@error="e=>errors = e"
|
|
@finish="save"
|
|
@cancel="done"
|
|
>
|
|
<NameNsDescription
|
|
:value="value"
|
|
:mode="mode"
|
|
:register-before-hook="registerBeforeHook"
|
|
:namespaced="false"
|
|
/>
|
|
|
|
<InfoBox v-if="value.claim">
|
|
<div class="row">
|
|
<div class="col span-6 text-center">
|
|
<label class="text-muted">Persistent Volume Claim:</label>
|
|
<n-link :to="value.claim.detailLocation">
|
|
{{ value.claim.namespacedName }}
|
|
</n-link>
|
|
</div>
|
|
<div class="col span-6 text-center">
|
|
<label class="text-muted">Age:</label>
|
|
<LiveDate class="live-date" :value="value.metadata.creationTimestamp" />
|
|
</div>
|
|
</div>
|
|
</InfoBox>
|
|
|
|
<div class="row mb-20 top-level">
|
|
<div class="col span-6">
|
|
<LabeledSelect
|
|
:value="plugin"
|
|
:label="'Volume Plugin'"
|
|
:localized-label="true"
|
|
option-label="labelKey"
|
|
:options="plugins"
|
|
:mode="modeOverride"
|
|
:required="true"
|
|
@input="updatePlugin($event)"
|
|
/>
|
|
</div>
|
|
<div class="col span-6">
|
|
<UnitInput
|
|
v-model="capacity"
|
|
:required="true"
|
|
:label="'Capacity'"
|
|
:suffix="'GiB'"
|
|
:mode="mode"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<Tabbed :side-tabs="true">
|
|
<Tab name="plugin-configuration" :label="'Plugin Configuration'" :weight="1">
|
|
<component :is="getComponent(plugin)" :key="plugin" :value="value" :mode="modeOverride" />
|
|
</Tab>
|
|
<Tab
|
|
name="customize"
|
|
:label="'Customize'"
|
|
:weight="0"
|
|
>
|
|
<div class="row mb-20">
|
|
<div class="col span-6">
|
|
<h3>{{ t('persistentVolume.customize.accessModes.label') }}</h3>
|
|
<div><Checkbox v-model="readWriteOnce" :label="t('persistentVolume.customize.accessModes.readWriteOnce')" :mode="mode" /></div>
|
|
<div><Checkbox v-model="readOnlyMany" :label="t('persistentVolume.customize.accessModes.readOnlyMany')" :mode="mode" /></div>
|
|
<div><Checkbox v-model="readWriteMany" :label="t('persistentVolume.customize.accessModes.readWriteMany')" :mode="mode" /></div>
|
|
</div>
|
|
<div class="col span-6">
|
|
<ArrayList
|
|
v-model="value.spec.mountOptions"
|
|
:mode="mode"
|
|
:title="t('persistentVolume.customize.mountOptions.label')"
|
|
:add-label="t('persistentVolume.customize.mountOptions.addLabel')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="row mb-20">
|
|
<div class="col span-6">
|
|
<LabeledSelect
|
|
v-model="value.spec.storageClassName"
|
|
:label="t('persistentVolume.customize.assignToStorageClass.label')"
|
|
:options="storageClassOptions"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col span-12">
|
|
<h3>{{ t('persistentVolume.customize.affinity.label') }} <span v-if="areNodeSelectorsRequired" class="required text-small">*</span></h3>
|
|
<ArrayListGrouped v-model="nodeSelectorTerms" :mode="modeOverride" :default-add-value="{matchExpressions:[]}" :add-label="t('workload.scheduling.affinity.addNodeSelector')">
|
|
<template #default="props">
|
|
<MatchExpressions
|
|
v-model="props.row.value.matchExpressions"
|
|
:mode="modeOverride"
|
|
:initial-empty-row="!isView"
|
|
class="col span-12"
|
|
:type="NODE"
|
|
:show-remove="false"
|
|
/>
|
|
</template>
|
|
</ArrayListGrouped>
|
|
</div>
|
|
</div>
|
|
</Tab>
|
|
</Tabbed>
|
|
</CruResource>
|
|
</template>
|
|
<style lang="scss" scoped>
|
|
.top-level .col > .labeled-select:not(.taggable) {
|
|
max-height: 100%;
|
|
height: 100%;
|
|
}
|
|
</style>
|