mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-01 03:36:04 +08:00
Latest changes from Harvester master - a537c1ae38eb7030542ac371f24ae3336cd9d422
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
<script>
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import MessageLink from '@shell/components/MessageLink';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { isCronValid } from '@pkg/harvester/utils/cron';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { HCI } from '../types';
|
||||
import { sortBy } from '@shell/utils/sort';
|
||||
import { BACKUP_TYPE } from '../config/types';
|
||||
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
||||
|
||||
export default {
|
||||
name: 'CreateVMSchedule',
|
||||
components: {
|
||||
CruResource,
|
||||
Tabbed,
|
||||
Tab,
|
||||
RadioGroup,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
MessageLink,
|
||||
Banner,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
async fetch() {
|
||||
const hash = await allHash({
|
||||
settings: this.$store.dispatch('harvester/findAll', { type: HCI.SETTING }),
|
||||
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
|
||||
});
|
||||
|
||||
this.allVms = hash.vms;
|
||||
this.settings = hash.settings;
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
if (this.mode === _CREATE) {
|
||||
const defaultNs = this.$store.getters['defaultNamespace'];
|
||||
const vmNamespace = this.$route.query?.vmNamespace || defaultNs;
|
||||
const vmName = this.$route.query?.vmName;
|
||||
|
||||
delete this.value.metadata.annotations;
|
||||
delete this.value.metadata.labels;
|
||||
|
||||
this.value['metadata'] = {
|
||||
namespace: vmNamespace,
|
||||
name: vmName ? `svmbackup-${ vmName }` : ''
|
||||
};
|
||||
|
||||
if (!this.value.spec) {
|
||||
this.value['spec'] = {
|
||||
cron: '',
|
||||
retain: 8,
|
||||
maxFailure: 4,
|
||||
vmbackup: {
|
||||
source: {
|
||||
apiGroup: 'kubevirt.io',
|
||||
kind: 'VirtualMachine',
|
||||
name: vmName || ''
|
||||
},
|
||||
type: BACKUP_TYPE.BACKUP
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { settings: [] };
|
||||
},
|
||||
|
||||
computed: {
|
||||
backupTargetResource() {
|
||||
return this.settings.find( O => O.id === 'backup-target');
|
||||
},
|
||||
isEmptyValue() {
|
||||
return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
|
||||
},
|
||||
canUpdate() {
|
||||
return this?.backupTargetResource?.canUpdate;
|
||||
},
|
||||
errorMessage() {
|
||||
return this.backupTargetResource?.errMessage;
|
||||
},
|
||||
canSave() {
|
||||
return !!this.value.spec.cron && isCronValid(this.value.spec.cron) &&
|
||||
!!this.value.metadata.name &&
|
||||
!!this.value.metadata.namespace &&
|
||||
!!this.value.spec.retain &&
|
||||
!!this.value.spec.maxFailure;
|
||||
},
|
||||
isBackupTargetUnAvailable() {
|
||||
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && (this.errorMessage || this.isEmptyValue) && this.canUpdate;
|
||||
},
|
||||
vmOptions() {
|
||||
const nsVmList = this.$store.getters['harvester/all'](HCI.VM).filter(vm => vm.metadata.namespace === this.value.metadata.namespace);
|
||||
const vmObjectLists = nsVmList.map(obj => ({
|
||||
label: obj.nameDisplay,
|
||||
value: obj.name,
|
||||
}));
|
||||
|
||||
return sortBy(vmObjectLists, 'label');
|
||||
},
|
||||
|
||||
namespaces() {
|
||||
const allNamespaces = this.$store.getters['allNamespaces'];
|
||||
const out = sortBy(
|
||||
allNamespaces.map((obj) => {
|
||||
return {
|
||||
label: obj.nameDisplay,
|
||||
value: obj.id,
|
||||
};
|
||||
}),
|
||||
'label'
|
||||
);
|
||||
|
||||
return out;
|
||||
},
|
||||
toBackupTargetSetting() {
|
||||
const { cluster } = this.$router?.currentRoute?.params || {};
|
||||
|
||||
return {
|
||||
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-id`,
|
||||
params: {
|
||||
resource: `${ HCI.SETTING }`,
|
||||
cluster,
|
||||
id: 'backup-target'
|
||||
},
|
||||
query: { mode: _EDIT }
|
||||
};
|
||||
},
|
||||
scheduleTypeOptions() {
|
||||
return [BACKUP_TYPE.BACKUP, BACKUP_TYPE.SNAPSHOT];
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'value.metadata.namespace'() {
|
||||
this.value.spec.vmbackup.source.name = '';
|
||||
},
|
||||
'value.spec.vmbackup.source.name'(neu) {
|
||||
this.value.metadata.name = `svm${ this.value.spec.vmbackup.type }-${ neu }`;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onTypeChange(newType) {
|
||||
this.value.metadata.name = `svm${ newType }-${ this.value.spec.vmbackup.source.name }`;
|
||||
},
|
||||
getBackupTargetValueIsEmpty(resource) {
|
||||
let out = true;
|
||||
|
||||
if (resource?.value) {
|
||||
try {
|
||||
const valueJson = JSON.parse(resource?.value);
|
||||
|
||||
out = !valueJson.type;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
validateFailure(count) {
|
||||
if (this.value.spec.retain && count > this.value.spec.retain) {
|
||||
this.value.spec['maxFailure'] = this.value.spec.retain;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CruResource
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
:validationPassed="canSave"
|
||||
@finish="save"
|
||||
@error="e=>errors = e"
|
||||
>
|
||||
<div class="banner">
|
||||
<Banner
|
||||
v-if="isBackupTargetUnAvailable"
|
||||
color="error"
|
||||
>
|
||||
<MessageLink
|
||||
v-if="isEmptyValue"
|
||||
:to="toBackupTargetSetting"
|
||||
:target="_blank"
|
||||
prefix-label="harvester.backup.message.noSetting.prefix"
|
||||
middle-label="harvester.backup.message.noSetting.middle"
|
||||
suffix-label="harvester.schedule.message.noSetting.suffix"
|
||||
/>
|
||||
<MessageLink
|
||||
v-else
|
||||
:to="toBackupTargetSetting"
|
||||
prefix-label="harvester.backup.message.errorTip.prefix"
|
||||
middle-label="harvester.backup.message.errorTip.middle"
|
||||
>
|
||||
<template v-slot:suffix>
|
||||
{{ t('harvester.backup.message.errorTip.suffix') }} {{ errorMessage }}
|
||||
</template>
|
||||
</MessageLink>
|
||||
</Banner>
|
||||
<div class="mb-30">
|
||||
<RadioGroup
|
||||
v-model:value="value.spec.vmbackup.type"
|
||||
name="model"
|
||||
:options="scheduleTypeOptions"
|
||||
:labels="[t('harvester.schedule.type.backup'), t('harvester.schedule.type.snapshot')]"
|
||||
:disabled="isEdit || isView"
|
||||
:mode="mode"
|
||||
row
|
||||
@input="onTypeChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="row mb-30">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model:value="value.metadata.namespace"
|
||||
:label="t('nameNsDescription.namespace.label')"
|
||||
:options="namespaces"
|
||||
required
|
||||
:disabled="isBackupTargetUnAvailable || isEdit || isView"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model:value="value.spec.vmbackup.source.name"
|
||||
:label="t('harvester.schedule.virtualMachine.title')"
|
||||
:placeholder="t('harvester.schedule.virtualMachine.placeholder')"
|
||||
:options="vmOptions"
|
||||
required
|
||||
:disabled="isBackupTargetUnAvailable || isEdit || isView"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tabbed v-bind="$attrs" class="mt-15" :side-tabs="true">
|
||||
<Tab name="basics" :label="t('harvester.network.tabs.basics')" :weight="99" class="bordered-table">
|
||||
<LabeledInput
|
||||
v-model:value="value.spec.cron"
|
||||
class="mb-30"
|
||||
type="cron"
|
||||
required
|
||||
:mode="mode"
|
||||
:label="t('harvester.schedule.cron')"
|
||||
placeholder="0 * * * *"
|
||||
:disabled="isBackupTargetUnAvailable || isView"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-model:value.number="value.spec.retain"
|
||||
v-int-number
|
||||
class="mb-30"
|
||||
:min="2"
|
||||
:max="250"
|
||||
type="number"
|
||||
:label="t('harvester.schedule.retain.label')"
|
||||
required
|
||||
:tooltip="t('harvester.schedule.retain.tooltip')"
|
||||
:disabled="isBackupTargetUnAvailable || isView"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-model:value.number="value.spec.maxFailure"
|
||||
v-int-number
|
||||
class="mb-30"
|
||||
:min="2"
|
||||
type="number"
|
||||
:label="t('harvester.schedule.maxFailure.label')"
|
||||
required
|
||||
:tooltip="t('harvester.schedule.maxFailure.tooltip')"
|
||||
:disabled="isBackupTargetUnAvailable || isView"
|
||||
@input="validateFailure"
|
||||
/>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</CruResource>
|
||||
</template>
|
||||
@@ -159,6 +159,10 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
cancelAction() {
|
||||
this.$router.go(-1);
|
||||
},
|
||||
|
||||
async saveRestore(buttonCb) {
|
||||
this.update();
|
||||
|
||||
@@ -261,7 +265,7 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Footer mode="create" class="footer" :errors="errors" @save="saveRestore" @done="done" />
|
||||
<Footer mode="create" class="footer" :errors="errors" @save="saveRestore" @done="cancelAction" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -157,6 +157,9 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
cancelAction() {
|
||||
this.$router.go(-1);
|
||||
},
|
||||
async saveRestore(buttonCb) {
|
||||
this.update();
|
||||
|
||||
@@ -241,7 +244,7 @@ export default {
|
||||
<LabeledSelect v-if="!restoreNewVm" v-model:value="deletionPolicy" :label="t('harvester.backup.restore.deletePreviousVolumes')" :options="deletionPolicyOption" />
|
||||
</div>
|
||||
|
||||
<Footer mode="create" class="footer" :errors="errors" @save="saveRestore" @done="done" />
|
||||
<Footer mode="create" class="footer" :errors="errors" @save="saveRestore" @done="cancelAction" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -225,13 +225,15 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
headerFor(type) {
|
||||
return {
|
||||
headerFor(type, hasVolBackups = false) {
|
||||
const mainHeader = {
|
||||
[SOURCE_TYPE.NEW]: this.$store.getters['i18n/t']('harvester.virtualMachine.volume.title.volume'),
|
||||
[SOURCE_TYPE.IMAGE]: this.$store.getters['i18n/t']('harvester.virtualMachine.volume.title.vmImage'),
|
||||
[SOURCE_TYPE.ATTACH_VOLUME]: this.$store.getters['i18n/t']('harvester.virtualMachine.volume.title.existingVolume'),
|
||||
[SOURCE_TYPE.CONTAINER]: this.$store.getters['i18n/t']('harvester.virtualMachine.volume.title.container'),
|
||||
}[type];
|
||||
|
||||
return hasVolBackups ? `${ mainHeader } and Backups` : mainHeader;
|
||||
},
|
||||
|
||||
update() {
|
||||
@@ -322,7 +324,7 @@ export default {
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
{{ headerFor(volume.source) }}
|
||||
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
|
||||
</span>
|
||||
</h3>
|
||||
<div>
|
||||
|
||||
+25
-3
@@ -3,6 +3,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
||||
import { Banner } from '@components/Banner';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditContainer',
|
||||
@@ -10,7 +11,7 @@ export default {
|
||||
emits: ['update'],
|
||||
|
||||
components: {
|
||||
LabeledInput, LabeledSelect, InputOrDisplay
|
||||
LabeledInput, LabeledSelect, InputOrDisplay, Banner
|
||||
},
|
||||
|
||||
props: {
|
||||
@@ -70,7 +71,6 @@ export default {
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-testid="input-hec-type"
|
||||
class="col span-6"
|
||||
@@ -111,7 +111,6 @@ export default {
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-testid="input-hec-bus"
|
||||
class="col span-6"
|
||||
@@ -131,5 +130,28 @@ export default {
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-20">
|
||||
<div
|
||||
v-if="value.volumeBackups"
|
||||
class="col span-6"
|
||||
>
|
||||
<InputOrDisplay
|
||||
:name="t('harvester.virtualMachine.volume.readyToUse')"
|
||||
:value="value.volumeBackups.readyToUse"
|
||||
:mode="mode"
|
||||
>
|
||||
<LabelValue
|
||||
:name="t('harvester.virtualMachine.volume.readyToUse')"
|
||||
:value="value.volumeBackups.readyToUse"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<Banner
|
||||
v-if="value.volumeBackups && value.volumeBackups.error && value.volumeBackups.error.message"
|
||||
color="error"
|
||||
class="mb-20"
|
||||
:label="value.volumeBackups.error.message"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+24
-3
@@ -1,9 +1,10 @@
|
||||
<script>
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||
|
||||
import { Banner } from '@components/Banner';
|
||||
import { sortBy } from '@shell/utils/sort';
|
||||
import { PVC } from '@shell/config/types';
|
||||
import { _CREATE } from '@shell/config/query-params';
|
||||
@@ -17,7 +18,7 @@ export default {
|
||||
emits: ['update'],
|
||||
|
||||
components: {
|
||||
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay
|
||||
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue, Banner
|
||||
},
|
||||
|
||||
props: {
|
||||
@@ -275,7 +276,6 @@ export default {
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-testid="input-hee-bus"
|
||||
class="col span-6"
|
||||
@@ -296,6 +296,27 @@ export default {
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
<div
|
||||
v-if="value.volumeBackups"
|
||||
class="col span-6"
|
||||
>
|
||||
<InputOrDisplay
|
||||
:name="t('harvester.virtualMachine.volume.readyToUse')"
|
||||
:value="value.volumeBackups.readyToUse"
|
||||
:mode="mode"
|
||||
>
|
||||
<LabelValue
|
||||
:name="t('harvester.virtualMachine.volume.readyToUse')"
|
||||
:value="value.volumeBackups.readyToUse"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<Banner
|
||||
v-if="value.volumeBackups && value.volumeBackups.error && value.volumeBackups.error.message"
|
||||
color="error"
|
||||
class="mb-20"
|
||||
:label="value.volumeBackups.error.message"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+23
-2
@@ -4,6 +4,7 @@ import UnitInput from '@shell/components/form/UnitInput';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { PVC } from '@shell/config/types';
|
||||
import { formatSi, parseSi } from '@shell/utils/units';
|
||||
import { HCI } from '../../../../types';
|
||||
@@ -18,7 +19,7 @@ export default {
|
||||
emits: ['update'],
|
||||
|
||||
components: {
|
||||
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue
|
||||
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue, Banner
|
||||
},
|
||||
|
||||
props: {
|
||||
@@ -101,6 +102,12 @@ export default {
|
||||
return image ? image.label : '-';
|
||||
},
|
||||
|
||||
readyToUse() {
|
||||
const val = String(this.value.volumeBackups?.readyToUse || false);
|
||||
|
||||
return ucFirst(val);
|
||||
},
|
||||
|
||||
pvcsResource() {
|
||||
const allPVCs = this.$store.getters['harvester/all'](PVC) || [];
|
||||
|
||||
@@ -301,7 +308,7 @@ export default {
|
||||
</div>
|
||||
<div
|
||||
v-if="isView"
|
||||
class="col span-6"
|
||||
class="col span-3"
|
||||
>
|
||||
<LabelValue
|
||||
:name="t('harvester.virtualMachine.volume.encryption')"
|
||||
@@ -309,5 +316,19 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-20">
|
||||
<div v-if="value.volumeBackups && isView" class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.virtualMachine.volume.readyToUse')"
|
||||
:value="readyToUse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Banner
|
||||
v-if="value.volumeBackups && value.volumeBackups.error && value.volumeBackups.error.message"
|
||||
color="error"
|
||||
class="mb-20"
|
||||
:label="value.volumeBackups.error.message"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,7 +4,7 @@ import UnitInput from '@shell/components/form/UnitInput';
|
||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
import { Banner } from '@components/Banner';
|
||||
import { PVC, STORAGE_CLASS } from '@shell/config/types';
|
||||
import { formatSi, parseSi } from '@shell/utils/units';
|
||||
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
||||
@@ -18,7 +18,7 @@ export default {
|
||||
emits: ['update'],
|
||||
|
||||
components: {
|
||||
InputOrDisplay, Loading, LabeledInput, LabeledSelect, UnitInput, LabelValue
|
||||
InputOrDisplay, Loading, LabeledInput, LabeledSelect, UnitInput, LabelValue, Banner
|
||||
},
|
||||
|
||||
props: {
|
||||
@@ -68,9 +68,16 @@ export default {
|
||||
return ucFirst(String(this.value.isEncrypted));
|
||||
},
|
||||
|
||||
readyToUse() {
|
||||
const val = String(this.value.volumeBackups?.readyToUse || false);
|
||||
|
||||
return ucFirst(val);
|
||||
},
|
||||
|
||||
isView() {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
|
||||
pvcsResource() {
|
||||
const allPVCs = this.$store.getters['harvester/all'](PVC) || [];
|
||||
|
||||
@@ -221,7 +228,6 @@ export default {
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div
|
||||
data-testid="input-hev-bus"
|
||||
@@ -248,5 +254,19 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-20">
|
||||
<div v-if="value.volumeBackups && isView" class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.virtualMachine.volume.readyToUse')"
|
||||
:value="readyToUse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Banner
|
||||
v-if="value.volumeBackups && value.volumeBackups.error && value.volumeBackups.error.message"
|
||||
color="error"
|
||||
class="mb-20"
|
||||
:label="value.volumeBackups.error.message"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script>
|
||||
import { isEqual } from 'lodash';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
|
||||
Reference in New Issue
Block a user