mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-02-04 15:01:46 +00:00
Latest changes from Harvester master - d7d9d4af8a88d677695d7aff47a81d52041dfcca
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
parent
d04cd35a08
commit
dc74441d26
@ -206,7 +206,7 @@ export default {
|
|||||||
|
|
||||||
<Banner
|
<Banner
|
||||||
v-if="errors.length"
|
v-if="errors.length"
|
||||||
color="warning"
|
color="error"
|
||||||
>
|
>
|
||||||
{{ errors }}
|
{{ errors }}
|
||||||
</Banner>
|
</Banner>
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import { AGE, ROLE, STATE, PRINCIPAL } from '@shell/config/table-headers';
|
|||||||
import Banner from '@components/Banner/Banner.vue';
|
import Banner from '@components/Banner/Banner.vue';
|
||||||
import Tabbed from '@shell/components/Tabbed/index.vue';
|
import Tabbed from '@shell/components/Tabbed/index.vue';
|
||||||
import Tab from '@shell/components/Tabbed/Tab.vue';
|
import Tab from '@shell/components/Tabbed/Tab.vue';
|
||||||
import SortableTable from '@shell/components/SortableTable';
|
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
|
||||||
@ -19,7 +18,6 @@ export default {
|
|||||||
ResourceTable,
|
ResourceTable,
|
||||||
Tabbed,
|
Tabbed,
|
||||||
Tab,
|
Tab,
|
||||||
SortableTable
|
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@ -124,8 +124,8 @@ export default {
|
|||||||
<span v-if="setting.customized" class="modified">
|
<span v-if="setting.customized" class="modified">
|
||||||
Modified
|
Modified
|
||||||
</span>
|
</span>
|
||||||
<span v-if="setting.technicalPreview" v-clean-tooltip="t('advancedSettings.technicalPreview')" class="technical-preview">
|
<span v-if="setting.experimental" v-clean-tooltip="t('advancedSettings.experimental')" class="experimental">
|
||||||
Technical Preview
|
Experimental
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<h2 v-clean-html="t(setting.description, {}, true)">
|
<h2 v-clean-html="t(setting.description, {}, true)">
|
||||||
@ -216,9 +216,9 @@ export default {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.technical-preview {
|
.experimental {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
border: 1px solid var(--warning);
|
border: 1px solid var(--error);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import ModalWithCard from '@shell/components/ModalWithCard';
|
|||||||
const PREFERED_SHORTCUT_KEYS = 'prefered-shortcut-keys';
|
const PREFERED_SHORTCUT_KEYS = 'prefered-shortcut-keys';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
name: 'NovncConsoleCustomKeys',
|
||||||
|
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
@ -102,10 +104,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
show() {
|
|
||||||
this.$refs.recordShortcutKeys.open();
|
|
||||||
},
|
|
||||||
|
|
||||||
closeRecordingModal() {
|
closeRecordingModal() {
|
||||||
window.removeEventListener('keydown', this.handleShortcut);
|
window.removeEventListener('keydown', this.handleShortcut);
|
||||||
this.$emit('close');
|
this.$emit('close');
|
||||||
@ -172,7 +170,10 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWithCard ref="recordShortcutKeys" name="recordShortcutKeys" :width="550">
|
<ModalWithCard
|
||||||
|
name="recordShortcutKeys"
|
||||||
|
:width="550"
|
||||||
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<t k="harvester.virtualMachine.detail.console.customShortcutKeys" />
|
<t k="harvester.virtualMachine.detail.console.customShortcutKeys" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -104,6 +104,7 @@ const F_KEYS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
name: 'NovncConsoleWrapper',
|
||||||
components: {
|
components: {
|
||||||
NovncConsole, NovncConsoleItem, NovncConsoleCustomKeys
|
NovncConsole, NovncConsoleItem, NovncConsoleCustomKeys
|
||||||
},
|
},
|
||||||
@ -262,9 +263,6 @@ export default {
|
|||||||
|
|
||||||
showKeysModal() {
|
showKeysModal() {
|
||||||
this.renderKeysModal = true;
|
this.renderKeysModal = true;
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$refs.keysModal.show();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
hideKeysModal() {
|
hideKeysModal() {
|
||||||
@ -326,7 +324,11 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
</v-dropdown>
|
</v-dropdown>
|
||||||
|
|
||||||
<NovncConsoleCustomKeys v-if="renderKeysModal" ref="keysModal" :current-user="currentUser" @close="hideKeysModal" />
|
<NovncConsoleCustomKeys
|
||||||
|
v-if="renderKeysModal"
|
||||||
|
:current-user="currentUser"
|
||||||
|
@close="hideKeysModal"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<NovncConsole v-if="url && !isDown" ref="novncConsole" :url="url" />
|
<NovncConsole v-if="url && !isDown" ref="novncConsole" :url="url" />
|
||||||
<p v-if="isDown">
|
<p v-if="isDown">
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import InfoBox from '@shell/components/InfoBox';
|
|||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { CSI_DRIVER, VOLUME_SNAPSHOT_CLASS } from '../../types';
|
import { CSI_DRIVER, VOLUME_SNAPSHOT_CLASS } from '../../types';
|
||||||
|
|
||||||
const LONGHORN_DRIVER = 'driver.longhorn.io';
|
import { LONGHORN_DRIVER } from '@shell/config/types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterCsiDriver',
|
name: 'HarvesterCsiDriver',
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import semver from 'semver';
|
|||||||
const docVersion = `v${ semver.major(pkgJson.version) }.${ semver.minor(pkgJson.version) }`;
|
const docVersion = `v${ semver.major(pkgJson.version) }.${ semver.minor(pkgJson.version) }`;
|
||||||
|
|
||||||
export const DOC_LINKS = {
|
export const DOC_LINKS = {
|
||||||
CONSOLE_URL: `https://docs.harvesterhci.io/${ docVersion }/host/`,
|
CONSOLE_URL: `https://docs.harvesterhci.io/${ docVersion }/host/#remote-console`,
|
||||||
RANCHER_INTEGRATION_URL: `https://docs.harvesterhci.io/${ docVersion }/rancher/rancher-integration`,
|
RANCHER_INTEGRATION_URL: `https://docs.harvesterhci.io/${ docVersion }/rancher/rancher-integration`,
|
||||||
STORAGE_NETWORK_EXAMPLE: `https://docs.harvesterhci.io/${ docVersion }/advanced/storagenetwork#configuration-example`,
|
STORAGE_NETWORK_EXAMPLE: `https://docs.harvesterhci.io/${ docVersion }/advanced/storagenetwork#configuration-example`,
|
||||||
KSMTUNED_MODE: `https://docs.harvesterhci.io/${ docVersion }/host/#ksmtuned-mode`,
|
KSMTUNED_MODE: `https://docs.harvesterhci.io/${ docVersion }/host/#ksmtuned-mode`,
|
||||||
|
|||||||
@ -834,7 +834,7 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
label: 'Addons',
|
label: 'Add-ons',
|
||||||
group: 'advanced',
|
group: 'advanced',
|
||||||
name: HCI.ADD_ONS,
|
name: HCI.ADD_ONS,
|
||||||
ifHaveType: HCI.ADD_ONS,
|
ifHaveType: HCI.ADD_ONS,
|
||||||
|
|||||||
@ -68,6 +68,7 @@ export const ADD_ONS = {
|
|||||||
RANCHER_LOGGING: 'rancher-logging',
|
RANCHER_LOGGING: 'rancher-logging',
|
||||||
RANCHER_MONITORING: 'rancher-monitoring',
|
RANCHER_MONITORING: 'rancher-monitoring',
|
||||||
VM_IMPORT_CONTROLLER: 'vm-import-controller',
|
VM_IMPORT_CONTROLLER: 'vm-import-controller',
|
||||||
|
LVM_DRIVER: 'lvm.driver.harvesterhci.io',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CSI_SECRETS = {
|
export const CSI_SECRETS = {
|
||||||
|
|||||||
@ -62,4 +62,5 @@ export const HCI = {
|
|||||||
CPU_MANAGER: 'cpumanager',
|
CPU_MANAGER: 'cpumanager',
|
||||||
VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
|
VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
|
||||||
SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
|
SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
|
||||||
|
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -85,7 +85,7 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
kind: 'json', from: 'import', canReset: true
|
kind: 'json', from: 'import', canReset: true
|
||||||
},
|
},
|
||||||
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: {},
|
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: {},
|
||||||
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: { kind: 'boolean', technicalPreview: true },
|
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: { kind: 'boolean', experimental: true },
|
||||||
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import { Banner } from '@components/Banner';
|
|||||||
import HarvesterDisk from '../../mixins/harvester-disk';
|
import HarvesterDisk from '../../mixins/harvester-disk';
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
|
||||||
|
import { LONGHORN_VERSION_V1 } from '@shell/config/types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['update:value'],
|
emits: ['update:value'],
|
||||||
|
|
||||||
@ -86,6 +88,20 @@ export default {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
provisioner() {
|
||||||
|
let labelKey = `harvester.host.disk.storage.longhorn.${ LONGHORN_VERSION_V1 }.label`;
|
||||||
|
|
||||||
|
if (this.value?.blockDevice?.spec?.provisioner.longhorn) {
|
||||||
|
labelKey = `harvester.host.disk.storage.longhorn.${ this.value.blockDevice.spec.provisioner.longhorn.engineVersion }.label`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.value?.blockDevice?.spec?.provisioner.lvm) {
|
||||||
|
labelKey = 'harvester.host.disk.storage.lvm.label';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.t(labelKey);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
update() {
|
update() {
|
||||||
@ -198,12 +214,18 @@ export default {
|
|||||||
:value="value.displayName"
|
:value="value.displayName"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col span-4">
|
<div v-if="value.path" class="col span-4">
|
||||||
<LabelValue
|
<LabelValue
|
||||||
:name="t('harvester.host.disk.path.label')"
|
:name="t('harvester.host.disk.path.label')"
|
||||||
:value="value.path"
|
:value="value.path"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.disk.provisioner')"
|
||||||
|
:value="provisioner"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -103,11 +103,10 @@ export default {
|
|||||||
|
|
||||||
const blockDevices = this.$store.getters[`${ inStore }/all`](HCI.BLOCK_DEVICE);
|
const blockDevices = this.$store.getters[`${ inStore }/all`](HCI.BLOCK_DEVICE);
|
||||||
const provisionedBlockDevices = blockDevices.filter((d) => {
|
const provisionedBlockDevices = blockDevices.filter((d) => {
|
||||||
const provisioned = d?.spec?.fileSystem?.provisioned;
|
|
||||||
const isCurrentNode = d?.spec?.nodeName === this.value.id;
|
const isCurrentNode = d?.spec?.nodeName === this.value.id;
|
||||||
const isLonghornMounted = findBy(this.longhornDisks, 'name', d.metadata.name);
|
const isLonghornMounted = findBy(this.longhornDisks, 'name', d.metadata.name);
|
||||||
|
|
||||||
return provisioned && isCurrentNode && !isLonghornMounted;
|
return d?.isProvisioned && isCurrentNode && !isLonghornMounted;
|
||||||
})
|
})
|
||||||
.map((d) => {
|
.map((d) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -125,7 +125,7 @@ export default {
|
|||||||
|
|
||||||
<div class="vm-list mb-5">
|
<div class="vm-list mb-5">
|
||||||
<BadgeState
|
<BadgeState
|
||||||
v-for="(vm, i) in unhealthyVM.vms" :key="i" color="bg-error mb-5 mr-5"
|
v-for="(vm, k) in unhealthyVM.vms" :key="k" color="bg-error mb-5 mr-5"
|
||||||
:label="vm"
|
:label="vm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -63,6 +63,7 @@ export default {
|
|||||||
stringify,
|
stringify,
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
|
this.isOpen = false;
|
||||||
this.$store.commit('harvester-common/toggleBundleModal', false);
|
this.$store.commit('harvester-common/toggleBundleModal', false);
|
||||||
this.backUpName = '';
|
this.backUpName = '';
|
||||||
},
|
},
|
||||||
@ -109,10 +110,12 @@ export default {
|
|||||||
<app-modal
|
<app-modal
|
||||||
v-if="isOpen"
|
v-if="isOpen"
|
||||||
name="bundle-modal"
|
name="bundle-modal"
|
||||||
|
custom-class="bundleModal"
|
||||||
:click-to-close="false"
|
:click-to-close="false"
|
||||||
:width="550"
|
:width="550"
|
||||||
:height="390"
|
:height="390"
|
||||||
class="remove-modal support-modal"
|
class="remove-modal support-modal"
|
||||||
|
@close="close"
|
||||||
>
|
>
|
||||||
<div class="p-20">
|
<div class="p-20">
|
||||||
<h2>
|
<h2>
|
||||||
|
|||||||
@ -1,22 +1,35 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { CSI_DRIVER, LONGHORN, LONGHORN_DRIVER, LONGHORN_VERSION_V1, LONGHORN_VERSION_V2 } from '@shell/config/types';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import LabelValue from '@shell/components/LabelValue';
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
import { BadgeState } from '@components/BadgeState';
|
import { BadgeState } from '@components/BadgeState';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { RadioGroup, RadioButton } from '@components/Form/Radio';
|
import { RadioGroup, RadioButton } from '@components/Form/Radio';
|
||||||
import HarvesterDisk from '../../mixins/harvester-disk';
|
import HarvesterDisk from '../../mixins/harvester-disk';
|
||||||
import Tags from '../../components/DiskTags';
|
import Tags from '../../components/DiskTags';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
|
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import { LONGHORN_SYSTEM } from './index';
|
import { LONGHORN_SYSTEM } from './index';
|
||||||
|
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
||||||
|
import ModalWithCard from '@shell/components/ModalWithCard';
|
||||||
|
import { randomStr } from '@shell/utils/string';
|
||||||
|
import { LONGHORN_V2_DATA_ENGINE } from './index.vue';
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
|
||||||
|
const _NEW = '_NEW';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
LabeledInput,
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
LabelValue,
|
LabelValue,
|
||||||
BadgeState,
|
BadgeState,
|
||||||
Banner,
|
Banner,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
RadioButton,
|
RadioButton,
|
||||||
|
ModalWithCard,
|
||||||
Tags,
|
Tags,
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -35,18 +48,95 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
node: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'edit',
|
default: 'edit',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {};
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await allHash({
|
||||||
|
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }),
|
||||||
|
lvmVolumeGroups: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.LVM_VOLUME_GROUP }),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let provisioner = `${ this.value.provisioner || LONGHORN_DRIVER }`;
|
||||||
|
|
||||||
|
if (provisioner === LONGHORN_DRIVER) {
|
||||||
|
provisioner = `${ provisioner }_${ this.value.provisionerVersion || LONGHORN_VERSION_V1 }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
provisioner,
|
||||||
|
volumeGroupDialog: null,
|
||||||
|
randomStr: randomStr(10).toLowerCase(),
|
||||||
|
isOpen: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
provisioners() {
|
||||||
|
const out = [];
|
||||||
|
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const csiDrivers = this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
|
||||||
|
|
||||||
|
csiDrivers.forEach(({ name }) => {
|
||||||
|
switch (name) {
|
||||||
|
case LONGHORN_DRIVER:
|
||||||
|
out.push({
|
||||||
|
label: `harvester.host.disk.storage.longhorn.${ LONGHORN_VERSION_V1 }.label`,
|
||||||
|
value: `${ name }_${ LONGHORN_VERSION_V1 }`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.longhornSystemVersion === LONGHORN_VERSION_V2 || this.value.provisionerVersion === LONGHORN_VERSION_V2) {
|
||||||
|
out.push({
|
||||||
|
label: `harvester.host.disk.storage.longhorn.${ LONGHORN_VERSION_V2 }.label`,
|
||||||
|
value: `${ name }_${ LONGHORN_VERSION_V2 }`,
|
||||||
|
disabled: this.forceLonghornV1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case LVM_DRIVER:
|
||||||
|
out.push({
|
||||||
|
label: 'harvester.host.disk.storage.lvm.label',
|
||||||
|
value: name,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
lvmVolumeGroups() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const lvmVolumeGroups = this.$store.getters[`${ inStore }/all`](HCI.LVM_VOLUME_GROUP) || [];
|
||||||
|
|
||||||
|
const out = lvmVolumeGroups.filter(group => group.spec.nodeName === this.node.name).map(g => g.spec.vgName);
|
||||||
|
|
||||||
|
out.unshift({
|
||||||
|
label: this.t('harvester.host.disk.lvmVolumeGroup.create'),
|
||||||
|
value: _NEW,
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
targetDisk() {
|
targetDisk() {
|
||||||
return this.disks.find(disk => disk.name === this.value.name);
|
return this.disks.find(disk => disk.name === this.value.name);
|
||||||
},
|
},
|
||||||
|
|
||||||
schedulableTooltipMessage() {
|
schedulableTooltipMessage() {
|
||||||
const { name, path } = this.value;
|
const { name, path } = this.value;
|
||||||
|
|
||||||
@ -56,6 +146,7 @@ export default {
|
|||||||
return this.schedulableCondition.message;
|
return this.schedulableCondition.message;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
allowSchedulingOptions() {
|
allowSchedulingOptions() {
|
||||||
return [{
|
return [{
|
||||||
label: this.t('generic.enabled'),
|
label: this.t('generic.enabled'),
|
||||||
@ -87,7 +178,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
isProvisioned() {
|
isProvisioned() {
|
||||||
return this.blockDevice?.spec.fileSystem.provisioned;
|
return this.blockDevice?.isProvisioned;
|
||||||
},
|
},
|
||||||
|
|
||||||
forceFormattedDisabled() {
|
forceFormattedDisabled() {
|
||||||
@ -153,8 +244,83 @@ export default {
|
|||||||
isFormatting() {
|
isFormatting() {
|
||||||
return this.blockDevice.isFormatting;
|
return this.blockDevice.isFormatting;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
longhornSystemVersion() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const v2DataEngine = this.$store.getters[`${ inStore }/byId`](LONGHORN.SETTINGS, LONGHORN_V2_DATA_ENGINE) || {};
|
||||||
|
|
||||||
|
return v2DataEngine.value === 'true' ? LONGHORN_VERSION_V2 : LONGHORN_VERSION_V1;
|
||||||
|
},
|
||||||
|
|
||||||
|
forceLonghornV1() {
|
||||||
|
return this.node?.labels[HCI_LABELS_ANNOTATIONS.DISABLE_LONGHORN_V2_ENGINE] === 'true';
|
||||||
|
},
|
||||||
|
|
||||||
|
isLvm() {
|
||||||
|
return this.value.provisioner === LVM_DRIVER;
|
||||||
|
},
|
||||||
|
|
||||||
|
isLonghorn() {
|
||||||
|
return this.value.provisioner === LONGHORN_DRIVER;
|
||||||
|
},
|
||||||
|
|
||||||
|
isLonghornV1() {
|
||||||
|
return this.isLonghorn && this.value.provisionerVersion === LONGHORN_VERSION_V1;
|
||||||
|
},
|
||||||
|
|
||||||
|
provisionerTooltip() {
|
||||||
|
if (
|
||||||
|
this.mode === _EDIT &&
|
||||||
|
this.isLonghorn &&
|
||||||
|
this.longhornSystemVersion === LONGHORN_VERSION_V2 &&
|
||||||
|
this.forceLonghornV1
|
||||||
|
) {
|
||||||
|
return this.t('harvester.storage.storageClass.longhorn.versionTooltip');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
provisioner(value) {
|
||||||
|
this.randomStr = randomStr(10).toLowerCase();
|
||||||
|
|
||||||
|
const [provisioner, provisionerVersion] = value?.split('_');
|
||||||
|
|
||||||
|
this.value.provisioner = provisioner;
|
||||||
|
|
||||||
|
if (provisioner === LONGHORN_DRIVER) {
|
||||||
|
this.value.provisionerVersion = provisionerVersion || LONGHORN_VERSION_V1;
|
||||||
|
} else {
|
||||||
|
this.value.provisionerVersion = undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'value.lvmVolumeGroup'(neu) {
|
||||||
|
if (neu === _NEW) {
|
||||||
|
this.value.lvmVolumeGroup = null;
|
||||||
|
this.showCreateVolumeGroup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
showCreateVolumeGroup() {
|
||||||
|
this.volumeGroupDialog = null;
|
||||||
|
this.isOpen = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
hideCreateVolumeGroup() {
|
||||||
|
this.isOpen = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
saveCreateVolumeGroup(buttonCb) {
|
||||||
|
buttonCb(true);
|
||||||
|
this.value.lvmVolumeGroup = this.volumeGroupDialog;
|
||||||
|
this.hideCreateVolumeGroup();
|
||||||
|
},
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
this.$emit('update:value', this.value);
|
this.$emit('update:value', this.value);
|
||||||
},
|
},
|
||||||
@ -176,7 +342,7 @@ export default {
|
|||||||
:label="t('harvester.host.disk.fileSystem.formatting')"
|
:label="t('harvester.host.disk.fileSystem.formatting')"
|
||||||
/>
|
/>
|
||||||
<Banner
|
<Banner
|
||||||
v-else-if="isFormatted && !isCorrupted"
|
v-else-if="isFormatted && isLonghornV1 && !isCorrupted"
|
||||||
color="info"
|
color="info"
|
||||||
:label="formattedBannerLabel"
|
:label="formattedBannerLabel"
|
||||||
/>
|
/>
|
||||||
@ -256,8 +422,22 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="(value.isNew && !isFormatted) || isCorrupted" class="row mt-10">
|
|
||||||
<div class="col span-6">
|
<div class="row mt-10">
|
||||||
|
<div :class="`col span-${ value.isNew ? '6': '12' }`">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="provisioner"
|
||||||
|
:mode="mode"
|
||||||
|
label-key="harvester.host.disk.provisioner"
|
||||||
|
:localized-label="true"
|
||||||
|
:searchable="true"
|
||||||
|
:options="provisioners"
|
||||||
|
:disabled="isProvisioned || !value.isNew"
|
||||||
|
:tooltip="provisionerTooltip"
|
||||||
|
@keydown.native.enter.prevent="()=>{}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="(value.isNew && isLonghornV1 && !isFormatted) || isCorrupted" class="col span-6">
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
v-model:value="value.forceFormatted"
|
v-model:value="value.forceFormatted"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
@ -279,7 +459,44 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="value.isNew && isLvm" class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="value.lvmVolumeGroup"
|
||||||
|
:mode="mode"
|
||||||
|
label-key="harvester.host.disk.lvmVolumeGroup.label"
|
||||||
|
:localized-label="true"
|
||||||
|
:searchable="false"
|
||||||
|
:taggable="true"
|
||||||
|
:multiple="false"
|
||||||
|
:required="true"
|
||||||
|
:disabled="isProvisioned"
|
||||||
|
:options="lvmVolumeGroups"
|
||||||
|
@keydown.native.enter.prevent="()=>{}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ModalWithCard
|
||||||
|
v-if="isOpen"
|
||||||
|
:ref="randomStr"
|
||||||
|
:name="randomStr"
|
||||||
|
width="30%"
|
||||||
|
@finish="saveCreateVolumeGroup"
|
||||||
|
@close="hideCreateVolumeGroup"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
{{ t('harvester.host.disk.lvmVolumeGroup.label') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #content>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="volumeGroupDialog"
|
||||||
|
:label="t('generic.name')"
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
@keydown.native.enter.prevent="()=>{}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ModalWithCard>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
|
|||||||
import ButtonDropdown from '@shell/components/ButtonDropdown';
|
import ButtonDropdown from '@shell/components/ButtonDropdown';
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import { LONGHORN, SECRET } from '@shell/config/types';
|
import { LONGHORN, SECRET, LONGHORN_DRIVER, LONGHORN_VERSION_V1, LONGHORN_VERSION_V2 } from '@shell/config/types';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { formatSi } from '@shell/utils/units';
|
import { formatSi } from '@shell/utils/units';
|
||||||
import { findBy } from '@shell/utils/array';
|
import { findBy } from '@shell/utils/array';
|
||||||
@ -29,9 +29,13 @@ import HarvesterDisk from './HarvesterDisk';
|
|||||||
import HarvesterSeeder from './HarvesterSeeder';
|
import HarvesterSeeder from './HarvesterSeeder';
|
||||||
import HarvesterKsmtuned from './HarvesterKsmtuned';
|
import HarvesterKsmtuned from './HarvesterKsmtuned';
|
||||||
import Tags from '../../components/DiskTags';
|
import Tags from '../../components/DiskTags';
|
||||||
|
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
||||||
|
import isEqual from 'lodash/isEqual';
|
||||||
|
|
||||||
export const LONGHORN_SYSTEM = 'longhorn-system';
|
export const LONGHORN_SYSTEM = 'longhorn-system';
|
||||||
|
|
||||||
|
export const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditNode',
|
name: 'HarvesterEditNode',
|
||||||
|
|
||||||
@ -66,10 +70,11 @@ export default {
|
|||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
const hash = {
|
const hash = {
|
||||||
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
||||||
blockDevices: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BLOCK_DEVICE }),
|
blockDevices: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BLOCK_DEVICE }),
|
||||||
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
|
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
|
||||||
secrets: this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }),
|
secrets: this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }),
|
||||||
|
longhornV2DataEngine: this.$store.dispatch(`${ inStore }/find`, { type: LONGHORN.SETTINGS, id: LONGHORN_V2_DATA_ENGINE }),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.INVENTORY)) {
|
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.INVENTORY)) {
|
||||||
@ -80,23 +85,25 @@ export default {
|
|||||||
|
|
||||||
const blockDevices = this.$store.getters[`${ inStore }/all`](HCI.BLOCK_DEVICE);
|
const blockDevices = this.$store.getters[`${ inStore }/all`](HCI.BLOCK_DEVICE);
|
||||||
const provisionedBlockDevices = blockDevices.filter((d) => {
|
const provisionedBlockDevices = blockDevices.filter((d) => {
|
||||||
const provisioned = d?.spec?.fileSystem?.provisioned;
|
|
||||||
const isCurrentNode = d?.spec?.nodeName === this.value.id;
|
const isCurrentNode = d?.spec?.nodeName === this.value.id;
|
||||||
const isLonghornMounted = findBy(this.longhornDisks, 'name', d.metadata.name);
|
const isLonghornMounted = findBy(this.longhornDisks, 'name', d.metadata.name);
|
||||||
|
|
||||||
return provisioned && isCurrentNode && !isLonghornMounted;
|
return d?.isProvisioned && isCurrentNode && !isLonghornMounted;
|
||||||
})
|
})
|
||||||
.map((d) => {
|
.map((d) => {
|
||||||
const corrupted = d?.status?.deviceStatus?.fileSystem?.corrupted;
|
const corrupted = d?.status?.deviceStatus?.fileSystem?.corrupted;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isNew: true,
|
isNew: true,
|
||||||
name: d?.metadata?.name,
|
name: d?.metadata?.name,
|
||||||
originPath: d?.spec?.fileSystem?.mountPoint,
|
originPath: d?.spec?.fileSystem?.mountPoint,
|
||||||
path: d?.spec?.fileSystem?.mountPoint,
|
path: d?.spec?.fileSystem?.mountPoint,
|
||||||
blockDevice: d,
|
blockDevice: d,
|
||||||
displayName: d?.displayName,
|
displayName: d?.displayName,
|
||||||
forceFormatted: corrupted ? true : d?.spec?.fileSystem?.forceFormatted || false,
|
forceFormatted: corrupted ? true : d?.spec?.fileSystem?.forceFormatted || false,
|
||||||
|
provisioner: d?.spec?.provisioner?.lvm ? LVM_DRIVER : LONGHORN_DRIVER,
|
||||||
|
provisionerVersion: d?.spec?.provisioner?.longhorn?.engineVersion || LONGHORN_VERSION_V1,
|
||||||
|
lvmVolumeGroup: d?.spec?.provisioner?.lvm?.vgName,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -160,6 +167,13 @@ export default {
|
|||||||
return out;
|
return out;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
longhornSystemVersion() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const v2DataEngine = this.$store.getters[`${ inStore }/byId`](LONGHORN.SETTINGS, LONGHORN_V2_DATA_ENGINE) || {};
|
||||||
|
|
||||||
|
return v2DataEngine.value === 'true' ? LONGHORN_VERSION_V2 : LONGHORN_VERSION_V1;
|
||||||
|
},
|
||||||
|
|
||||||
longhornDisks() {
|
longhornDisks() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
const longhornNode = this.$store.getters[`${ inStore }/byId`](LONGHORN.NODES, `${ LONGHORN_SYSTEM }/${ this.value.id }`);
|
const longhornNode = this.$store.getters[`${ inStore }/byId`](LONGHORN.NODES, `${ LONGHORN_SYSTEM }/${ this.value.id }`);
|
||||||
@ -180,16 +194,19 @@ export default {
|
|||||||
return {
|
return {
|
||||||
...diskStatus[key],
|
...diskStatus[key],
|
||||||
...diskSpec?.[key],
|
...diskSpec?.[key],
|
||||||
name: key,
|
name: key,
|
||||||
isNew: false,
|
isNew: false,
|
||||||
storageReserved: formatSi(diskSpec[key]?.storageReserved, formatOptions),
|
storageReserved: formatSi(diskSpec[key]?.storageReserved, formatOptions),
|
||||||
storageAvailable: formatSi(diskStatus[key]?.storageAvailable, formatOptions),
|
storageAvailable: formatSi(diskStatus[key]?.storageAvailable, formatOptions),
|
||||||
storageMaximum: formatSi(diskStatus[key]?.storageMaximum, formatOptions),
|
storageMaximum: formatSi(diskStatus[key]?.storageMaximum, formatOptions),
|
||||||
storageScheduled: formatSi(diskStatus[key]?.storageScheduled, formatOptions),
|
storageScheduled: formatSi(diskStatus[key]?.storageScheduled, formatOptions),
|
||||||
blockDevice,
|
blockDevice,
|
||||||
displayName: blockDevice?.displayName || key,
|
displayName: blockDevice?.displayName || key,
|
||||||
forceFormatted: blockDevice?.spec?.fileSystem?.forceFormatted || false,
|
forceFormatted: blockDevice?.spec?.fileSystem?.forceFormatted || false,
|
||||||
tags: diskSpec?.[key]?.tags || [],
|
tags: diskSpec?.[key]?.tags || [],
|
||||||
|
provisioner: blockDevice?.spec?.provisioner?.lvm ? LVM_DRIVER : LONGHORN_DRIVER,
|
||||||
|
provisionerVersion: blockDevice?.spec?.provisioner?.longhorn?.engineVersion || LONGHORN_VERSION_V1,
|
||||||
|
lvmVolumeGroup: blockDevice?.spec?.provisioner?.lvm?.vgName,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -197,7 +214,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
showFormattedWarning() {
|
showFormattedWarning() {
|
||||||
const out = this.newDisks.filter(d => d.forceFormatted && d.isNew) || [];
|
const out = this.newDisks.filter(d => d.forceFormatted && d.isNew && d.provisionerVersion === LONGHORN_VERSION_V1) || [];
|
||||||
|
|
||||||
return out.length > 0;
|
return out.length > 0;
|
||||||
},
|
},
|
||||||
@ -316,15 +333,18 @@ export default {
|
|||||||
|
|
||||||
this.newDisks.push({
|
this.newDisks.push({
|
||||||
name,
|
name,
|
||||||
path: mountPoint,
|
path: mountPoint,
|
||||||
allowScheduling: false,
|
allowScheduling: false,
|
||||||
evictionRequested: false,
|
evictionRequested: false,
|
||||||
storageReserved: 0,
|
storageReserved: 0,
|
||||||
isNew: true,
|
isNew: true,
|
||||||
originPath: disk?.spec?.fileSystem?.mountPoint,
|
originPath: disk?.spec?.fileSystem?.mountPoint,
|
||||||
blockDevice: disk,
|
blockDevice: disk,
|
||||||
displayName: disk?.displayName,
|
displayName: disk?.displayName,
|
||||||
forceFormatted,
|
forceFormatted,
|
||||||
|
provisioner: LONGHORN_DRIVER,
|
||||||
|
provisionerVersion: LONGHORN_VERSION_V1,
|
||||||
|
lvmVolumeGroup: null,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -338,13 +358,10 @@ export default {
|
|||||||
} else if (addDisks.length !== 0 && removeDisks.length === 0) {
|
} else if (addDisks.length !== 0 && removeDisks.length === 0) {
|
||||||
const updatedDisks = addDisks.filter((d) => {
|
const updatedDisks = addDisks.filter((d) => {
|
||||||
const blockDevice = this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, `${ LONGHORN_SYSTEM }/${ d.name }`);
|
const blockDevice = this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, `${ LONGHORN_SYSTEM }/${ d.name }`);
|
||||||
const { provisioned, forceFormatted } = blockDevice.spec.fileSystem;
|
const { forceFormatted } = blockDevice.spec.fileSystem;
|
||||||
|
const { provisioner } = blockDevice.spec;
|
||||||
|
|
||||||
if (provisioned && forceFormatted === d.forceFormatted) {
|
return !(blockDevice.isProvisioned && forceFormatted === d.forceFormatted && isEqual(provisioner, d.provisioner));
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (updatedDisks.length === 0) {
|
if (updatedDisks.length === 0) {
|
||||||
@ -356,16 +373,25 @@ export default {
|
|||||||
await Promise.all(addDisks.map((d) => {
|
await Promise.all(addDisks.map((d) => {
|
||||||
const blockDevice = this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, `${ LONGHORN_SYSTEM }/${ d.name }`);
|
const blockDevice = this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, `${ LONGHORN_SYSTEM }/${ d.name }`);
|
||||||
|
|
||||||
blockDevice.spec.fileSystem.provisioned = true;
|
blockDevice.spec.provision = true;
|
||||||
blockDevice.spec.fileSystem.forceFormatted = d.forceFormatted;
|
blockDevice.spec.fileSystem.forceFormatted = d.forceFormatted;
|
||||||
|
|
||||||
|
switch (d.provisioner) {
|
||||||
|
case LONGHORN_DRIVER:
|
||||||
|
blockDevice.spec.provisioner = { longhorn: { engineVersion: d.provisionerVersion } };
|
||||||
|
break;
|
||||||
|
case LVM_DRIVER:
|
||||||
|
blockDevice.spec.provisioner = { lvm: { vgName: d.lvmVolumeGroup } };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return blockDevice.save();
|
return blockDevice.save();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await Promise.all(removeDisks.map((d) => {
|
await Promise.all(removeDisks.map((d) => {
|
||||||
const blockDevice = this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, `${ LONGHORN_SYSTEM }/${ d.name }`);
|
const blockDevice = this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, `${ LONGHORN_SYSTEM }/${ d.name }`);
|
||||||
|
|
||||||
blockDevice.spec.fileSystem.provisioned = false;
|
blockDevice.spec.provision = false;
|
||||||
|
|
||||||
return blockDevice.save();
|
return blockDevice.save();
|
||||||
}));
|
}));
|
||||||
@ -418,7 +444,7 @@ export default {
|
|||||||
if ((!findBy(this.disks || [], 'name', d.metadata.name) &&
|
if ((!findBy(this.disks || [], 'name', d.metadata.name) &&
|
||||||
d?.spec?.nodeName === this.value.id &&
|
d?.spec?.nodeName === this.value.id &&
|
||||||
(!addedToNodeCondition || addedToNodeCondition?.status === 'False') &&
|
(!addedToNodeCondition || addedToNodeCondition?.status === 'False') &&
|
||||||
!d.spec?.fileSystem?.provisioned &&
|
!d?.isProvisioned &&
|
||||||
!isAdded) ||
|
!isAdded) ||
|
||||||
isRemoved
|
isRemoved
|
||||||
) {
|
) {
|
||||||
@ -573,6 +599,7 @@ export default {
|
|||||||
class="mb-20"
|
class="mb-20"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:disks="disks"
|
:disks="disks"
|
||||||
|
:node="value"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #add>
|
<template #add>
|
||||||
|
|||||||
@ -13,13 +13,23 @@ import Loading from '@shell/components/Loading';
|
|||||||
|
|
||||||
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
||||||
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
|
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
|
||||||
import { STORAGE_CLASS, LONGHORN } from '@shell/config/types';
|
import { STORAGE_CLASS, LONGHORN, LONGHORN_DRIVER, SECRET, NAMESPACE } from '@shell/config/types';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { clone } from '@shell/utils/object';
|
import { clone } from '@shell/utils/object';
|
||||||
import { CSI_DRIVER } from '../../types';
|
import { CSI_DRIVER } from '../../types';
|
||||||
import Tags from '../../components/DiskTags';
|
import Tags from '../../components/DiskTags';
|
||||||
|
|
||||||
const LONGHORN_DRIVER = 'driver.longhorn.io';
|
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
||||||
|
|
||||||
|
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
||||||
|
|
||||||
|
export const DATA_ENGINE_V1 = 'v1';
|
||||||
|
export const DATA_ENGINE_V2 = 'v2';
|
||||||
|
|
||||||
|
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
|
||||||
|
|
||||||
|
const VOLUME_BINDING_MODE_IMMEDIATE = 'Immediate';
|
||||||
|
const VOLUME_BINDING_MODE_WAIT = 'WaitForFirstConsumer';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterStorage',
|
name: 'HarvesterStorage',
|
||||||
@ -66,47 +76,63 @@ export default {
|
|||||||
const volumeBindingModeOptions = [
|
const volumeBindingModeOptions = [
|
||||||
{
|
{
|
||||||
label: this.t('storageClass.customize.volumeBindingMode.now'),
|
label: this.t('storageClass.customize.volumeBindingMode.now'),
|
||||||
value: 'Immediate'
|
value: VOLUME_BINDING_MODE_IMMEDIATE
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.t('harvester.storage.customize.volumeBindingMode.later'),
|
label: this.t('harvester.storage.customize.volumeBindingMode.later'),
|
||||||
value: 'WaitForFirstConsumer'
|
value: VOLUME_BINDING_MODE_WAIT
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const allowedTopologies = clone(this.value.allowedTopologies?.[0]?.matchLabelExpressions || []);
|
const allowedTopologies = clone(this.value.allowedTopologies?.[0]?.matchLabelExpressions || []).filter(t => t.key !== LVM_TOPOLOGY_LABEL);
|
||||||
|
|
||||||
this.value['parameters'] = this.value.parameters || {};
|
this.value['parameters'] = this.value.parameters || {};
|
||||||
this.value['provisioner'] = this.value.provisioner || LONGHORN_DRIVER;
|
this.value['provisioner'] = this.value.provisioner || LONGHORN_DRIVER;
|
||||||
this.value['allowVolumeExpansion'] = this.value.allowVolumeExpansion || allowVolumeExpansionOptions[0].value;
|
this.value['allowVolumeExpansion'] = this.value.allowVolumeExpansion || allowVolumeExpansionOptions[0].value;
|
||||||
this.value['reclaimPolicy'] = this.value.reclaimPolicy || reclaimPolicyOptions[0].value;
|
this.value['reclaimPolicy'] = this.value.reclaimPolicy || reclaimPolicyOptions[0].value;
|
||||||
this.value['volumeBindingMode'] = this.value.volumeBindingMode || volumeBindingModeOptions[0].value;
|
|
||||||
|
if (this.value.provisioner === LONGHORN_DRIVER) {
|
||||||
|
this.value['parameters']['dataEngine'] = this.value.longhornVersion;
|
||||||
|
this.value['volumeBindingMode'] = this.value.volumeBindingMode || VOLUME_BINDING_MODE_IMMEDIATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.value.provisioner === LVM_DRIVER) {
|
||||||
|
this.value['volumeBindingMode'] = this.value.volumeBindingMode || VOLUME_BINDING_MODE_WAIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
let provisioner = `${ this.value.provisioner || LONGHORN_DRIVER }`;
|
||||||
|
|
||||||
|
if (provisioner === LONGHORN_DRIVER) {
|
||||||
|
provisioner = `${ provisioner }_${ this.value.longhornVersion }`;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
LVM_DRIVER,
|
||||||
reclaimPolicyOptions,
|
reclaimPolicyOptions,
|
||||||
allowVolumeExpansionOptions,
|
allowVolumeExpansionOptions,
|
||||||
volumeBindingModeOptions,
|
volumeBindingModeOptions,
|
||||||
mountOptions: [],
|
mountOptions: [],
|
||||||
provisioner: LONGHORN_DRIVER,
|
|
||||||
STORAGE_CLASS,
|
STORAGE_CLASS,
|
||||||
|
provisioner,
|
||||||
allowedTopologies,
|
allowedTopologies,
|
||||||
defaultAddValue: {
|
defaultAddValue: {
|
||||||
key: '',
|
key: '',
|
||||||
values: [],
|
values: [],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetch() {
|
async fetch() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
const hash = {
|
await allHash({
|
||||||
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
|
||||||
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
secrets: this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }),
|
||||||
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }),
|
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);
|
longhornV2DataEngine: this.$store.dispatch(`${ inStore }/find`, { type: LONGHORN.SETTINGS, id: LONGHORN_V2_DATA_ENGINE }),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
@ -120,20 +146,37 @@ export default {
|
|||||||
return this.isCreate ? _CREATE : _VIEW;
|
return this.isCreate ? _CREATE : _VIEW;
|
||||||
},
|
},
|
||||||
|
|
||||||
provisionerWatch() {
|
|
||||||
return this.value.provisioner;
|
|
||||||
},
|
|
||||||
|
|
||||||
provisioners() {
|
provisioners() {
|
||||||
const csiDrivers = this.$store.getters[`${ this.inStore }/all`](CSI_DRIVER) || [];
|
const out = [];
|
||||||
const format = { [LONGHORN_DRIVER]: 'storageClass.longhorn.title' };
|
|
||||||
|
|
||||||
return csiDrivers.map((provisioner) => {
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
return {
|
const csiDrivers = this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
|
||||||
label: format[provisioner.name] || provisioner.name,
|
|
||||||
value: provisioner.name,
|
csiDrivers.forEach(({ name }) => {
|
||||||
};
|
switch (name) {
|
||||||
|
case LONGHORN_DRIVER:
|
||||||
|
out.push({
|
||||||
|
label: `harvester.storage.storageClass.longhorn.${ DATA_ENGINE_V1 }.label`,
|
||||||
|
value: `${ name }_${ DATA_ENGINE_V1 }`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.longhornSystemVersion === DATA_ENGINE_V2 || this.value.longhornVersion === DATA_ENGINE_V2) {
|
||||||
|
out.push({
|
||||||
|
label: `harvester.storage.storageClass.longhorn.${ DATA_ENGINE_V2 }.label`,
|
||||||
|
value: `${ name }_${ DATA_ENGINE_V2 }`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case LVM_DRIVER:
|
||||||
|
out.push({
|
||||||
|
label: 'harvester.storage.storageClass.lvm.label',
|
||||||
|
value: name,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
},
|
},
|
||||||
|
|
||||||
schema() {
|
schema() {
|
||||||
@ -141,11 +184,41 @@ export default {
|
|||||||
|
|
||||||
return this.$store.getters[`${ inStore }/schemaFor`](STORAGE_CLASS);
|
return this.$store.getters[`${ inStore }/schemaFor`](STORAGE_CLASS);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
longhornSystemVersion() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const v2DataEngine = this.$store.getters[`${ inStore }/byId`](LONGHORN.SETTINGS, LONGHORN_V2_DATA_ENGINE) || {};
|
||||||
|
|
||||||
|
return v2DataEngine.value === 'true' ? DATA_ENGINE_V2 : DATA_ENGINE_V1;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
provisionerWatch() {
|
provisioner(neu) {
|
||||||
this.value['parameters'] = {};
|
const [provisioner, dataEngine] = neu?.split('_');
|
||||||
|
|
||||||
|
let parameters = {};
|
||||||
|
|
||||||
|
if (provisioner === LVM_DRIVER) {
|
||||||
|
const matchLabelExpressions = (this.value.allowedTopologies?.[0]?.matchLabelExpressions || []).filter(t => t.key !== LVM_TOPOLOGY_LABEL);
|
||||||
|
|
||||||
|
if (matchLabelExpressions.length > 0) {
|
||||||
|
this.value['allowedTopologies'] = [{ matchLabelExpressions }];
|
||||||
|
} else {
|
||||||
|
delete this.value.allowedTopologies;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value['volumeBindingMode'] = VOLUME_BINDING_MODE_WAIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provisioner === LONGHORN_DRIVER) {
|
||||||
|
parameters = { dataEngine };
|
||||||
|
this.value['volumeBindingMode'] = VOLUME_BINDING_MODE_IMMEDIATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value['provisioner'] = provisioner;
|
||||||
|
this.value['allowVolumeExpansion'] = this.value.provisioner === LONGHORN_DRIVER;
|
||||||
|
this.value['parameters'] = parameters;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -162,11 +235,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateProvisioner(provisioner) {
|
|
||||||
this.value['provisioner'] = provisioner;
|
|
||||||
this.value['allowVolumeExpansion'] = provisioner === LONGHORN_DRIVER;
|
|
||||||
},
|
|
||||||
|
|
||||||
willSave() {
|
willSave() {
|
||||||
Object.keys(this.value.parameters).forEach((key) => {
|
Object.keys(this.value.parameters).forEach((key) => {
|
||||||
if (this.value.parameters[key] === null || this.value.parameters[key] === '') {
|
if (this.value.parameters[key] === null || this.value.parameters[key] === '') {
|
||||||
@ -178,10 +246,15 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
formatAllowedTopoloties() {
|
formatAllowedTopoloties() {
|
||||||
const neu = this.allowedTopologies;
|
const neu = this.allowedTopologies.filter(t => t.key !== LVM_TOPOLOGY_LABEL);
|
||||||
|
const lvmMatchExpression = (this.value.allowedTopologies?.[0]?.matchLabelExpressions || []).filter(t => t.key === LVM_TOPOLOGY_LABEL);
|
||||||
|
|
||||||
if (!neu || neu.length === 0) {
|
if (!neu || neu.length === 0) {
|
||||||
delete this.value.allowedTopologies;
|
if (lvmMatchExpression.length > 0) {
|
||||||
|
this.value.allowedTopologies = [{ matchLabelExpressions: lvmMatchExpression }];
|
||||||
|
} else {
|
||||||
|
delete this.value.allowedTopologies;
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -189,7 +262,7 @@ export default {
|
|||||||
const matchLabelExpressions = neu.filter(R => !!R.key.trim() && (R.values.length > 0 && !R.values.find(V => !V.trim())));
|
const matchLabelExpressions = neu.filter(R => !!R.key.trim() && (R.values.length > 0 && !R.values.find(V => !V.trim())));
|
||||||
|
|
||||||
if (matchLabelExpressions.length > 0) {
|
if (matchLabelExpressions.length > 0) {
|
||||||
this.value.allowedTopologies = [{ matchLabelExpressions }];
|
this.value.allowedTopologies = [{ matchLabelExpressions: [...matchLabelExpressions, ...lvmMatchExpression] }];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -219,7 +292,7 @@ export default {
|
|||||||
:register-before-hook="registerBeforeHook"
|
:register-before-hook="registerBeforeHook"
|
||||||
/>
|
/>
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
:value="value.provisioner"
|
v-model:value="provisioner"
|
||||||
label="Provisioner"
|
label="Provisioner"
|
||||||
:options="provisioners"
|
:options="provisioners"
|
||||||
:localized-label="true"
|
:localized-label="true"
|
||||||
@ -227,13 +300,12 @@ export default {
|
|||||||
:searchable="true"
|
:searchable="true"
|
||||||
:taggable="true"
|
:taggable="true"
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
@update:value="updateProvisioner($event)"
|
|
||||||
/>
|
/>
|
||||||
<Tabbed :side-tabs="true">
|
<Tabbed :side-tabs="true">
|
||||||
<Tab name="parameters" :label="t('storageClass.parameters.label')" :weight="2">
|
<Tab name="parameters" :label="t('storageClass.parameters.label')" :weight="2">
|
||||||
<component
|
<component
|
||||||
:is="getComponent(value.provisioner)"
|
:is="getComponent(provisioner)"
|
||||||
:key="value.provisioner"
|
:key="provisioner"
|
||||||
:value="value"
|
:value="value"
|
||||||
:mode="modeOverride"
|
:mode="modeOverride"
|
||||||
:real-mode="realMode"
|
:real-mode="realMode"
|
||||||
@ -268,6 +340,7 @@ export default {
|
|||||||
:label="t('storageClass.customize.volumeBindingMode.label')"
|
:label="t('storageClass.customize.volumeBindingMode.label')"
|
||||||
:mode="modeOverride"
|
:mode="modeOverride"
|
||||||
:options="volumeBindingModeOptions"
|
:options="volumeBindingModeOptions"
|
||||||
|
:disabled="provisioner === LVM_DRIVER"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,11 +3,12 @@ import KeyValue from '@shell/components/form/KeyValue';
|
|||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import RadioGroup from '@components/Form/Radio/RadioGroup';
|
import RadioGroup from '@components/Form/Radio/RadioGroup';
|
||||||
import { SECRET, NAMESPACE, LONGHORN } from '@shell/config/types';
|
import { SECRET, LONGHORN } from '@shell/config/types';
|
||||||
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
||||||
import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map';
|
import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map';
|
||||||
import { clone } from '@shell/utils/object';
|
import { clone } from '@shell/utils/object';
|
||||||
import { uniq } from '@shell/utils/array';
|
import { uniq } from '@shell/utils/array';
|
||||||
|
import { DATA_ENGINE_V1 } from '../index.vue';
|
||||||
|
|
||||||
// UI components for Longhorn storage class parameters
|
// UI components for Longhorn storage class parameters
|
||||||
const DEFAULT_PARAMETERS = [
|
const DEFAULT_PARAMETERS = [
|
||||||
@ -17,6 +18,7 @@ const DEFAULT_PARAMETERS = [
|
|||||||
'nodeSelector',
|
'nodeSelector',
|
||||||
'migratable',
|
'migratable',
|
||||||
'encrypted',
|
'encrypted',
|
||||||
|
'dataEngine',
|
||||||
];
|
];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -29,7 +31,7 @@ const {
|
|||||||
} = CSI_SECRETS;
|
} = CSI_SECRETS;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DriverLonghornIO',
|
name: 'DriverLonghornIOV1',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
KeyValue,
|
KeyValue,
|
||||||
@ -53,16 +55,6 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
await this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE });
|
|
||||||
|
|
||||||
const allSecrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
|
|
||||||
|
|
||||||
// only show non-system secret to user to select
|
|
||||||
this.secrets = allSecrets.filter(secret => secret.isSystem === false);
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
if (this.realMode === _CREATE) {
|
if (this.realMode === _CREATE) {
|
||||||
this.value['parameters'] = {
|
this.value['parameters'] = {
|
||||||
@ -72,12 +64,23 @@ export default {
|
|||||||
nodeSelector: null,
|
nodeSelector: null,
|
||||||
encrypted: 'false',
|
encrypted: 'false',
|
||||||
migratable: 'true',
|
migratable: 'true',
|
||||||
|
dataEngine: DATA_ENGINE_V1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { secrets: [] };
|
return { };
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
secrets() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const allSecrets = this.$store.getters[`${ inStore }/all`](SECRET);
|
||||||
|
|
||||||
|
// only show non-system secret to user to select
|
||||||
|
return allSecrets.filter(secret => secret.isSystem === false);
|
||||||
|
},
|
||||||
|
|
||||||
longhornNodes() {
|
longhornNodes() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
@ -0,0 +1,352 @@
|
|||||||
|
<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 { SECRET, LONGHORN } from '@shell/config/types';
|
||||||
|
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
||||||
|
import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map';
|
||||||
|
import { clone } from '@shell/utils/object';
|
||||||
|
import { uniq } from '@shell/utils/array';
|
||||||
|
import { DATA_ENGINE_V2 } from '../index.vue';
|
||||||
|
|
||||||
|
// UI components for Longhorn storage class parameters
|
||||||
|
const DEFAULT_PARAMETERS = [
|
||||||
|
'numberOfReplicas',
|
||||||
|
'staleReplicaTimeout',
|
||||||
|
'diskSelector',
|
||||||
|
'nodeSelector',
|
||||||
|
'migratable',
|
||||||
|
'encrypted',
|
||||||
|
'dataEngine',
|
||||||
|
];
|
||||||
|
|
||||||
|
const {
|
||||||
|
CSI_PROVISIONER_SECRET_NAME,
|
||||||
|
CSI_PROVISIONER_SECRET_NAMESPACE,
|
||||||
|
CSI_NODE_PUBLISH_SECRET_NAME,
|
||||||
|
CSI_NODE_PUBLISH_SECRET_NAMESPACE,
|
||||||
|
CSI_NODE_STAGE_SECRET_NAME,
|
||||||
|
CSI_NODE_STAGE_SECRET_NAMESPACE
|
||||||
|
} = CSI_SECRETS;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DriverLonghornIOV2',
|
||||||
|
|
||||||
|
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.value['parameters'] = {
|
||||||
|
numberOfReplicas: '3',
|
||||||
|
staleReplicaTimeout: '30',
|
||||||
|
diskSelector: null,
|
||||||
|
nodeSelector: null,
|
||||||
|
encrypted: 'false',
|
||||||
|
migratable: 'false',
|
||||||
|
dataEngine: DATA_ENGINE_V2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
secrets() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const allSecrets = this.$store.getters[`${ inStore }/all`](SECRET);
|
||||||
|
|
||||||
|
// only show non-system secret to user to select
|
||||||
|
return allSecrets.filter(secret => secret.isSystem === false);
|
||||||
|
},
|
||||||
|
|
||||||
|
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'
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
|
||||||
|
secretOptions() {
|
||||||
|
return this.secrets.map(secret => secret.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
volumeEncryptionOptions() {
|
||||||
|
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.forEach((key) => {
|
||||||
|
delete parameters[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.values(CSI_SECRETS).forEach((key) => {
|
||||||
|
delete parameters[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
return parameters;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
Object.assign(this.value.parameters, value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
volumeEncryption: {
|
||||||
|
set(neu) {
|
||||||
|
this.value['parameters'] = {
|
||||||
|
...this.value.parameters,
|
||||||
|
encrypted: neu
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
get() {
|
||||||
|
return this.value?.parameters?.encrypted || 'false';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
secret: {
|
||||||
|
get() {
|
||||||
|
const selectedNs = this.value.parameters[CSI_PROVISIONER_SECRET_NAMESPACE];
|
||||||
|
const selectedName = this.value.parameters[CSI_PROVISIONER_SECRET_NAME];
|
||||||
|
|
||||||
|
if (selectedNs && selectedName) {
|
||||||
|
return `${ selectedNs }/${ selectedName }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
|
||||||
|
set(selectedSecret) {
|
||||||
|
const [namespace, name] = selectedSecret.split('/');
|
||||||
|
|
||||||
|
this.value['parameters'] = {
|
||||||
|
...this.value.parameters,
|
||||||
|
[CSI_PROVISIONER_SECRET_NAME]: name,
|
||||||
|
[CSI_NODE_PUBLISH_SECRET_NAME]: name,
|
||||||
|
[CSI_NODE_STAGE_SECRET_NAME]: name,
|
||||||
|
[CSI_PROVISIONER_SECRET_NAMESPACE]: namespace,
|
||||||
|
[CSI_NODE_PUBLISH_SECRET_NAMESPACE]: namespace,
|
||||||
|
[CSI_NODE_STAGE_SECRET_NAMESPACE]: namespace
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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:value="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="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:value="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:value="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-20">
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="value.parameters.migratable"
|
||||||
|
name="layer3NetworkMode"
|
||||||
|
:label="t('harvester.storage.parameters.migratable.label')"
|
||||||
|
:mode="mode"
|
||||||
|
:options="migratableOptions"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-20">
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="volumeEncryption"
|
||||||
|
name="volumeEncryption"
|
||||||
|
:label="t('harvester.storage.volumeEncryption')"
|
||||||
|
:mode="mode"
|
||||||
|
:options="volumeEncryptionOptions"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="value.parameters.encrypted === 'true'" class="row mt-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="secret"
|
||||||
|
:label="t('harvester.storage.secret')"
|
||||||
|
:options="secretOptions"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<KeyValue
|
||||||
|
v-model:value="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>
|
||||||
@ -0,0 +1,169 @@
|
|||||||
|
<script>
|
||||||
|
|
||||||
|
import KeyValue from '@shell/components/form/KeyValue';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { clone } from '@shell/utils/object';
|
||||||
|
import { HCI } from '../../../types';
|
||||||
|
import { NODE } from '@shell/config/types';
|
||||||
|
import { LVM_TOPOLOGY_LABEL } from '../index.vue';
|
||||||
|
|
||||||
|
const DEFAULT_PARAMETERS = [
|
||||||
|
'type',
|
||||||
|
'vgName'
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEFAULT_TOPOLOGIES = [{
|
||||||
|
matchLabelExpressions: [{
|
||||||
|
key: LVM_TOPOLOGY_LABEL,
|
||||||
|
values: []
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
KeyValue,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
realMode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await allHash({
|
||||||
|
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
|
||||||
|
lvmVolumeGroups: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.LVM_VOLUME_GROUP }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const node = (this.value.allowedTopologies?.[0]?.matchLabelExpressions || []).find(t => t.key === LVM_TOPOLOGY_LABEL)?.values[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
volumeGroupTypes: ['striped', 'dm-thin'],
|
||||||
|
node,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
node(value) {
|
||||||
|
delete (this.value.parameters.vgName);
|
||||||
|
|
||||||
|
const allowedTopologies = [...DEFAULT_TOPOLOGIES];
|
||||||
|
|
||||||
|
allowedTopologies[0].matchLabelExpressions[0].values = [value];
|
||||||
|
|
||||||
|
this.value.allowedTopologies = allowedTopologies;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
nodes() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const nodes = this.$store.getters[`${ inStore }/all`](NODE) || [];
|
||||||
|
|
||||||
|
return nodes.filter(n => n.labels[LVM_TOPOLOGY_LABEL] === n.name).map(n => n.name);
|
||||||
|
},
|
||||||
|
|
||||||
|
volumeGroups() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const lvmVolumeGroups = this.$store.getters[`${ inStore }/all`](HCI.LVM_VOLUME_GROUP) || [];
|
||||||
|
|
||||||
|
return lvmVolumeGroups
|
||||||
|
.filter(group => group.spec.nodeName === this.node)
|
||||||
|
.map(g => g.spec.vgName);
|
||||||
|
},
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="node"
|
||||||
|
:label="t('harvester.storage.parameters.node.label')"
|
||||||
|
:options="nodes"
|
||||||
|
:mode="mode"
|
||||||
|
:required="true"
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="value.parameters.vgName"
|
||||||
|
:label="t('harvester.storage.parameters.lvmVolumeGroup.label')"
|
||||||
|
:options="volumeGroups"
|
||||||
|
:mode="mode"
|
||||||
|
:required="true"
|
||||||
|
>
|
||||||
|
<template #no-options="{ searching }">
|
||||||
|
<span v-if="!searching" class="text-muted">
|
||||||
|
{{ t('harvester.storage.parameters.lvmVolumeGroup.no-options', null, true) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</LabeledSelect>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="value.parameters.type"
|
||||||
|
:label="t('harvester.storage.parameters.lvmVolumeGroupType.label')"
|
||||||
|
:options="volumeGroupTypes"
|
||||||
|
:mode="mode"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<KeyValue
|
||||||
|
v-model:value="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>
|
||||||
@ -16,6 +16,7 @@ import { STORAGE_CLASS } from '@shell/config/types';
|
|||||||
import { VM_IMAGE_FILE_FORMAT } from '../validators/vm-image';
|
import { VM_IMAGE_FILE_FORMAT } from '../validators/vm-image';
|
||||||
import { OS } from '../mixins/harvester-vm';
|
import { OS } from '../mixins/harvester-vm';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
|
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
||||||
|
|
||||||
const ENCRYPT = 'encrypt';
|
const ENCRYPT = 'encrypt';
|
||||||
const DECRYPT = 'decrypt';
|
const DECRYPT = 'decrypt';
|
||||||
@ -138,16 +139,16 @@ export default {
|
|||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
||||||
|
|
||||||
const out = storages.filter(s => !s.parameters?.backingImage).map((s) => {
|
return storages
|
||||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
.filter(s => !s.parameters?.backingImage && s.provisioner !== LVM_DRIVER) // Lvm storage is not supported.
|
||||||
|
.map((s) => {
|
||||||
|
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
value: s.name,
|
value: s.name,
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
return out;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
storageClassName: {
|
storageClassName: {
|
||||||
|
|||||||
@ -267,7 +267,7 @@ export default {
|
|||||||
:label="t('workload.container.titles.nodeScheduling')"
|
:label="t('workload.container.titles.nodeScheduling')"
|
||||||
:weight="-3"
|
:weight="-3"
|
||||||
>
|
>
|
||||||
<template #default="{active}">
|
<template #default>
|
||||||
<NodeScheduling
|
<NodeScheduling
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:value="spec.template.spec"
|
:value="spec.template.spec"
|
||||||
@ -277,7 +277,7 @@ export default {
|
|||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab :label="t('harvester.tab.vmScheduling')" name="vmScheduling" :weight="-4">
|
<Tab :label="t('harvester.tab.vmScheduling')" name="vmScheduling" :weight="-4">
|
||||||
<template #default="{active}">
|
<template #default>
|
||||||
<PodAffinity
|
<PodAffinity
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:value="spec.template.spec"
|
:value="spec.template.spec"
|
||||||
|
|||||||
@ -7,18 +7,20 @@ import ResourceTabs from '@shell/components/form/ResourceTabs';
|
|||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { get } from '@shell/utils/object';
|
import { get } from '@shell/utils/object';
|
||||||
import { STORAGE_CLASS, LONGHORN, PV } from '@shell/config/types';
|
import { STORAGE_CLASS, LONGHORN, PV } from '@shell/config/types';
|
||||||
import { sortBy } from '@shell/utils/sort';
|
import { sortBy } from '@shell/utils/sort';
|
||||||
import { saferDump } from '@shell/utils/create-yaml';
|
import { saferDump } from '@shell/utils/create-yaml';
|
||||||
import { _CREATE } from '@shell/config/query-params';
|
import { _CREATE, _EDIT } from '@shell/config/query-params';
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import { STATE, NAME, AGE, NAMESPACE } from '@shell/config/table-headers';
|
import { STATE, NAME, AGE, NAMESPACE } from '@shell/config/table-headers';
|
||||||
import { InterfaceOption, VOLUME_DATA_SOURCE_KIND } from '../config/harvester-map';
|
import { InterfaceOption, VOLUME_DATA_SOURCE_KIND } from '../config/harvester-map';
|
||||||
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
||||||
|
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
||||||
|
import { DATA_ENGINE_V2 } from './harvesterhci.io.storage/index.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterVolume',
|
name: 'HarvesterVolume',
|
||||||
@ -26,6 +28,7 @@ export default {
|
|||||||
emits: ['update:value'],
|
emits: ['update:value'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
|
Banner,
|
||||||
Tab,
|
Tab,
|
||||||
UnitInput,
|
UnitInput,
|
||||||
CruResource,
|
CruResource,
|
||||||
@ -95,6 +98,10 @@ export default {
|
|||||||
return this.source === 'blank';
|
return this.source === 'blank';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isEdit() {
|
||||||
|
return this.mode === _EDIT;
|
||||||
|
},
|
||||||
|
|
||||||
isVMImage() {
|
isVMImage() {
|
||||||
return this.source === 'url';
|
return this.source === 'url';
|
||||||
},
|
},
|
||||||
@ -160,11 +167,14 @@ export default {
|
|||||||
return VOLUME_DATA_SOURCE_KIND[this.value.spec?.dataSource?.kind];
|
return VOLUME_DATA_SOURCE_KIND[this.value.spec?.dataSource?.kind];
|
||||||
},
|
},
|
||||||
|
|
||||||
storageClassOptions() {
|
storageClasses() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
|
||||||
|
|
||||||
const out = storages.filter(s => !s.parameters?.backingImage).map((s) => {
|
return this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
||||||
|
},
|
||||||
|
|
||||||
|
storageClassOptions() {
|
||||||
|
return this.storageClasses.filter(s => !s.parameters?.backingImage).map((s) => {
|
||||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -172,8 +182,6 @@ export default {
|
|||||||
value: s.name,
|
value: s.name,
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
return out;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
frontend() {
|
frontend() {
|
||||||
@ -220,6 +228,10 @@ export default {
|
|||||||
|
|
||||||
rebuildStatus() {
|
rebuildStatus() {
|
||||||
return this.value.longhornEngine?.status?.rebuildStatus;
|
return this.value.longhornEngine?.status?.rebuildStatus;
|
||||||
|
},
|
||||||
|
|
||||||
|
isLonghornV2() {
|
||||||
|
return this.value.storageClass?.isLonghornV2;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -231,6 +243,10 @@ export default {
|
|||||||
let imageAnnotations = '';
|
let imageAnnotations = '';
|
||||||
let storageClassName = this.value.spec.storageClassName;
|
let storageClassName = this.value.spec.storageClassName;
|
||||||
|
|
||||||
|
const storageClass = this.storageClasses.find(sc => sc.name === storageClassName);
|
||||||
|
const storageClassProvisioner = storageClass?.provisioner;
|
||||||
|
const storageClassDataEngine = storageClass?.parameters?.dataEngine;
|
||||||
|
|
||||||
if (this.isVMImage && this.imageId) {
|
if (this.isVMImage && this.imageId) {
|
||||||
const images = this.$store.getters['harvester/all'](HCI.IMAGE);
|
const images = this.$store.getters['harvester/all'](HCI.IMAGE);
|
||||||
|
|
||||||
@ -245,8 +261,9 @@ export default {
|
|||||||
|
|
||||||
const spec = {
|
const spec = {
|
||||||
...this.value.spec,
|
...this.value.spec,
|
||||||
resources: { requests: { storage: this.storage } },
|
resources: { requests: { storage: this.storage } },
|
||||||
storageClassName
|
storageClassName,
|
||||||
|
accessModes: storageClassProvisioner === LVM_DRIVER || storageClassDataEngine === DATA_ENGINE_V2 ? ['ReadWriteOnce'] : ['ReadWriteMany'],
|
||||||
};
|
};
|
||||||
|
|
||||||
this.value.setAnnotations(imageAnnotations);
|
this.value.setAnnotations(imageAnnotations);
|
||||||
@ -335,10 +352,15 @@ export default {
|
|||||||
:output-modifier="true"
|
:output-modifier="true"
|
||||||
:increment="1024"
|
:increment="1024"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
:disabled="isLonghornV2 && isEdit"
|
||||||
required
|
required
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Banner v-if="isLonghornV2 && isEdit" color="warning">
|
||||||
|
<span>{{ t('harvester.volume.longhorn.disableResize') }}</span>
|
||||||
|
</Banner>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab v-if="!isCreate" name="details" :label="t('harvester.volume.tabs.details')" :weight="2.5" class="bordered-table">
|
<Tab v-if="!isCreate" name="details" :label="t('harvester.volume.tabs.details')" :weight="2.5" class="bordered-table">
|
||||||
<LabeledInput v-model:value="frontendDisplay" class="mb-20" :mode="mode" :disabled="true" :label="t('harvester.volume.frontend')" />
|
<LabeledInput v-model:value="frontendDisplay" class="mb-20" :mode="mode" :disabled="true" :label="t('harvester.volume.frontend')" />
|
||||||
|
|||||||
@ -91,7 +91,7 @@ export default {
|
|||||||
{{ deviceCRD.metadata.name }}
|
{{ deviceCRD.metadata.name }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="(nodeName, i) in allNodeNames" :key="i" class="compat-cell"
|
v-for="(nodeName, k) in allNodeNames" :key="k" class="compat-cell"
|
||||||
:class="{'has-device': nodeHasDevice(nodeName, deviceCRD)}"
|
:class="{'has-device': nodeHasDevice(nodeName, deviceCRD)}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,10 +8,10 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
|||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import ModalWithCard from '@shell/components/ModalWithCard';
|
import ModalWithCard from '@shell/components/ModalWithCard';
|
||||||
|
|
||||||
import { PVC, STORAGE_CLASS } from '@shell/config/types';
|
import { PVC } from '@shell/config/types';
|
||||||
import { clone } from '@shell/utils/object';
|
import { clone } from '@shell/utils/object';
|
||||||
|
import { ucFirst, randomStr } from '@shell/utils/string';
|
||||||
import { removeObject } from '@shell/utils/array';
|
import { removeObject } from '@shell/utils/array';
|
||||||
import { randomStr } from '@shell/utils/string';
|
|
||||||
import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
|
import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
|
||||||
import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
|
import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
|
||||||
import { SOURCE_TYPE } from '../../../config/harvester-map';
|
import { SOURCE_TYPE } from '../../../config/harvester-map';
|
||||||
@ -82,11 +82,12 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
ucFirst,
|
||||||
SOURCE_TYPE,
|
SOURCE_TYPE,
|
||||||
rows: clone(this.value),
|
rows: clone(this.value),
|
||||||
nameIdx: 1,
|
nameIdx: 1,
|
||||||
vol: null,
|
vol: null,
|
||||||
isOpen: false,
|
isOpen: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -175,10 +176,7 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (type === SOURCE_TYPE.NEW) {
|
if (type === SOURCE_TYPE.NEW) {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
neu.storageClassName = this.defaultStorageClass?.metadata?.name || 'longhorn';
|
||||||
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find( O => O.isDefault);
|
|
||||||
|
|
||||||
neu.storageClassName = defaultStorage?.metadata?.name || 'longhorn';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.rows.push(neu);
|
this.rows.push(neu);
|
||||||
@ -258,6 +256,10 @@ export default {
|
|||||||
|
|
||||||
getImageDisplayName(id) {
|
getImageDisplayName(id) {
|
||||||
return this.$store.getters['harvester/all'](HCI.IMAGE).find(image => image.id === id)?.spec?.displayName;
|
return this.$store.getters['harvester/all'](HCI.IMAGE).find(image => image.id === id)?.spec?.displayName;
|
||||||
|
},
|
||||||
|
|
||||||
|
isLonghornV2(volume) {
|
||||||
|
return volume?.pvc?.storageClass?.isLonghornV2;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -371,12 +373,24 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Banner
|
<div class="mt-15">
|
||||||
v-if="volume.volumeStatus && !isCreate"
|
<Banner
|
||||||
class="mt-15 volume-status"
|
v-if="volume.volumeStatus && !isCreate"
|
||||||
color="warning"
|
class="volume-status"
|
||||||
:label="volume.volumeStatus"
|
color="warning"
|
||||||
/>
|
:label="ucFirst(volume.volumeStatus)"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-if="value.volumeBackups && value.volumeBackups.error && value.volumeBackups.error.message"
|
||||||
|
color="error"
|
||||||
|
:label="ucFirst(value.volumeBackups.error.message)"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-if="isLonghornV2(volume) && !isView"
|
||||||
|
color="warning"
|
||||||
|
:label="t('harvester.volume.longhorn.disableResize')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -497,4 +511,8 @@ export default {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -114,6 +114,10 @@ export default {
|
|||||||
return allPVCs.find((P) => {
|
return allPVCs.find((P) => {
|
||||||
return this.namespace ? P.id === `${ this.namespace }/${ this.value.volumeName }` : true;
|
return this.namespace ? P.id === `${ this.namespace }/${ this.value.volumeName }` : true;
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
isLonghornV2() {
|
||||||
|
return this.value.pvc?.storageClass?.isLonghornV2;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -279,6 +283,7 @@ export default {
|
|||||||
:label="t('harvester.fields.size')"
|
:label="t('harvester.fields.size')"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:required="validateRequired"
|
:required="validateRequired"
|
||||||
|
:disable="isLonghornV2"
|
||||||
suffix="GiB"
|
suffix="GiB"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -4,13 +4,14 @@ import UnitInput from '@shell/components/form/UnitInput';
|
|||||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import { PVC, STORAGE_CLASS } from '@shell/config/types';
|
import { PVC, STORAGE_CLASS } from '@shell/config/types';
|
||||||
import { formatSi, parseSi } from '@shell/utils/units';
|
import { formatSi, parseSi } from '@shell/utils/units';
|
||||||
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
||||||
import { _VIEW } from '@shell/config/query-params';
|
import { _VIEW } from '@shell/config/query-params';
|
||||||
import LabelValue from '@shell/components/LabelValue';
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
import { ucFirst } from '@shell/utils/string';
|
import { ucFirst } from '@shell/utils/string';
|
||||||
|
import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass';
|
||||||
|
import { DATA_ENGINE_V2 } from '../../../../edit/harvesterhci.io.storage/index.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditVolume',
|
name: 'HarvesterEditVolume',
|
||||||
@ -18,7 +19,7 @@ export default {
|
|||||||
emits: ['update'],
|
emits: ['update'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
InputOrDisplay, Loading, LabeledInput, LabeledSelect, UnitInput, LabelValue, Banner
|
InputOrDisplay, Loading, LabeledInput, LabeledSelect, UnitInput, LabelValue
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@ -88,10 +89,12 @@ export default {
|
|||||||
return !this.value.newCreateId && this.isEdit && this.isVirtualType;
|
return !this.value.newCreateId && this.isEdit && this.isVirtualType;
|
||||||
},
|
},
|
||||||
|
|
||||||
storageClassOptions() {
|
storageClasses() {
|
||||||
const storages = this.$store.getters[`harvester/all`](STORAGE_CLASS) || [];
|
return this.$store.getters[`harvester/all`](STORAGE_CLASS) || [];
|
||||||
|
},
|
||||||
|
|
||||||
const out = storages.filter(s => !s.parameters?.backingImage).map((s) => {
|
storageClassOptions() {
|
||||||
|
return this.storageClasses.filter(s => !s.parameters?.backingImage).map((s) => {
|
||||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -99,12 +102,25 @@ export default {
|
|||||||
value: s.name,
|
value: s.name,
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
return out;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isLonghornV2() {
|
||||||
|
return this.value.pvc?.storageClass?.isLonghornV2;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
'value.storageClassName': {
|
||||||
|
immediate: true,
|
||||||
|
handler(neu) {
|
||||||
|
const storageClass = this.storageClasses.find(sc => sc.name === neu);
|
||||||
|
const provisioner = storageClass?.provisioner;
|
||||||
|
const engine = storageClass?.parameters?.dataEngine;
|
||||||
|
|
||||||
|
this.value.accessMode = provisioner === LVM_DRIVER || engine === DATA_ENGINE_V2 ? 'ReadWriteOnce' : 'ReadWriteMany';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
'value.type'(neu) {
|
'value.type'(neu) {
|
||||||
if (neu === 'cd-rom') {
|
if (neu === 'cd-rom') {
|
||||||
this.value['bus'] = 'sata';
|
this.value['bus'] = 'sata';
|
||||||
@ -222,6 +238,7 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
:required="validateRequired"
|
:required="validateRequired"
|
||||||
:label="t('harvester.fields.size')"
|
:label="t('harvester.fields.size')"
|
||||||
|
:disabled="isLonghornV2"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
</InputOrDisplay>
|
</InputOrDisplay>
|
||||||
@ -261,11 +278,5 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -614,7 +614,7 @@ export default {
|
|||||||
:label="t('workload.container.titles.nodeScheduling')"
|
:label="t('workload.container.titles.nodeScheduling')"
|
||||||
:weight="-3"
|
:weight="-3"
|
||||||
>
|
>
|
||||||
<template #default="{active}">
|
<template #default>
|
||||||
<NodeScheduling
|
<NodeScheduling
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:value="spec.template.spec"
|
:value="spec.template.spec"
|
||||||
@ -628,7 +628,7 @@ export default {
|
|||||||
name="vmScheduling"
|
name="vmScheduling"
|
||||||
:weight="-4"
|
:weight="-4"
|
||||||
>
|
>
|
||||||
<template #default="{active}">
|
<template #default>
|
||||||
<PodAffinity
|
<PodAffinity
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:value="spec.template.spec"
|
:value="spec.template.spec"
|
||||||
|
|||||||
@ -145,7 +145,7 @@ harvester:
|
|||||||
name: 'New Volume Name'
|
name: 'New Volume Name'
|
||||||
success: 'New Volume { name } restored successfully.'
|
success: 'New Volume { name } restored successfully.'
|
||||||
vmSnapshot:
|
vmSnapshot:
|
||||||
title: Take VM Snapshot
|
title: Take Virtual Machine Snapshot
|
||||||
name: Name
|
name: Name
|
||||||
success: 'Take virtual machine Snapshot { name } successfully.'
|
success: 'Take virtual machine Snapshot { name } successfully.'
|
||||||
restart:
|
restart:
|
||||||
@ -171,7 +171,7 @@ harvester:
|
|||||||
encryptImage: Encrypt Image
|
encryptImage: Encrypt Image
|
||||||
decryptImage: Decrypt Image
|
decryptImage: Decrypt Image
|
||||||
ejectCDROM: Eject CD-ROM
|
ejectCDROM: Eject CD-ROM
|
||||||
editVMQuota: Edit VM Quota
|
editVMQuota: Edit Virtual Machine Quota
|
||||||
launchFormTemplate: Launch instance from template
|
launchFormTemplate: Launch instance from template
|
||||||
modifyTemplate: Modify template (Create new version)
|
modifyTemplate: Modify template (Create new version)
|
||||||
setDefaultVersion: Set default version
|
setDefaultVersion: Set default version
|
||||||
@ -236,7 +236,7 @@ harvester:
|
|||||||
retain: Retain
|
retain: Retain
|
||||||
scheduleType: Type
|
scheduleType: Type
|
||||||
maxFailure: Max Failure
|
maxFailure: Max Failure
|
||||||
sourceVm: Source VM
|
sourceVm: Source Virtual Machine
|
||||||
vmSchedule: Virtual Machine Schedule
|
vmSchedule: Virtual Machine Schedule
|
||||||
hostIp: Host IP
|
hostIp: Host IP
|
||||||
vm:
|
vm:
|
||||||
@ -319,7 +319,7 @@ harvester:
|
|||||||
goSetting:
|
goSetting:
|
||||||
prefix: The pcidevices-controller add-on is not enabled, click
|
prefix: The pcidevices-controller add-on is not enabled, click
|
||||||
middle: here
|
middle: here
|
||||||
suffix: to enable it to manage your PCI devices.
|
suffix: to enable the add-on to successfully manage your PCI devices.
|
||||||
noPCIPermission: Please contact your system administrator to enable the PCI devices first.
|
noPCIPermission: Please contact your system administrator to enable the PCI devices first.
|
||||||
enablePassthroughWarning: Please be careful not to use host-owned PCI devices (e.g., management and VLAN NICs). Incorrect device allocation may cause damage to your cluster, including node failure.
|
enablePassthroughWarning: Please be careful not to use host-owned PCI devices (e.g., management and VLAN NICs). Incorrect device allocation may cause damage to your cluster, including node failure.
|
||||||
|
|
||||||
@ -508,6 +508,18 @@ harvester:
|
|||||||
label: Storage Scheduled
|
label: Storage Scheduled
|
||||||
storageMaximum:
|
storageMaximum:
|
||||||
label: Storage Maximum
|
label: Storage Maximum
|
||||||
|
provisioner: Provisioner
|
||||||
|
lvmVolumeGroup:
|
||||||
|
label: Volume Group
|
||||||
|
create: Create New...
|
||||||
|
storage:
|
||||||
|
longhorn:
|
||||||
|
LonghornV1:
|
||||||
|
label: Longhorn V1 (CSI)
|
||||||
|
LonghornV2:
|
||||||
|
label: Longhorn V2 (CSI)
|
||||||
|
lvm:
|
||||||
|
label: LVM
|
||||||
tags:
|
tags:
|
||||||
label: Host Tags
|
label: Host Tags
|
||||||
addLabel: Add Host Tag
|
addLabel: Add Host Tag
|
||||||
@ -634,7 +646,7 @@ harvester:
|
|||||||
addNetwork: Add Network
|
addNetwork: Add Network
|
||||||
addPort: Add Port
|
addPort: Add Port
|
||||||
cloudConfig:
|
cloudConfig:
|
||||||
title: Cloud Config
|
title: Cloud Configuration
|
||||||
createTemplateTitle: 'Create {name}.'
|
createTemplateTitle: 'Create {name}.'
|
||||||
createNew: Create new...
|
createNew: Create new...
|
||||||
cloudInit:
|
cloudInit:
|
||||||
@ -694,7 +706,7 @@ harvester:
|
|||||||
instance: Virtual Machines
|
instance: Virtual Machines
|
||||||
monitor: Monitor Data
|
monitor: Monitor Data
|
||||||
keypairs: SSH Keys
|
keypairs: SSH Keys
|
||||||
cloudConfig: Cloud Config
|
cloudConfig: Cloud Configuration
|
||||||
metrics: Virtual Machine Metrics
|
metrics: Virtual Machine Metrics
|
||||||
details:
|
details:
|
||||||
title:
|
title:
|
||||||
@ -786,6 +798,8 @@ harvester:
|
|||||||
externalLink:
|
externalLink:
|
||||||
tips: Check volume details
|
tips: Check volume details
|
||||||
rebuildingMessage: 'Rebuilding: {percentage}%'
|
rebuildingMessage: 'Rebuilding: {percentage}%'
|
||||||
|
longhorn:
|
||||||
|
disableResize: 'Longhorn V2 volumes cannot be resized.'
|
||||||
|
|
||||||
image:
|
image:
|
||||||
label: Images
|
label: Images
|
||||||
@ -915,7 +929,7 @@ harvester:
|
|||||||
viewSetting:
|
viewSetting:
|
||||||
prefix: Click
|
prefix: Click
|
||||||
middle: here
|
middle: here
|
||||||
suffix: to view the backup config.
|
suffix: to view the backup configuration.
|
||||||
testConnect:
|
testConnect:
|
||||||
actionLabel: Test connection
|
actionLabel: Test connection
|
||||||
waitingLabel: Testing connection...
|
waitingLabel: Testing connection...
|
||||||
@ -987,6 +1001,7 @@ harvester:
|
|||||||
cert: Upload a self-signed SSL certificate
|
cert: Upload a self-signed SSL certificate
|
||||||
vlanChangeTip: The newly modified default network interface only applies to newly added nodes, not existing ones.
|
vlanChangeTip: The newly modified default network interface only applies to newly added nodes, not existing ones.
|
||||||
defaultPhysicalNIC: Default Network Interface
|
defaultPhysicalNIC: Default Network Interface
|
||||||
|
modifiedMessage: Settings that have been customized from default settings are tagged with 'Modified'.
|
||||||
percentTip: The value in parentheses represents the distribution percentage of the network interface on all hosts. If an interface less than 100% is selected, the user needs to manually specify the network interface on the host where the vlan network configuration fails.
|
percentTip: The value in parentheses represents the distribution percentage of the network interface on all hosts. If an interface less than 100% is selected, the user needs to manually specify the network interface on the host where the vlan network configuration fails.
|
||||||
message:
|
message:
|
||||||
ca:
|
ca:
|
||||||
@ -1036,7 +1051,7 @@ harvester:
|
|||||||
addRewrite: Add Rewrite
|
addRewrite: Add Rewrite
|
||||||
addMirror: Add Mirror
|
addMirror: Add Mirror
|
||||||
configs:
|
configs:
|
||||||
configs: Configs
|
configs: Configurations
|
||||||
registryEDQNorIP: Registry FDQN or IP
|
registryEDQNorIP: Registry FDQN or IP
|
||||||
registryPlaceholder: myregistry.local:5000
|
registryPlaceholder: myregistry.local:5000
|
||||||
username: Username
|
username: Username
|
||||||
@ -1044,7 +1059,7 @@ harvester:
|
|||||||
auth: Auth
|
auth: Auth
|
||||||
identityToken: Identity Token
|
identityToken: Identity Token
|
||||||
insecureSkipVerify: InsecureSkipVerify
|
insecureSkipVerify: InsecureSkipVerify
|
||||||
addConfig: Add Config
|
addConfig: Add Configuration
|
||||||
|
|
||||||
upgrade:
|
upgrade:
|
||||||
selectExitImage: Please select the OS image to upgrade.
|
selectExitImage: Please select the OS image to upgrade.
|
||||||
@ -1116,7 +1131,7 @@ harvester:
|
|||||||
configuration:
|
configuration:
|
||||||
label: Configuration
|
label: Configuration
|
||||||
alertmanagerConfig:
|
alertmanagerConfig:
|
||||||
label: Alertmanager Configs
|
label: Alertmanager Configurations
|
||||||
diabledMonitoringTips:
|
diabledMonitoringTips:
|
||||||
prefix: 'Enable the'
|
prefix: 'Enable the'
|
||||||
middle: 'monitoring'
|
middle: 'monitoring'
|
||||||
@ -1136,13 +1151,13 @@ harvester:
|
|||||||
fluentbit: Fluentbit
|
fluentbit: Fluentbit
|
||||||
fluentd: Fluentd
|
fluentd: Fluentd
|
||||||
clusterFlow:
|
clusterFlow:
|
||||||
label: Cluster Flow
|
label: Cluster Flows
|
||||||
clusterOutput:
|
clusterOutput:
|
||||||
label: Cluster Output
|
label: Cluster Outputs
|
||||||
flow:
|
flow:
|
||||||
label: Flow
|
label: Flows
|
||||||
output:
|
output:
|
||||||
label: Output
|
label: Outputs
|
||||||
diabledTips:
|
diabledTips:
|
||||||
prefix: 'Enable'
|
prefix: 'Enable'
|
||||||
middle: 'logging'
|
middle: 'logging'
|
||||||
@ -1177,6 +1192,14 @@ harvester:
|
|||||||
label: Disk Selector
|
label: Disk Selector
|
||||||
storageClass:
|
storageClass:
|
||||||
label: Storage Class
|
label: Storage Class
|
||||||
|
longhorn:
|
||||||
|
v1:
|
||||||
|
label: Longhorn V1 (CSI)
|
||||||
|
v2:
|
||||||
|
label: Longhorn V2 (CSI)
|
||||||
|
versionTooltip: Longhorn V2 is disabled for this node.
|
||||||
|
lvm:
|
||||||
|
label: LVM
|
||||||
title: Storage Classes
|
title: Storage Classes
|
||||||
customize:
|
customize:
|
||||||
volumeBindingMode:
|
volumeBindingMode:
|
||||||
@ -1194,6 +1217,13 @@ harvester:
|
|||||||
no-options: No available tags, please add in the `Host > Storage` page
|
no-options: No available tags, please add in the `Host > Storage` page
|
||||||
migratable:
|
migratable:
|
||||||
label: Migratable
|
label: Migratable
|
||||||
|
lvmVolumeGroupType:
|
||||||
|
label: Volume Group Type
|
||||||
|
lvmVolumeGroup:
|
||||||
|
label: Volume Group Name
|
||||||
|
no-options: No available Volume Groups, please add in the `Host > Storage` page
|
||||||
|
node:
|
||||||
|
label: Node
|
||||||
allowedTopologies:
|
allowedTopologies:
|
||||||
title: Allowed Topologies
|
title: Allowed Topologies
|
||||||
tooltip: Allowed Topologies helps scheduling virtual machines on hosts which match all of below expressions.
|
tooltip: Allowed Topologies helps scheduling virtual machines on hosts which match all of below expressions.
|
||||||
@ -1229,7 +1259,7 @@ harvester:
|
|||||||
label: Mode
|
label: Mode
|
||||||
miimon:
|
miimon:
|
||||||
label: Miimon
|
label: Miimon
|
||||||
tooltip: <Code>-1</Code> means to keep the original value
|
tooltip: Miimon specifies the MII link monitoring frequency in milliseconds. <Code>-1</Code> means to keep the original value.
|
||||||
nodeSelector:
|
nodeSelector:
|
||||||
matchingNodes:
|
matchingNodes:
|
||||||
matchesSome: |-
|
matchesSome: |-
|
||||||
@ -1435,9 +1465,9 @@ harvester:
|
|||||||
|
|
||||||
usb:
|
usb:
|
||||||
label: USB Devices
|
label: USB Devices
|
||||||
noPermission: Please contact system admin to add Harvester addons first
|
noPermission: Please contact system admin to add Harvester add-ons first
|
||||||
goSetting:
|
goSetting:
|
||||||
prefix: The pcidevices-controller addon is not enabled, click
|
prefix: The pcidevices-controller add-on is not enabled, click
|
||||||
middle: here
|
middle: here
|
||||||
suffix: to enable it to manage your USB devices.
|
suffix: to enable it to manage your USB devices.
|
||||||
enableGroup: Enable Group
|
enableGroup: Enable Group
|
||||||
@ -1499,7 +1529,7 @@ harvester:
|
|||||||
placeholder: 'topology.kubernetes.io/zone'
|
placeholder: 'topology.kubernetes.io/zone'
|
||||||
|
|
||||||
advancedSettings:
|
advancedSettings:
|
||||||
technicalPreview: 'Technical Previews allow users to test and evaluate early-access functionality prior to official supported releases'
|
experimental: 'Experimental features allow users to test and evaluate early-access functionality prior to official supported releases'
|
||||||
descriptions:
|
descriptions:
|
||||||
'harv-vlan': Default Network Interface name of the VLAN network.
|
'harv-vlan': Default Network Interface name of the VLAN network.
|
||||||
'harv-backup-target': Custom backup target to store virtual machine backups.
|
'harv-backup-target': Custom backup target to store virtual machine backups.
|
||||||
@ -1557,8 +1587,8 @@ typeLabel:
|
|||||||
}
|
}
|
||||||
harvesterhci.io.networkattachmentdefinition: |-
|
harvesterhci.io.networkattachmentdefinition: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
one { Virtual Machines Network }
|
one { Virtual Machine Network }
|
||||||
other { Virtual Machines Networks }
|
other { Virtual Machine Networks }
|
||||||
}
|
}
|
||||||
harvesterhci.io.volume: |-
|
harvesterhci.io.volume: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
@ -1587,8 +1617,8 @@ typeLabel:
|
|||||||
}
|
}
|
||||||
harvesterhci.io.virtualmachinebackup: |-
|
harvesterhci.io.virtualmachinebackup: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
one { Virtual Machines Backup }
|
one { Virtual Machine Backup }
|
||||||
other { Virtual Machines Backups }
|
other { Virtual Machine Backups }
|
||||||
}
|
}
|
||||||
harvesterhci.io.cloudtemplate: |-
|
harvesterhci.io.cloudtemplate: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
@ -1602,8 +1632,8 @@ typeLabel:
|
|||||||
}
|
}
|
||||||
harvesterhci.io.vmsnapshot: |-
|
harvesterhci.io.vmsnapshot: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
one { Virtual Machines Snapshot }
|
one { Virtual Machine Snapshot }
|
||||||
other { Virtual Machines Snapshots }
|
other { Virtual Machine Snapshots }
|
||||||
}
|
}
|
||||||
network.harvesterhci.io.vlanconfig: |-
|
network.harvesterhci.io.vlanconfig: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
@ -1655,6 +1685,11 @@ typeLabel:
|
|||||||
one { Cluster Network }
|
one { Cluster Network }
|
||||||
other { Cluster Networks }
|
other { Cluster Networks }
|
||||||
}
|
}
|
||||||
|
harvesterhci.io.addon: |-
|
||||||
|
{count, plural,
|
||||||
|
one { Add-on }
|
||||||
|
other { Add-ons }
|
||||||
|
}
|
||||||
devices.harvesterhci.io.sriovnetworkdevice: |-
|
devices.harvesterhci.io.sriovnetworkdevice: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
one { SR-IOV Network Device }
|
one { SR-IOV Network Device }
|
||||||
|
|||||||
@ -138,7 +138,7 @@ export default {
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<Banner color="warning" class="settings-banner">
|
<Banner color="warning" class="settings-banner">
|
||||||
<div>
|
<div>
|
||||||
{{ t('advancedSettings.subtext') }}
|
{{ t('harvester.setting.modifiedMessage') }}
|
||||||
</div>
|
</div>
|
||||||
</Banner>
|
</Banner>
|
||||||
|
|
||||||
|
|||||||
@ -97,8 +97,8 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
changeRows(filteredRows, searchSchedule) {
|
changeRows(filteredRows, searchSchedule) {
|
||||||
this[searchSchedule] = searchSchedule;
|
this['searchSchedule'] = searchSchedule;
|
||||||
this[backups] = filteredRows;
|
this['backups'] = filteredRows;
|
||||||
},
|
},
|
||||||
|
|
||||||
sortGenerationFn() {
|
sortGenerationFn() {
|
||||||
@ -119,7 +119,7 @@ export default {
|
|||||||
NAMESPACE,
|
NAMESPACE,
|
||||||
{
|
{
|
||||||
name: 'targetVM',
|
name: 'targetVM',
|
||||||
labelKey: 'tableHeaders.targetVm',
|
labelKey: 'harvester.tableHeaders.targetVm',
|
||||||
value: 'attachVM',
|
value: 'attachVM',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
formatter: 'AttachVMWithName'
|
formatter: 'AttachVMWithName'
|
||||||
|
|||||||
@ -64,7 +64,7 @@ export default {
|
|||||||
NAMESPACE,
|
NAMESPACE,
|
||||||
{
|
{
|
||||||
name: 'targetVM',
|
name: 'targetVM',
|
||||||
labelKey: 'tableHeaders.targetVm',
|
labelKey: 'harvester.tableHeaders.targetVm',
|
||||||
value: 'attachVM',
|
value: 'attachVM',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
sort: 'attachVM',
|
sort: 'attachVM',
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
import ResourceTable from '@shell/components/ResourceTable';
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
import { PV, PVC, SCHEMA, LONGHORN } from '@shell/config/types';
|
import {
|
||||||
|
PV, PVC, SCHEMA, LONGHORN, STORAGE_CLASS
|
||||||
|
} from '@shell/config/types';
|
||||||
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
|
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
|
||||||
import HarvesterVolumeState from '../formatters/HarvesterVolumeState';
|
import HarvesterVolumeState from '../formatters/HarvesterVolumeState';
|
||||||
|
|
||||||
@ -32,6 +34,7 @@ export default {
|
|||||||
pvcs: this.$store.dispatch(`${ inStore }/findAll`, { type: PVC }),
|
pvcs: this.$store.dispatch(`${ inStore }/findAll`, { type: PVC }),
|
||||||
pvs: this.$store.dispatch(`${ inStore }/findAll`, { type: PV }),
|
pvs: this.$store.dispatch(`${ inStore }/findAll`, { type: PV }),
|
||||||
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
|
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
|
||||||
|
scs: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const volumeSnapshotSchema = this.$store.getters[`${ inStore }/schemaFor`](VOLUME_SNAPSHOT);
|
const volumeSnapshotSchema = this.$store.getters[`${ inStore }/schemaFor`](VOLUME_SNAPSHOT);
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import ResourceTable from '@shell/components/ResourceTable';
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
|
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
|
||||||
import { PVC, PV, NODE, POD } from '@shell/config/types';
|
import {
|
||||||
|
PVC, PV, NODE, POD, STORAGE_CLASS
|
||||||
|
} from '@shell/config/types';
|
||||||
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
@ -79,6 +81,7 @@ export default {
|
|||||||
images: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.IMAGE }),
|
images: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.IMAGE }),
|
||||||
restore: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESTORE }),
|
restore: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESTORE }),
|
||||||
backups: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BACKUP }),
|
backups: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BACKUP }),
|
||||||
|
storage: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.RESOURCE_QUOTA)) {
|
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.RESOURCE_QUOTA)) {
|
||||||
|
|||||||
@ -24,6 +24,8 @@ import { HCI } from '../../types';
|
|||||||
import { parseVolumeClaimTemplates } from '../../utils/vm';
|
import { parseVolumeClaimTemplates } from '../../utils/vm';
|
||||||
import impl, { QGA_JSON, USB_TABLET } from './impl';
|
import impl, { QGA_JSON, USB_TABLET } from './impl';
|
||||||
|
|
||||||
|
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
||||||
|
|
||||||
export const MANAGEMENT_NETWORK = 'management Network';
|
export const MANAGEMENT_NETWORK = 'management Network';
|
||||||
|
|
||||||
export const OS = [{
|
export const OS = [{
|
||||||
@ -98,6 +100,7 @@ export default {
|
|||||||
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
|
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
|
||||||
secrets: this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }),
|
secrets: this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }),
|
||||||
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
|
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
|
||||||
|
longhornV2Engine: this.$store.dispatch(`${ inStore }/find`, { type: LONGHORN.SETTINGS, id: LONGHORN_V2_DATA_ENGINE }),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.$store.getters[`${ inStore }/schemaFor`](NODE)) {
|
if (this.$store.getters[`${ inStore }/schemaFor`](NODE)) {
|
||||||
@ -239,7 +242,7 @@ export default {
|
|||||||
defaultStorageClass() {
|
defaultStorageClass() {
|
||||||
const defaultStorage = this.$store.getters[`${ this.inStore }/all`](STORAGE_CLASS).find( O => O.isDefault);
|
const defaultStorage = this.$store.getters[`${ this.inStore }/all`](STORAGE_CLASS).find( O => O.isDefault);
|
||||||
|
|
||||||
return defaultStorage?.metadata?.name || 'longhorn';
|
return defaultStorage;
|
||||||
},
|
},
|
||||||
|
|
||||||
storageClassSetting() {
|
storageClassSetting() {
|
||||||
@ -444,7 +447,7 @@ export default {
|
|||||||
id: randomStr(5),
|
id: randomStr(5),
|
||||||
source: SOURCE_TYPE.IMAGE,
|
source: SOURCE_TYPE.IMAGE,
|
||||||
name: 'disk-0',
|
name: 'disk-0',
|
||||||
accessMode: 'ReadWriteMany',
|
accessMode: 'ReadWriteMany', // root disk only support LHv1 volume, should be RWX
|
||||||
bus,
|
bus,
|
||||||
volumeName: '',
|
volumeName: '',
|
||||||
size,
|
size,
|
||||||
@ -1402,14 +1405,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setCpuPinning(value) {
|
|
||||||
if (value) {
|
|
||||||
set(this.spec.template.spec.domain.cpu, 'dedicatedCpuPlacement', true);
|
|
||||||
} else {
|
|
||||||
delete this.spec.template.spec.domain.cpu['dedicatedCpuPlacement'];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setTPM(tpmEnabled) {
|
setTPM(tpmEnabled) {
|
||||||
if (tpmEnabled) {
|
if (tpmEnabled) {
|
||||||
set(this.spec.template.spec.domain.devices, 'tpm', {});
|
set(this.spec.template.spec.domain.devices, 'tpm', {});
|
||||||
|
|||||||
@ -489,7 +489,7 @@ export default class HciNode extends HarvesterResource {
|
|||||||
get unProvisionedDisks() {
|
get unProvisionedDisks() {
|
||||||
const blockDevices = this.blockDevices || [];
|
const blockDevices = this.blockDevices || [];
|
||||||
|
|
||||||
return blockDevices.filter(d => d?.spec?.fileSystem?.provisioned && d?.status?.provisionPhase !== 'Provisioned');
|
return blockDevices.filter(d => d?.isProvisioned && d?.status?.provisionPhase !== 'Provisioned');
|
||||||
}
|
}
|
||||||
|
|
||||||
get diskStatusCount() {
|
get diskStatusCount() {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { _CLONE } from '@shell/config/query-params';
|
import { _CLONE } from '@shell/config/query-params';
|
||||||
import pick from 'lodash/pick';
|
import pick from 'lodash/pick';
|
||||||
import { PV, LONGHORN } from '@shell/config/types';
|
import { PV, LONGHORN, STORAGE_CLASS } from '@shell/config/types';
|
||||||
import { DESCRIPTION } from '@shell/config/labels-annotations';
|
import { DESCRIPTION } from '@shell/config/labels-annotations';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import { findBy } from '@shell/utils/array';
|
import { findBy } from '@shell/utils/array';
|
||||||
@ -10,7 +10,10 @@ import { HCI, VOLUME_SNAPSHOT } from '../../types';
|
|||||||
import HarvesterResource from '../harvester';
|
import HarvesterResource from '../harvester';
|
||||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
|
||||||
|
|
||||||
const DEGRADED_ERROR = 'replica scheduling failed';
|
import { LONGHORN_DRIVER } from '@shell/config/types';
|
||||||
|
import { DATA_ENGINE_V2 } from '../../edit/harvesterhci.io.storage/index.vue';
|
||||||
|
|
||||||
|
const DEGRADED_ERRORS = ['replica scheduling failed', 'precheck new replica failed'];
|
||||||
|
|
||||||
export default class HciPv extends HarvesterResource {
|
export default class HciPv extends HarvesterResource {
|
||||||
applyDefaults(_, realMode) {
|
applyDefaults(_, realMode) {
|
||||||
@ -27,32 +30,44 @@ export default class HciPv extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get availableActions() {
|
get availableActions() {
|
||||||
const out = super._availableActions;
|
let out = super._availableActions;
|
||||||
const clone = out.find(action => action.action === 'goToClone');
|
|
||||||
|
|
||||||
if (clone) {
|
// Longhorn V2 provisioner do not support volume clone feature yet
|
||||||
clone.action = 'goToCloneVolume';
|
if (this.storageClass.longhornVersion === DATA_ENGINE_V2) {
|
||||||
|
out = out.filter(action => action.action !== 'goToClone');
|
||||||
|
} else {
|
||||||
|
const clone = out.find(action => action.action === 'goToClone');
|
||||||
|
|
||||||
|
if (clone) {
|
||||||
|
clone.action = 'goToCloneVolume';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.storageClass.provisioner !== LONGHORN_DRIVER || this.storageClass.longhornVersion !== DATA_ENGINE_V2) {
|
||||||
|
out = [
|
||||||
|
{
|
||||||
|
action: 'exportImage',
|
||||||
|
enabled: this.hasAction('export') && !this.isEncrypted,
|
||||||
|
icon: 'icon icon-copy',
|
||||||
|
label: this.t('harvester.action.exportImage')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'snapshot',
|
||||||
|
enabled: this.hasAction('snapshot'),
|
||||||
|
icon: 'icon icon-backup',
|
||||||
|
label: this.t('harvester.action.snapshot'),
|
||||||
|
},
|
||||||
|
...out
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
action: 'exportImage',
|
|
||||||
enabled: this.hasAction('export') && !this.isEncrypted,
|
|
||||||
icon: 'icon icon-copy',
|
|
||||||
label: this.t('harvester.action.exportImage')
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
action: 'cancelExpand',
|
action: 'cancelExpand',
|
||||||
enabled: this.hasAction('cancelExpand'),
|
enabled: this.hasAction('cancelExpand'),
|
||||||
icon: 'icon icon-backup',
|
icon: 'icon icon-backup',
|
||||||
label: this.t('harvester.action.cancelExpand')
|
label: this.t('harvester.action.cancelExpand')
|
||||||
},
|
},
|
||||||
{
|
|
||||||
action: 'snapshot',
|
|
||||||
enabled: this.hasAction('snapshot'),
|
|
||||||
icon: 'icon icon-backup',
|
|
||||||
label: this.t('harvester.action.snapshot'),
|
|
||||||
},
|
|
||||||
...out
|
...out
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -91,13 +106,19 @@ export default class HciPv extends HarvesterResource {
|
|||||||
this.metadata.annotations = pick(this.metadata.annotations, keys);
|
this.metadata.annotations = pick(this.metadata.annotations, keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get storageClass() {
|
||||||
|
const inStore = this.$rootGetters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return this.$rootGetters[`${ inStore }/all`](STORAGE_CLASS).find(sc => sc.name === this.spec.storageClassName);
|
||||||
|
}
|
||||||
|
|
||||||
get canUpdate() {
|
get canUpdate() {
|
||||||
return this.hasLink('update');
|
return this.hasLink('update');
|
||||||
}
|
}
|
||||||
|
|
||||||
get stateDisplay() {
|
get stateDisplay() {
|
||||||
const volumeError = this.relatedPV?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_ERROR];
|
const volumeError = this.relatedPV?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_ERROR];
|
||||||
const degradedVolume = volumeError === DEGRADED_ERROR;
|
const degradedVolume = DEGRADED_ERRORS.includes(volumeError);
|
||||||
const status = this?.status?.phase === 'Bound' && !volumeError && this.isLonghornVolumeReady ? 'Ready' : 'Not Ready';
|
const status = this?.status?.phase === 'Bound' && !volumeError && this.isLonghornVolumeReady ? 'Ready' : 'Not Ready';
|
||||||
|
|
||||||
const conditions = this?.status?.conditions || [];
|
const conditions = this?.status?.conditions || [];
|
||||||
@ -116,7 +137,7 @@ export default class HciPv extends HarvesterResource {
|
|||||||
// state is similar with stateDisplay, the reason we keep this property is the status of In-use should not be displayed on vm detail page
|
// state is similar with stateDisplay, the reason we keep this property is the status of In-use should not be displayed on vm detail page
|
||||||
get state() {
|
get state() {
|
||||||
const volumeError = this.relatedPV?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_ERROR];
|
const volumeError = this.relatedPV?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_ERROR];
|
||||||
const degradedVolume = volumeError === DEGRADED_ERROR;
|
const degradedVolume = DEGRADED_ERRORS.includes(volumeError);
|
||||||
let status = this?.status?.phase === 'Bound' && !volumeError ? 'Ready' : 'Not Ready';
|
let status = this?.status?.phase === 'Bound' && !volumeError ? 'Ready' : 'Not Ready';
|
||||||
|
|
||||||
const conditions = this?.status?.conditions || [];
|
const conditions = this?.status?.conditions || [];
|
||||||
|
|||||||
@ -2,6 +2,10 @@ import { clone } from '@shell/utils/object';
|
|||||||
import StorageClass from '@shell/models/storage.k8s.io.storageclass';
|
import StorageClass from '@shell/models/storage.k8s.io.storageclass';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
|
||||||
|
import { LONGHORN_DRIVER } from '@shell/config/types';
|
||||||
|
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../edit/harvesterhci.io.storage/index.vue';
|
||||||
|
|
||||||
|
export const LVM_DRIVER = 'lvm.driver.harvesterhci.io';
|
||||||
|
|
||||||
export default class HciStorageClass extends StorageClass {
|
export default class HciStorageClass extends StorageClass {
|
||||||
get detailLocation() {
|
get detailLocation() {
|
||||||
@ -31,4 +35,30 @@ export default class HciStorageClass extends StorageClass {
|
|||||||
get parentNameOverride() {
|
get parentNameOverride() {
|
||||||
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.STORAGE }"`, { count: 1 })?.trim();
|
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.STORAGE }"`, { count: 1 })?.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get longhornVersion() {
|
||||||
|
if (this.provisioner === LONGHORN_DRIVER) {
|
||||||
|
return (this.parameters || {}).dataEngine || DATA_ENGINE_V1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get provisionerDisplay() {
|
||||||
|
let key = '';
|
||||||
|
|
||||||
|
if (this.provisioner === LONGHORN_DRIVER) {
|
||||||
|
key = `harvester.storage.storageClass.longhorn.${ this.longhornVersion }.label`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.provisioner === LVM_DRIVER) {
|
||||||
|
key = `harvester.storage.storageClass.lvm.label`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.$rootGetters['i18n/t'](key);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isLonghornV2() {
|
||||||
|
return this.provisioner === LONGHORN_DRIVER && this.longhornVersion === DATA_ENGINE_V2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import jsyaml from 'js-yaml';
|
|||||||
import startCase from 'lodash/startCase';
|
import startCase from 'lodash/startCase';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '../config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '../config/labels-annotations';
|
||||||
import HarvesterResource from './harvester';
|
import HarvesterResource from './harvester';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
export default class HciAddonConfig extends HarvesterResource {
|
export default class HciAddonConfig extends HarvesterResource {
|
||||||
get availableActions() {
|
get availableActions() {
|
||||||
@ -106,6 +107,10 @@ export default class HciAddonConfig extends HarvesterResource {
|
|||||||
return failedCondition?.message || super.stateDescription;
|
return failedCondition?.message || super.stateDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get parentNameOverride() {
|
||||||
|
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.ADD_ONS }"`, { count: 1 })?.trim();
|
||||||
|
}
|
||||||
|
|
||||||
get displayName() {
|
get displayName() {
|
||||||
const isExperimental = this.metadata?.labels?.[HCI_ANNOTATIONS.ADDON_EXPERIMENTAL] === 'true';
|
const isExperimental = this.metadata?.labels?.[HCI_ANNOTATIONS.ADDON_EXPERIMENTAL] === 'true';
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export default class HciBlockDevice extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isChildPartProvisioned() {
|
get isChildPartProvisioned() {
|
||||||
const parts = this.childParts.filter(p => p.spec?.fileSystem?.provisioned) || [];
|
const parts = this.childParts.filter(p => p.isProvisioned) || [];
|
||||||
|
|
||||||
return parts.length > 0;
|
return parts.length > 0;
|
||||||
}
|
}
|
||||||
@ -59,4 +59,9 @@ export default class HciBlockDevice extends HarvesterResource {
|
|||||||
|
|
||||||
return formatting.status === 'True';
|
return formatting.status === 'True';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isProvisioned() {
|
||||||
|
// spec.fileSystem.provisioned is deprecated
|
||||||
|
return this.spec?.fileSystem?.provisioned || this.spec?.provision;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { HARVESTER_NAME, HARVESTER_NAME as VIRTUAL } from '@shell/config/feature
|
|||||||
import { SETTING } from '@shell/config/settings';
|
import { SETTING } from '@shell/config/settings';
|
||||||
|
|
||||||
export default class HciCluster extends ProvCluster {
|
export default class HciCluster extends ProvCluster {
|
||||||
|
|
||||||
get stateObj() {
|
get stateObj() {
|
||||||
return this._stateObj;
|
return this._stateObj;
|
||||||
}
|
}
|
||||||
@ -29,10 +30,6 @@ export default class HciCluster extends ProvCluster {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedHarvesterClusterVersion = '';
|
|
||||||
|
|
||||||
_uiInfo = undefined;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch and cache the response for /ui-info
|
* Fetch and cache the response for /ui-info
|
||||||
*
|
*
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
|
|||||||
import { BACKUP_TYPE } from '../config/types';
|
import { BACKUP_TYPE } from '../config/types';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import HarvesterResource from './harvester';
|
import HarvesterResource from './harvester';
|
||||||
|
import { LVM_DRIVER } from './harvester/storage.k8s.io.storageclass';
|
||||||
|
|
||||||
export const OFF = 'Off';
|
export const OFF = 'Off';
|
||||||
|
|
||||||
@ -87,12 +88,17 @@ const IgnoreMessages = ['pod has unbound immediate PersistentVolumeClaims'];
|
|||||||
|
|
||||||
export default class VirtVm extends HarvesterResource {
|
export default class VirtVm extends HarvesterResource {
|
||||||
get availableActions() {
|
get availableActions() {
|
||||||
const out = super._availableActions;
|
let out = super._availableActions;
|
||||||
|
|
||||||
const clone = out.find(action => action.action === 'goToClone');
|
// VM attached with Longhorn V2 volume doesn't support clone feature
|
||||||
|
if (this.longhornV2Volumes.length > 0) {
|
||||||
|
out = out.filter(action => action.action !== 'goToClone');
|
||||||
|
} else {
|
||||||
|
const clone = out.find(action => action.action === 'goToClone');
|
||||||
|
|
||||||
if (clone) {
|
if (clone) {
|
||||||
clone.action = 'goToCloneVM';
|
clone.action = 'goToCloneVM';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -150,7 +156,7 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'takeVMSnapshot',
|
action: 'takeVMSnapshot',
|
||||||
enabled: !!this.actions?.backup,
|
enabled: !!this.actions?.backup && !this.longhornV2Volumes.length,
|
||||||
icon: 'icon icon-snapshot',
|
icon: 'icon icon-snapshot',
|
||||||
label: this.t('harvester.action.vmSnapshot')
|
label: this.t('harvester.action.vmSnapshot')
|
||||||
},
|
},
|
||||||
@ -593,16 +599,26 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
return vmis.find(VMI => VMI.id === this.id);
|
return vmis.find(VMI => VMI.id === this.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
get encryptedVolumeType() {
|
get volumes() {
|
||||||
const inStore = this.productInStore;
|
const pvcs = this.$rootGetters[`${ this.productInStore }/all`](PVC);
|
||||||
const pvcs = this.$rootGetters[`${ inStore }/all`](PVC);
|
|
||||||
|
|
||||||
const volumeClaimNames = this.spec.template.spec.volumes?.map(v => v.persistentVolumeClaim?.claimName).filter(v => !!v) || [];
|
const volumeClaimNames = this.spec.template.spec.volumes?.map(v => v.persistentVolumeClaim?.claimName).filter(v => !!v) || [];
|
||||||
const volumes = pvcs.filter(pvc => volumeClaimNames.includes(pvc.metadata.name));
|
|
||||||
|
|
||||||
if (volumes.every(vol => vol.isEncrypted)) {
|
return pvcs.filter(pvc => volumeClaimNames.includes(pvc.metadata.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
get lvmVolumes() {
|
||||||
|
return this.volumes.filter(volume => volume.storageClass.provisioner === LVM_DRIVER);
|
||||||
|
}
|
||||||
|
|
||||||
|
get longhornV2Volumes() {
|
||||||
|
return this.volumes.filter(volume => volume.storageClass.isLonghornV2);
|
||||||
|
}
|
||||||
|
|
||||||
|
get encryptedVolumeType() {
|
||||||
|
if (this.volumes.every(vol => vol.isEncrypted)) {
|
||||||
return 'all';
|
return 'all';
|
||||||
} else if (volumes.some(vol => vol.isEncrypted)) {
|
} else if (this.volumes.some(vol => vol.isEncrypted)) {
|
||||||
return 'partial';
|
return 'partial';
|
||||||
} else {
|
} else {
|
||||||
return 'none';
|
return 'none';
|
||||||
|
|||||||
@ -50,6 +50,7 @@ export const HCI = {
|
|||||||
LB: 'loadbalancer.harvesterhci.io.loadbalancer',
|
LB: 'loadbalancer.harvesterhci.io.loadbalancer',
|
||||||
IP_POOL: 'loadbalancer.harvesterhci.io.ippool',
|
IP_POOL: 'loadbalancer.harvesterhci.io.ippool',
|
||||||
HARVESTER_CONFIG: 'rke-machine-config.cattle.io.harvesterconfig',
|
HARVESTER_CONFIG: 'rke-machine-config.cattle.io.harvesterconfig',
|
||||||
|
LVM_VOLUME_GROUP: 'harvesterhci.io.lvmvolumegroup'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';
|
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';
|
||||||
|
|||||||
@ -1,683 +0,0 @@
|
|||||||
#!/usr/bin/node
|
|
||||||
/* eslint-disable no-console */
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const glob = require('glob');
|
|
||||||
const semver = require('semver');
|
|
||||||
/**
|
|
||||||
* Init logger
|
|
||||||
*/
|
|
||||||
const stats = {
|
|
||||||
libraries: [],
|
|
||||||
node: [],
|
|
||||||
githubActions: [],
|
|
||||||
nvmrc: [],
|
|
||||||
webpack: [],
|
|
||||||
jest: [],
|
|
||||||
router: [],
|
|
||||||
resolution: [],
|
|
||||||
eslint: [],
|
|
||||||
vueSyntax: [],
|
|
||||||
style: [],
|
|
||||||
total: [],
|
|
||||||
};
|
|
||||||
const ignore = [
|
|
||||||
'**/node_modules/**',
|
|
||||||
'**/dist/**',
|
|
||||||
'**/scripts/vue-migrate.js',
|
|
||||||
'docusaurus/**',
|
|
||||||
'storybook-static/**',
|
|
||||||
'storybook/**',
|
|
||||||
];
|
|
||||||
const nodeRequirement = '20.0.0';
|
|
||||||
const isDry = process.argv.includes('--dry');
|
|
||||||
const isVerbose = process.argv.includes('--verbose');
|
|
||||||
const removePlaceholder = 'REMOVE';
|
|
||||||
const params = { paths: null };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Package updates
|
|
||||||
* Files: package.json
|
|
||||||
*/
|
|
||||||
const packageUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || '**/package.json', { ignore });
|
|
||||||
|
|
||||||
files.forEach((file) => {
|
|
||||||
let content = fs.readFileSync(file, 'utf8');
|
|
||||||
const toReplaceNode = false;
|
|
||||||
|
|
||||||
// TODO: Refactor and loop?
|
|
||||||
const [librariesContent, replaceLibraries] = packageUpdatesLibraries(file, content);
|
|
||||||
|
|
||||||
if (replaceLibraries.length) {
|
|
||||||
content = librariesContent;
|
|
||||||
printContent(file, `Updating`, replaceLibraries);
|
|
||||||
stats.libraries.push(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [nodeContent, replaceNode] = packageUpdatesEngine(file, content);
|
|
||||||
|
|
||||||
if (replaceNode.length) {
|
|
||||||
printContent(file, `Updating node`, replaceNode);
|
|
||||||
content = nodeContent;
|
|
||||||
stats.node.push(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [resolutionContent, replaceResolution] = packageUpdatesResolution(file, content);
|
|
||||||
|
|
||||||
if (replaceResolution.length) {
|
|
||||||
printContent(file, `Updating resolution`, replaceResolution);
|
|
||||||
content = resolutionContent;
|
|
||||||
stats.libraries.push(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (replaceLibraries || toReplaceNode || replaceResolution) {
|
|
||||||
stats.total.push(file);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify package vue related libraries versions
|
|
||||||
*/
|
|
||||||
const packageUpdatesLibraries = (file, oldContent) => {
|
|
||||||
let content = oldContent;
|
|
||||||
let parsedJson = JSON.parse(content);
|
|
||||||
const replaceLibraries = [];
|
|
||||||
const types = ['dependencies', 'devDependencies', 'peerDependencies'];
|
|
||||||
// [Library name, new version or new library, new library version]
|
|
||||||
const librariesUpdates = [
|
|
||||||
['@nuxt/babel-preset-app', removePlaceholder],
|
|
||||||
['@types/jest', '^29.5.2'],
|
|
||||||
['@typescript-eslint/eslint-plugin', '~5.4.0'],
|
|
||||||
['@typescript-eslint/parser', '~5.4.0'],
|
|
||||||
['@vue/cli-plugin-babel', '~5.0.0'],
|
|
||||||
['@vue/cli-plugin-e2e-cypress', '~5.0.0'],
|
|
||||||
['@vue/cli-plugin-eslint', '~5.0.0'],
|
|
||||||
['@vue/cli-plugin-router', '~5.0.0'],
|
|
||||||
['@vue/cli-plugin-typescript', '~5.0.0'],
|
|
||||||
['@vue/cli-plugin-unit-jest', '~5.0.0'],
|
|
||||||
['@vue/cli-plugin-vuex', '~5.0.0'],
|
|
||||||
['@vue/cli-service', '~5.0.0'],
|
|
||||||
['@vue/eslint-config-typescript', '~9.1.0'],
|
|
||||||
['@vue/vue2-jest', '@vue/vue3-jest', '^27.0.0-alpha.1'],
|
|
||||||
['@vue/test-utils', '~2.0.0-0'],
|
|
||||||
['core-js', '3.25.3'],
|
|
||||||
['cache-loader', '^4.1.0'],
|
|
||||||
['node-polyfill-webpack-plugin', '^3.0.0'],
|
|
||||||
['portal-vue', '~3.0.0'],
|
|
||||||
['require-extension-hooks-babel', '1.0.0'],
|
|
||||||
['require-extension-hooks-vue', '3.0.0'],
|
|
||||||
['require-extension-hooks', '0.3.3'],
|
|
||||||
['sass-loader', '~12.0.0'],
|
|
||||||
['typescript', '~4.5.5'],
|
|
||||||
['vue-router', '~4.0.3'],
|
|
||||||
['vue-virtual-scroll-list', 'vue3-virtual-scroll-list', '0.2.1'],
|
|
||||||
['vue', '~3.2.13'],
|
|
||||||
['vuex', '~4.0.0'],
|
|
||||||
['xterm', '5.2.1'],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Loop through each type of dependencies since many often not correctly placed or hard to track
|
|
||||||
types.forEach((type) => {
|
|
||||||
if (parsedJson[type]) {
|
|
||||||
librariesUpdates.forEach(([library, newVersion, newLibraryVersion]) => {
|
|
||||||
if (parsedJson[type][library]) {
|
|
||||||
const version = semver.coerce(parsedJson[type][library]);
|
|
||||||
|
|
||||||
if (newVersion === removePlaceholder) {
|
|
||||||
// Remove library
|
|
||||||
replaceLibraries.push([library, [parsedJson[type][library], removePlaceholder]]);
|
|
||||||
delete parsedJson[type][library];
|
|
||||||
content = JSON.stringify(parsedJson, null, 2);
|
|
||||||
writeContent(file, content);
|
|
||||||
} else if (newLibraryVersion) {
|
|
||||||
// Replace with a new library if present, due breaking changes in Vue3
|
|
||||||
replaceLibraries.push([library, [parsedJson[type][library], newVersion, newLibraryVersion]]);
|
|
||||||
content = content.replaceAll(`"${ library }": "${ parsedJson[type][library] }"`, `"${ newVersion }": "${ newLibraryVersion }"`);
|
|
||||||
parsedJson = JSON.parse(content);
|
|
||||||
writeContent(file, content);
|
|
||||||
} else if (version && semver.lt(version, semver.coerce(newVersion))) {
|
|
||||||
// Update library version if outdated
|
|
||||||
replaceLibraries.push([library, [parsedJson[type][library], newVersion]]);
|
|
||||||
content = content.replaceAll(`"${ library }": "${ parsedJson[type][library] }"`, `"${ library }": "${ newVersion }"`);
|
|
||||||
parsedJson = JSON.parse(content);
|
|
||||||
writeContent(file, content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return [content, replaceLibraries];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify package engines node to latest
|
|
||||||
*/
|
|
||||||
const packageUpdatesEngine = (file, oldContent) => {
|
|
||||||
let content = oldContent;
|
|
||||||
let parsedJson = JSON.parse(content);
|
|
||||||
const replaceNode = [];
|
|
||||||
|
|
||||||
// Verify package engines node to latest
|
|
||||||
if (parsedJson.engines) {
|
|
||||||
const outdated = semver.lt(semver.coerce(parsedJson.engines.node), semver.coerce(nodeRequirement));
|
|
||||||
|
|
||||||
if (outdated) {
|
|
||||||
replaceNode.push([parsedJson.engines.node, nodeRequirement]);
|
|
||||||
content = content.replaceAll(`"node": "${ parsedJson.engines.node }"`, `"node": ">=${ nodeRequirement }"`);
|
|
||||||
parsedJson = JSON.parse(content);
|
|
||||||
writeContent(file, content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [content, replaceNode];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add resolutions for VueCLI
|
|
||||||
*/
|
|
||||||
const packageUpdatesResolution = (file, oldContent) => {
|
|
||||||
let content = oldContent;
|
|
||||||
let parsedJson = JSON.parse(content);
|
|
||||||
const replaceResolution = [];
|
|
||||||
const resolutions = [
|
|
||||||
['@vue/cli-service/html-webpack-plugin', '^5.0.0'],
|
|
||||||
['**/webpack', removePlaceholder],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Verify package engines node to latest
|
|
||||||
if (parsedJson.resolutions) {
|
|
||||||
resolutions.forEach(([library, newVersion]) => {
|
|
||||||
if (newVersion === removePlaceholder) {
|
|
||||||
delete parsedJson.resolutions[library];
|
|
||||||
content = JSON.stringify(parsedJson, null, 2);
|
|
||||||
parsedJson = JSON.parse(content);
|
|
||||||
writeContent(file, content);
|
|
||||||
} else if (!parsedJson.resolutions[library]) {
|
|
||||||
// Add resolution if not present
|
|
||||||
parsedJson.resolutions[library] = newVersion;
|
|
||||||
content = JSON.stringify(parsedJson, null, 2);
|
|
||||||
parsedJson = JSON.parse(content);
|
|
||||||
writeContent(file, content);
|
|
||||||
} else {
|
|
||||||
// Ensure resolution version is up to date
|
|
||||||
const outdated = semver.lt(semver.coerce(parsedJson.resolutions[library]), semver.coerce(newVersion));
|
|
||||||
|
|
||||||
if (outdated) {
|
|
||||||
replaceResolution.push([parsedJson.engines.node, nodeRequirement]);
|
|
||||||
content = content.replaceAll(`"${ library }": "${ parsedJson.resolutions[library] }"`, `"${ library }": "${ newVersion }"`);
|
|
||||||
parsedJson = JSON.parse(content);
|
|
||||||
writeContent(file, content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return [content, replaceResolution];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GitHub Actions updates
|
|
||||||
* Files: .github/workflows/**.yml
|
|
||||||
*
|
|
||||||
* Verify GitHub Actions use of current node version, e.g. node-version: '<18'
|
|
||||||
*/
|
|
||||||
const gitHubActionsUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || '.github/workflows/**.{yml,yaml}', { ignore });
|
|
||||||
|
|
||||||
files.forEach((file) => {
|
|
||||||
let content = fs.readFileSync(file, 'utf8');
|
|
||||||
const nodeVersionMatches = content.matchAll(/node-version: \'([0-9.x]+)\'/g);
|
|
||||||
const toReplace = [];
|
|
||||||
|
|
||||||
// Check all the node occurrences within the test file
|
|
||||||
if (nodeVersionMatches) {
|
|
||||||
for (const matches of nodeVersionMatches) {
|
|
||||||
for (const match of matches) {
|
|
||||||
const nodeVersion = semver.coerce(match);
|
|
||||||
|
|
||||||
if (nodeVersion && semver.lt(nodeVersion, semver.coerce(nodeRequirement))) {
|
|
||||||
content = content.replaceAll(`node-version: '${ match }'`, `node-version: '20.x'`);
|
|
||||||
|
|
||||||
writeContent(file, content);
|
|
||||||
toReplace.push([match, nodeRequirement]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toReplace.length) {
|
|
||||||
printContent(file, `Updating node`, toReplace);
|
|
||||||
stats.githubActions.push(file);
|
|
||||||
stats.total.push(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NVM updates
|
|
||||||
* Files: .nvmrc
|
|
||||||
*
|
|
||||||
* Verify presence of .nvmrc, create one if none, update if any
|
|
||||||
*/
|
|
||||||
const nvmUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || '**/.nvmrc', { ignore });
|
|
||||||
const nvmRequirement = 20;
|
|
||||||
|
|
||||||
files.forEach((file) => {
|
|
||||||
if (file) {
|
|
||||||
let content = fs.readFileSync(file, 'utf8');
|
|
||||||
const nodeVersionMatch = content.match(/([0-9.x]+)/g);
|
|
||||||
const nodeVersion = semver.coerce(nodeVersionMatch[0]);
|
|
||||||
|
|
||||||
// Ensure node version is up to date
|
|
||||||
if (nodeVersion && semver.lt(nodeVersion, semver.coerce(nodeRequirement))) {
|
|
||||||
printContent(file, `Updating node ${ [nodeVersionMatch[0], nvmRequirement] }`);
|
|
||||||
content = content.replaceAll(nodeVersionMatch[0], nvmRequirement);
|
|
||||||
|
|
||||||
writeContent(file, content);
|
|
||||||
stats.nvmrc.push(file);
|
|
||||||
stats.total.push(file);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
writeContent('.nvmrc', nvmRequirement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vue config update
|
|
||||||
* Files: vue.config.js
|
|
||||||
*
|
|
||||||
* Verify vue.config presence of deprecated Webpack5 options
|
|
||||||
* - devServer.public: 'path' -> client: { webSocketURL: 'path' }
|
|
||||||
*/
|
|
||||||
const vueConfigUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || 'vue.config**.js', { ignore });
|
|
||||||
|
|
||||||
files.forEach((file) => {
|
|
||||||
const content = fs.readFileSync(file, 'utf8');
|
|
||||||
|
|
||||||
// Verify vue.config presence of deprecated Webpack5 options
|
|
||||||
if (content.includes('devServer.public: \'path\'')) {
|
|
||||||
stats.webpack.push(file);
|
|
||||||
stats.total.push(file);
|
|
||||||
// TODO: Add replacement
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vue syntax update (to do not mix with tests)
|
|
||||||
* Files: .vue, .js, .ts (not .spec.ts, not .test.ts)
|
|
||||||
*/
|
|
||||||
const vueSyntaxUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || '**/*.{vue,js,ts}', { ignore: [...ignore, '**/*.spec.ts', '**/__tests__/**', '**/*.test.ts', 'jest.setup.js', '**/*.d.ts', '**/vue-shim.ts'] });
|
|
||||||
const replacementCases = [
|
|
||||||
// Prioritize set and delete to be converted since removed in Vue3
|
|
||||||
[/\=\> Vue\.set\((.*?),\s*(.*?),\s*(.*?)\)/g, (_, obj, prop, val) => `=> (${ obj.trim() }[${ prop.trim() }] = ${ val.trim() })`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/\=\> Vue\.set\((.*?),\s*'([^']*?)',\s*\{([\s\S]*?)\}\)/g, (_, obj, prop, val) => `=> (${ obj.trim() }['${ prop }'] = {${ val.trim() }})`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/\=\> Vue\.set\((.*?),\s*'([^']*?)',\s*(.*?)\)/g, (_, obj, prop, val) => `=> (${ obj.trim() }.${ prop } = ${ val.trim() })`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
|
|
||||||
[/Vue\.set\((.*?),\s*(.*?),\s*(.*?)\)/g, (_, obj, prop, val) => `${ obj.trim() }[${ prop.trim() }] = ${ val.trim() }`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/Vue\.set\((.*?),\s*'([^']*?)',\s*\{([\s\S]*?)\}\)/g, (_, obj, prop, val) => `${ obj.trim() }['${ prop }'] = {${ val.trim() }}`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/Vue\.set\((.*?),\s*'([^']*?)',\s*(.*?)\)/g, (_, obj, prop, val) => `${ obj.trim() }.${ prop } = ${ val.trim() }`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/Vue\.delete\((.*?),\s*(.*?)\)/g, (_, obj, prop) => `delete ${ obj.trim() }[${ prop.trim() }]`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
|
|
||||||
[/\=\> this\.\$set\((.*?),\s*(.*?),\s*(.*?)\)/g, (_, obj, prop, val) => `=> (${ obj.trim() }[${ prop.trim() }] = ${ val.trim() })`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/\=\> this\.\$set\((.*?),\s*'([^']*?)',\s*\{([\s\S]*?)\}\)/g, (_, obj, prop, val) => `=> (${ obj.trim() }['${ prop }'] = {${ val.trim() }})`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/\=\> this\.\$set\((.*?),\s*'([^']*?)',\s*(.*?)\)/g, (_, obj, prop, val) => `=> (${ obj.trim() }.${ prop } = ${ val.trim() })`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
|
|
||||||
[/this.\$set\((.*?),\s*'([^']*?)',\s*(.*?)\)/g, (_, obj, prop, val) => obj.trim() === 'this' ? `this['${ prop }'] = ${ val }` : `${ obj.trim() }['${ prop }'] = ${ val }`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
|
|
||||||
[/this\.\$set\((.*?),\s*(.*?),\s*(.*?)\)/g, (_, obj, prop, val) => `${ obj.trim() }[${ prop.trim() }] = ${ val.trim() }`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/this\.\$set\((.*?),\s*'([^']*?)',\s*\{([\s\S]*?)\}\)/g, (_, obj, prop, val) => `${ obj.trim() }['${ prop }'] = {${ val.trim() }}`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/this\.\$set\((.*?),\s*'([^']*?)',\s*(.*?)\)/g, (_, obj, prop, val) => `${ obj.trim() }.${ prop } = ${ val.trim() }`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/this\.\$delete\((.*?),\s*(.*?)\)/g, (_, obj, prop) => `delete ${ obj.trim() }[${ prop.trim() }]`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
|
|
||||||
// Replace imports for all the cases where createApp is needed, before the rest of the replacements
|
|
||||||
[/import Vue from 'vue';?/g, `import { createApp } from \'vue\';\nconst vueApp = createApp({});`, `https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp`],
|
|
||||||
[`new Vue(`, `createApp(`, `https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp`],
|
|
||||||
[`Vue.config`, `vueApp.config`, `https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp`],
|
|
||||||
[`Vue.directive`, `vueApp.directive`, `https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp`],
|
|
||||||
[`Vue.filter(`, `vueApp.filter(`, `https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp`],
|
|
||||||
[`Vue.mixin(`, `vueApp.mixin(`, `https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp`],
|
|
||||||
[`Vue.component(`, `vueApp.component(`, `https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp`],
|
|
||||||
[`Vue.use(`, `vueApp.use(`, `https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp`],
|
|
||||||
[`Vue.prototype`, `vueApp.config.globalProperties`, `https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp`],
|
|
||||||
['Vue.util', '', 'Vue.util is private and no longer available https://v3-migration.vuejs.org/migration-build.html#partially-compatible-with-caveats'],
|
|
||||||
// [`Vue.extend`, removePlaceholder, 'https://v3-migration.vuejs.org/breaking-changes/global-api.html#vue-extend-removed'],
|
|
||||||
// [`Vue.extend`, `createApp({})`], // (mixins)
|
|
||||||
|
|
||||||
[`vue-virtual-scroll-list`, `vue3-virtual-scroll-list`, 'library update'],
|
|
||||||
|
|
||||||
[`Vue.nextTick`, `nextTick`, 'https://v3-migration.vuejs.org/breaking-changes/global-api-treeshaking.html#global-api-treeshaking'],
|
|
||||||
[`this.nextTick`, `nextTick`, 'https://v3-migration.vuejs.org/breaking-changes/global-api-treeshaking.html#global-api-treeshaking'],
|
|
||||||
// TODO: Add missing import
|
|
||||||
|
|
||||||
[/( {4,}default)\(\)\s*\{([\s\S]*?)this\.([\s\S]*?\}\s*\})/g, (_, before, middle, after) => `${ before }(props) {${ middle }props.${ after }`, 'https://v3-migration.vuejs.org/breaking-changes/props-default-this.html'],
|
|
||||||
[`value=`, `modelValue=`],
|
|
||||||
[`@input=`, `@update:modelValue=`],
|
|
||||||
// [`v-bind.sync=`, `:modelValue=`, `https://v3-migration.vuejs.org/breaking-changes/v-model.html#using-v-bind-sync`],
|
|
||||||
// ['v-model=', ':modelValue=', ''],
|
|
||||||
[`click.native`, `click`, `https://v3-migration.vuejs.org/breaking-changes/v-model.html#using-v-bind-sync`],
|
|
||||||
[`v-on="$listeners"`, removePlaceholder, `removed and integrated with $attrs https://v3-migration.vuejs.org/breaking-changes/listeners-removed.html`],
|
|
||||||
[`:listeners="$listeners"`, `:v-bind="$attrs"`, `removed and integrated with $attrs https://v3-migration.vuejs.org/breaking-changes/listeners-removed.html`],
|
|
||||||
|
|
||||||
[/this\.\$scopedSlots\[(\w+)\]|this\.\$scopedSlots\.(\w+)/, (match, key1, key2) => `this.$slots.${ key1 || key2 }()`, `(many components loop through them) https://v3-migration.vuejs.org/breaking-changes/slots-unification.html`],
|
|
||||||
[` $scopedSlots`, ` $slots`, `(many components loop through them) https://v3-migration.vuejs.org/breaking-changes/slots-unification.html`],
|
|
||||||
[/slot="(\w+:\w+)"\s+slot-scope="(\w+)"/g, `$1="$2"`, `not mentioned in migration https://vuejs.org/guide/components/slots.html#scoped-slots`],
|
|
||||||
[/this\.\$slots\['([^']+)'\]/g, `this.$slots[\'$1\']()`, `not mentioned in migration https://vuejs.org/guide/components/slots.html#scoped-slots`],
|
|
||||||
// [/this\.\$slots\.([^']+)'/g, `this.$slots.$1()`, `not mentioned in migration https://vuejs.org/guide/components/slots.html#scoped-slots`],
|
|
||||||
// [/this\.\$slots(?!\s*\(\))(\b|\?|['"\[])/g, `this.$slots()$1`, `https://eslint.vuejs.org/rules/require-slots-as-functions.html`], // TODO: Add exception for existing brackets
|
|
||||||
|
|
||||||
// Portals are now Vue3 Teleports
|
|
||||||
[/<portal|<portal-target|<\/portal|<\/portal-target/g, '', `https://v3.vuejs.org/guide/teleport.html`],
|
|
||||||
|
|
||||||
// TODO: probably requires JSDom
|
|
||||||
// [/<template v-for="([\s\S]*?)">\s*<([\s\S]*?)\s*([\s\S]*?):key="([\s\S]*?)"([\s\S]*?)<\/([\s\S]*?)>\s*<\/template>/gs, '<template v-for="$1" :key="$4"><$3 $5>$6</$7>\n</template>', `https://v3-migration.vuejs.org/breaking-changes/key-attribute.html#with-template-v-for`],
|
|
||||||
// [/(<\w+(?!.*?v-for=)[^>]*?)\s*:key="[^"]*"\s*([^>]*>)/g, '$1$2', `https://v3-migration.vuejs.org/breaking-changes/key-attribute.html#with-template-v-for`],
|
|
||||||
[/(<\w+[^>v\-for]*?):key="[^"]*"\s*([^>]*>)/g, '$1$2', `https://v3-migration.vuejs.org/breaking-changes/key-attribute.html#with-template-v-for`],
|
|
||||||
[/(\<\w+\s+(?:[^>]*?\s+)?v-for="\(.*?,\s*(\w+)\s*\).*?")(?:\s*:key=".*"\s*)?([^>]*>)/g, '$1 :key="$2"$3', `https://v3-migration.vuejs.org/breaking-changes/key-attribute.html#with-template-v-for`],
|
|
||||||
[/(\<\w+\s+(?:[^>]*?\s+)?)v-for="(?!\*?\s+.*?\s+.*?)([^,]+?)\s+in\s+([^"]+?)"(?:\s*:key=".*"\s*)?([^>]*>)/g, '$1 v-for="($2, i) in $3" :key="i" $4', `https://v3-migration.vuejs.org/breaking-changes/key-attribute.html#with-template-v-for`],
|
|
||||||
|
|
||||||
// TODO: except for <components /> elements, probably requires JSDom
|
|
||||||
// [' is=', ``, `https://v3-migration.vuejs.org/breaking-changes/custom-elements-interop.html#customized-built-in-elements`],
|
|
||||||
// [' :is=', ``, `https://v3-migration.vuejs.org/breaking-changes/custom-elements-interop.html#customized-built-in-elements`],
|
|
||||||
|
|
||||||
// Directive updates
|
|
||||||
// [`bind(`, '', `beforeMount( but there's too many bind cases https://v3-migration.vuejs.org/breaking-changes/custom-directives.html`], // TODO: Restrict to directives and context
|
|
||||||
// [`update(`, '', `removed, also common term https://v3-migration.vuejs.org/breaking-changes/custom-directives.html`], // TODO: Restrict to directives and context
|
|
||||||
[`inserted(`, `mounted(`, 'https://v3-migration.vuejs.org/breaking-changes/custom-directives.html'],
|
|
||||||
[`componentUpdated(`, `updated(`, 'https://v3-migration.vuejs.org/breaking-changes/custom-directives.html'],
|
|
||||||
[`unbind`, `unmounted`, 'https://v3-migration.vuejs.org/breaking-changes/custom-directives.html'],
|
|
||||||
|
|
||||||
// [`propsData (app creation)`, ``, `use second argument of createApp({}) https://v3-migration.vuejs.org/breaking-changes/props-data.html`],
|
|
||||||
[`@hook:lifecycleHook`, `@vue:lifecycleHook`, `https://v3-migration.vuejs.org/breaking-changes/vnode-lifecycle-events.html`],
|
|
||||||
|
|
||||||
// Nuxt and initalize case only
|
|
||||||
// TODO: Use eventbus replacement as temporary solution?
|
|
||||||
[`$on('event', callback)`, '', `no migration, existing lib, https://v3-migration.vuejs.org/breaking-changes/events-api.html`],
|
|
||||||
[`$off('event', callback)`, '', `no migration, existing lib, https://v3-migration.vuejs.org/breaking-changes/events-api.html`],
|
|
||||||
[`$once('event', callback)`, '', `no migration, existing lib, https://v3-migration.vuejs.org/breaking-changes/events-api.html`],
|
|
||||||
|
|
||||||
// [`$children`, ``, `no migration, $refs are suggested as communication https://v3-migration.vuejs.org/breaking-changes/children.html`],
|
|
||||||
|
|
||||||
// Vuex
|
|
||||||
[`new Vuex.Store(`, `createStore(`, 'To install Vuex to a Vue instance, pass the store instead of Vuex https://vuex.vuejs.org/guide/migrating-to-4-0-from-3-x.html#installation-process'],
|
|
||||||
[`import Vuex from 'vuex'`, `import { createStore } from 'vuex'`, 'To install Vuex to a Vue instance, pass the store instead of Vuex https://vuex.vuejs.org/guide/migrating-to-4-0-from-3-x.html#installation-process'],
|
|
||||||
|
|
||||||
// Extra cases TBD (it seems like we already use the suggested way for arrays)
|
|
||||||
// watch option used on arrays not triggered by mutations https://v3-migration.vuejs.org/breaking-changes/watch.html
|
|
||||||
];
|
|
||||||
|
|
||||||
replaceCases('vueSyntax', files, replacementCases, `Updating Vue syntax`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vue Router
|
|
||||||
* Files: .vue, .js, .ts
|
|
||||||
*/
|
|
||||||
const routerUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || '**/*.{vue,js,ts}', { ignore });
|
|
||||||
const replacementCases = [
|
|
||||||
[`import Router from 'vue-router'`, `import { createRouter } from 'vue-router'`],
|
|
||||||
[`Vue.use(Router)`, `const router = createRouter({})`],
|
|
||||||
// [`currentRoute`, '', 'The currentRoute property is now a ref() https://router.vuejs.org/guide/migration/#The-currentRoute-property-is-now-a-ref-'],
|
|
||||||
[/import\s*\{([^}]*)\s* RouteConfig\s*([^}]*)\}\s*from\s*'vue-router'/g, (match, before, after) => `import {${ before.trim() } RouteRecordRaw ${ after.trim() }} from 'vue-router'`],
|
|
||||||
[/import\s*\{([^}]*)\s* Location\s*([^}]*)\}\s*from\s*'vue-router'/g, (match, before, after) => `import {${ before.trim() } RouteLocation ${ after.trim() }} from 'vue-router'`],
|
|
||||||
['imported Router', ''],
|
|
||||||
['router.name', '', 'now string | Symbol'],
|
|
||||||
[`mode: \'history\'`, 'history: createWebHistory()'],
|
|
||||||
// ['getMatchedComponents', '', 'https://router.vuejs.org/guide/migration/#Removal-of-router-getMatchedComponents-'],
|
|
||||||
];
|
|
||||||
|
|
||||||
replaceCases('router', files, replacementCases, `Updating Vue Router`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Jest update
|
|
||||||
* https://test-utils.vuejs.org/migration
|
|
||||||
* Files: .spec.js, .spec.ts, .test.js, .test.ts
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const jestUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || '**/*.{test.js,test.ts}', { ignore });
|
|
||||||
|
|
||||||
const cases = [
|
|
||||||
['config.mocks.$myGlobal', '', ''],
|
|
||||||
['createLocalVue', '', 'https://test-utils.vuejs.org/migration/#No-more-createLocalVue'],
|
|
||||||
['new Vuex.Store', '', ''],
|
|
||||||
['store', '', ''],
|
|
||||||
['propsData', 'props', 'https://test-utils.vuejs.org/migration/#propsData-is-now-props'],
|
|
||||||
['localVue.extend({})', '', ''],
|
|
||||||
['Vue.nextTick', '', ''],
|
|
||||||
['$destroy', '$unmount', 'https://test-utils.vuejs.org/migration/#destroy-is-now-unmount-to-match-Vue-3'],
|
|
||||||
['mocks', '', 'https://test-utils.vuejs.org/migration/#mocks-and-stubs-are-now-in-global'],
|
|
||||||
['stubs', '', 'https://test-utils.vuejs.org/migration/#mocks-and-stubs-are-now-in-global'],
|
|
||||||
['mixins', '', 'https://test-utils.vuejs.org/migration/#mocks-and-stubs-are-now-in-global'],
|
|
||||||
['plugins', '', 'https://test-utils.vuejs.org/migration/#mocks-and-stubs-are-now-in-global'],
|
|
||||||
['component', '', 'https://test-utils.vuejs.org/migration/#mocks-and-stubs-are-now-in-global'],
|
|
||||||
['directives', '', 'https://test-utils.vuejs.org/migration/#mocks-and-stubs-are-now-in-global'],
|
|
||||||
['slots', '', 'slots‘s scope is now exposed as params https://test-utils.vuejs.org/migration/#slots-s-scope-is-now-exposed-as-params'],
|
|
||||||
['scopedSlots', '', 'scopedSlots is now merged with slots https://test-utils.vuejs.org/migration/#scopedSlots-is-now-merged-with-slots'],
|
|
||||||
['parentComponent', '', 'deprecated '],
|
|
||||||
['contains', 'find', 'deprecated '],
|
|
||||||
['config.global.renderStubDefaultSlot = false', '', ''],
|
|
||||||
['findAll().at(0)', '', '']
|
|
||||||
];
|
|
||||||
|
|
||||||
replaceCases('jest', files, cases, `Updating Jest`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Jest config updates
|
|
||||||
* Files: jest.config.js, .json, .ts
|
|
||||||
*
|
|
||||||
* /node_modules/@vue/vue2-jest --> reference needs new library version
|
|
||||||
*/
|
|
||||||
const jestConfigUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || '**/jest.config.{js,ts,json}', { ignore });
|
|
||||||
const cases = [
|
|
||||||
['/node_modules/@vue/vue2-jest', '/node_modules/@vue/vue3-jest']
|
|
||||||
];
|
|
||||||
|
|
||||||
replaceCases('jest', files, cases, `Updating Jest config`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ESLint Updates
|
|
||||||
* Files: .eslintrc.js, .eslintrc.json, .eslintrc.yml
|
|
||||||
*/
|
|
||||||
const eslintUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || '**/.eslintrc.*{js,json,yml}', { ignore });
|
|
||||||
// Add cases introduced with new recommended settings
|
|
||||||
const replacePlugins = [
|
|
||||||
['plugin:vue/essential', 'plugin:vue/vue3-essential'],
|
|
||||||
['plugin:vue/strongly-recommended', 'plugin:vue/vue3-strongly-recommended'],
|
|
||||||
['plugin:vue/recommended', 'plugin:vue/vue3-recommended']
|
|
||||||
];
|
|
||||||
const newRules = {
|
|
||||||
'vue/one-component-per-file': 'off',
|
|
||||||
'vue/no-deprecated-slot-attribute': 'off',
|
|
||||||
'vue/require-explicit-emits': 'off',
|
|
||||||
'vue/v-on-event-hyphenation': 'off',
|
|
||||||
};
|
|
||||||
|
|
||||||
files.forEach((file) => {
|
|
||||||
let content = fs.readFileSync(file, 'utf8');
|
|
||||||
const matchedCases = [];
|
|
||||||
|
|
||||||
replacePlugins.forEach(([text, replacement]) => {
|
|
||||||
const isCase = content.includes(text);
|
|
||||||
|
|
||||||
if (isCase) {
|
|
||||||
content = content.replaceAll(text, replacement);
|
|
||||||
matchedCases.push([text, replacement]);
|
|
||||||
|
|
||||||
writeContent(file, content);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add the new rules if they don't exist
|
|
||||||
const eslintConfigPath = path.join(__dirname, `../${ file }`);
|
|
||||||
const eslintConfig = require(eslintConfigPath);
|
|
||||||
|
|
||||||
Object.keys(newRules).forEach((rule) => {
|
|
||||||
if (!eslintConfig.rules[rule]) {
|
|
||||||
eslintConfig.rules[rule] = newRules[rule];
|
|
||||||
matchedCases.push(rule);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
writeContent(eslintConfigPath, `module.exports = ${ JSON.stringify(eslintConfig, null, 2) }`);
|
|
||||||
|
|
||||||
if (matchedCases.length) {
|
|
||||||
printContent(file, `Updating ESLint`, matchedCases);
|
|
||||||
stats.eslint.push(file);
|
|
||||||
stats.total.push(file);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TS Updates
|
|
||||||
* Files: tsconfig*.json
|
|
||||||
*
|
|
||||||
* Add information about TS issues, recommend @ts-nocheck as temporary solution
|
|
||||||
*/
|
|
||||||
const tsUpdates = () => {
|
|
||||||
console.warn('TS checks are stricter and may require to be fixed manually.',
|
|
||||||
'Use @ts-nocheck to give you time to fix them.',
|
|
||||||
'Add exception to your ESLint config to avoid linting errors.');
|
|
||||||
// TODO: Add case
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Styles updates
|
|
||||||
*/
|
|
||||||
const stylesUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || '**/*.{vue, scss}', { ignore });
|
|
||||||
const cases = [
|
|
||||||
['::v-deep', ':deep()'],
|
|
||||||
];
|
|
||||||
|
|
||||||
replaceCases('style', files, cases, `Updating styles`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to write content
|
|
||||||
*/
|
|
||||||
const writeContent = (...args) => {
|
|
||||||
if (!isDry) {
|
|
||||||
fs.writeFileSync(...args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to print content
|
|
||||||
*/
|
|
||||||
const printContent = (...args) => {
|
|
||||||
if (isVerbose) {
|
|
||||||
console.log(...args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace all cases for the provided files
|
|
||||||
*/
|
|
||||||
const replaceCases = (fileType, files, replacementCases, printText) => {
|
|
||||||
files.forEach((file) => {
|
|
||||||
const matchedCases = [];
|
|
||||||
let content = fs.readFileSync(file, 'utf8');
|
|
||||||
|
|
||||||
replacementCases.forEach(([text, replacement, notes]) => {
|
|
||||||
// Simple text
|
|
||||||
if (typeof text === 'string') {
|
|
||||||
if (content.includes(text)) {
|
|
||||||
// Exclude cases without replacement
|
|
||||||
if (replacement) {
|
|
||||||
// Remove discontinued functionalities which do not break
|
|
||||||
content = content.replaceAll(text, replacement === removePlaceholder ? '' : replacement);
|
|
||||||
}
|
|
||||||
if (!matchedCases.includes(`${ text }, ${ replacement }, ${ notes }`)) {
|
|
||||||
matchedCases.push(`${ text }, ${ replacement }, ${ notes }`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Regex case
|
|
||||||
// TODO: Fix issue not replacing all
|
|
||||||
// eslint-disable-next-line no-lonely-if
|
|
||||||
if (text.test(content) && replacement) {
|
|
||||||
content = content.replace(new RegExp(text, 'g'), replacement);
|
|
||||||
if (!matchedCases.includes(`${ text }, ${ replacement }, ${ notes }`)) {
|
|
||||||
matchedCases.push(`${ text }, ${ replacement }, ${ notes }`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (matchedCases.length) {
|
|
||||||
writeContent(file, content);
|
|
||||||
printContent(file, printText, matchedCases);
|
|
||||||
stats[fileType].push(file);
|
|
||||||
stats.total.push(file);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Print log
|
|
||||||
*/
|
|
||||||
const printLog = () => {
|
|
||||||
if (process.argv.includes('--files')) {
|
|
||||||
console.dir(stats, { compact: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const statsCount = Object.entries(stats).reduce((acc, [key, value]) => ({
|
|
||||||
...acc,
|
|
||||||
[key]: value.length
|
|
||||||
}), {});
|
|
||||||
|
|
||||||
console.table(statsCount);
|
|
||||||
|
|
||||||
if (process.argv.includes('--log')) {
|
|
||||||
fs.writeFileSync('stats.json', JSON.stringify(stats, null, 2));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setParams = () => {
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
const paramKeys = ['paths'];
|
|
||||||
|
|
||||||
args.forEach((val) => {
|
|
||||||
paramKeys.forEach((key) => {
|
|
||||||
if (val.startsWith(`--${ key }=`)) {
|
|
||||||
params[key] = val.split('=')[1];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init application
|
|
||||||
*/
|
|
||||||
(function() {
|
|
||||||
setParams();
|
|
||||||
|
|
||||||
packageUpdates();
|
|
||||||
gitHubActionsUpdates();
|
|
||||||
nvmUpdates();
|
|
||||||
vueConfigUpdates();
|
|
||||||
vueSyntaxUpdates();
|
|
||||||
routerUpdates();
|
|
||||||
// jestUpdates();
|
|
||||||
jestConfigUpdates();
|
|
||||||
eslintUpdates();
|
|
||||||
tsUpdates();
|
|
||||||
stylesUpdates();
|
|
||||||
|
|
||||||
printLog();
|
|
||||||
})();
|
|
||||||
687
vue-migrate.js
687
vue-migrate.js
@ -1,687 +0,0 @@
|
|||||||
#!/usr/bin/node
|
|
||||||
/* eslint-disable no-console */
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const glob = require('glob');
|
|
||||||
const semver = require('semver');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init logger
|
|
||||||
*/
|
|
||||||
const stats = {
|
|
||||||
libraries: [],
|
|
||||||
node: [],
|
|
||||||
githubActions: [],
|
|
||||||
nvmrc: [],
|
|
||||||
webpack: [],
|
|
||||||
jest: [],
|
|
||||||
router: [],
|
|
||||||
resolution: [],
|
|
||||||
eslint: [],
|
|
||||||
vueSyntax: [],
|
|
||||||
style: [],
|
|
||||||
total: [],
|
|
||||||
};
|
|
||||||
const ignore = [
|
|
||||||
'**/node_modules/**',
|
|
||||||
'**/dist/**',
|
|
||||||
'**/scripts/vue-migrate.js',
|
|
||||||
'docusaurus/**',
|
|
||||||
'storybook-static/**',
|
|
||||||
'storybook/**',
|
|
||||||
];
|
|
||||||
const nodeRequirement = '20.0.0';
|
|
||||||
const isDry = process.argv.includes('--dry');
|
|
||||||
const isVerbose = process.argv.includes('--verbose');
|
|
||||||
const removePlaceholder = 'REMOVE';
|
|
||||||
const params = { paths: null };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Package updates
|
|
||||||
* Files: package.json
|
|
||||||
*/
|
|
||||||
const packageUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || '**/package.json', { ignore });
|
|
||||||
|
|
||||||
files.forEach((file) => {
|
|
||||||
let content = fs.readFileSync(file, 'utf8');
|
|
||||||
const toReplaceNode = false;
|
|
||||||
|
|
||||||
// TODO: Refactor and loop?
|
|
||||||
const [librariesContent, replaceLibraries] = packageUpdatesLibraries(file, content);
|
|
||||||
|
|
||||||
if (replaceLibraries.length) {
|
|
||||||
content = librariesContent;
|
|
||||||
printContent(file, `Updating`, replaceLibraries);
|
|
||||||
stats.libraries.push(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [nodeContent, replaceNode] = packageUpdatesEngine(file, content);
|
|
||||||
|
|
||||||
if (replaceNode.length) {
|
|
||||||
printContent(file, `Updating node`, replaceNode);
|
|
||||||
content = nodeContent;
|
|
||||||
stats.node.push(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [resolutionContent, replaceResolution] = packageUpdatesResolution(file, content);
|
|
||||||
|
|
||||||
if (replaceResolution.length) {
|
|
||||||
printContent(file, `Updating resolution`, replaceResolution);
|
|
||||||
content = resolutionContent;
|
|
||||||
stats.libraries.push(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (replaceLibraries || toReplaceNode || replaceResolution) {
|
|
||||||
stats.total.push(file);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify package vue related libraries versions
|
|
||||||
*/
|
|
||||||
const packageUpdatesLibraries = (file, oldContent) => {
|
|
||||||
let content = oldContent;
|
|
||||||
let parsedJson = JSON.parse(content);
|
|
||||||
const replaceLibraries = [];
|
|
||||||
const types = ['dependencies', 'devDependencies', 'peerDependencies'];
|
|
||||||
// [Library name, new version or new library, new library version]
|
|
||||||
const librariesUpdates = [
|
|
||||||
['@nuxt/babel-preset-app', removePlaceholder],
|
|
||||||
['@types/jest', '^29.5.2'],
|
|
||||||
['@typescript-eslint/eslint-plugin', '~5.4.0'],
|
|
||||||
['@typescript-eslint/parser', '~5.4.0'],
|
|
||||||
['@vue/cli-plugin-babel', '5.0.8'],
|
|
||||||
['@vue/cli-plugin-e2e-cypress', '5.0.8'],
|
|
||||||
['@vue/cli-plugin-eslint', '5.0.8'],
|
|
||||||
['@vue/cli-plugin-router', '5.0.8'],
|
|
||||||
['@vue/cli-plugin-typescript', '5.0.8'],
|
|
||||||
['@vue/cli-plugin-unit-jest', '5.0.8'],
|
|
||||||
['@vue/cli-plugin-vuex', '5.0.8'],
|
|
||||||
['@vue/cli-service', '5.0.8'],
|
|
||||||
['@vue/eslint-config-typescript', '~9.1.0'],
|
|
||||||
['@vue/vue2-jest', '@vue/vue3-jest', '^27.0.0-alpha.1'],
|
|
||||||
['@vue/test-utils', '~2.0.0-0'],
|
|
||||||
['core-js', '3.25.3'],
|
|
||||||
['cache-loader', '^4.1.0'],
|
|
||||||
['node-polyfill-webpack-plugin', '^3.0.0'],
|
|
||||||
['portal-vue', '~3.0.0'],
|
|
||||||
['require-extension-hooks-babel', '1.0.0'],
|
|
||||||
['require-extension-hooks-vue', '3.0.0'],
|
|
||||||
['require-extension-hooks', '0.3.3'],
|
|
||||||
['sass-loader', '~12.0.0'],
|
|
||||||
['typescript', '~4.5.5'],
|
|
||||||
['vue-router', '~4.0.3'],
|
|
||||||
['vue3-virtual-scroll-list', 'vue3-virtual-scroll-list', '0.2.1'],
|
|
||||||
['vue', '~3.2.13'],
|
|
||||||
['vuex', '~4.0.0'],
|
|
||||||
['xterm', '5.2.1'],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Loop through each type of dependencies since many often not correctly placed or hard to track
|
|
||||||
types.forEach((type) => {
|
|
||||||
if (parsedJson[type]) {
|
|
||||||
librariesUpdates.forEach(([library, newVersion, newLibraryVersion]) => {
|
|
||||||
if (parsedJson[type][library]) {
|
|
||||||
const version = semver.coerce(parsedJson[type][library]);
|
|
||||||
|
|
||||||
if (newVersion === removePlaceholder) {
|
|
||||||
// Remove library
|
|
||||||
replaceLibraries.push([library, [parsedJson[type][library], removePlaceholder]]);
|
|
||||||
delete parsedJson[type][library];
|
|
||||||
content = JSON.stringify(parsedJson, null, 2);
|
|
||||||
writeContent(file, content);
|
|
||||||
} else if (newLibraryVersion) {
|
|
||||||
// Replace with a new library if present, due breaking changes in Vue3
|
|
||||||
replaceLibraries.push([library, [parsedJson[type][library], newVersion, newLibraryVersion]]);
|
|
||||||
content = content.replaceAll(`"${ library }": "${ parsedJson[type][library] }"`, `"${ newVersion }": "${ newLibraryVersion }"`);
|
|
||||||
parsedJson = JSON.parse(content);
|
|
||||||
writeContent(file, content);
|
|
||||||
} else if (version && semver.lt(version, semver.coerce(newVersion))) {
|
|
||||||
// Update library version if outdated
|
|
||||||
replaceLibraries.push([library, [parsedJson[type][library], newVersion]]);
|
|
||||||
content = content.replaceAll(`"${ library }": "${ parsedJson[type][library] }"`, `"${ library }": "${ newVersion }"`);
|
|
||||||
parsedJson = JSON.parse(content);
|
|
||||||
writeContent(file, content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return [content, replaceLibraries];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify package engines node to latest
|
|
||||||
*/
|
|
||||||
const packageUpdatesEngine = (file, oldContent) => {
|
|
||||||
let content = oldContent;
|
|
||||||
let parsedJson = JSON.parse(content);
|
|
||||||
const replaceNode = [];
|
|
||||||
|
|
||||||
// Verify package engines node to latest
|
|
||||||
if (parsedJson.engines) {
|
|
||||||
const outdated = semver.lt(semver.coerce(parsedJson.engines.node), semver.coerce(nodeRequirement));
|
|
||||||
|
|
||||||
if (outdated) {
|
|
||||||
replaceNode.push([parsedJson.engines.node, nodeRequirement]);
|
|
||||||
content = content.replaceAll(`"node": "${ parsedJson.engines.node }"`, `"node": ">=${ nodeRequirement }"`);
|
|
||||||
parsedJson = JSON.parse(content);
|
|
||||||
writeContent(file, content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [content, replaceNode];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add resolutions for VueCLI
|
|
||||||
*/
|
|
||||||
const packageUpdatesResolution = (file, oldContent) => {
|
|
||||||
let content = oldContent;
|
|
||||||
let parsedJson = JSON.parse(content);
|
|
||||||
const replaceResolution = [];
|
|
||||||
const resolutions = [
|
|
||||||
['@vue/cli-service/html-webpack-plugin', '^5.0.0'],
|
|
||||||
['**/webpack', removePlaceholder],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Verify package engines node to latest
|
|
||||||
if (parsedJson.resolutions) {
|
|
||||||
resolutions.forEach(([library, newVersion]) => {
|
|
||||||
if (newVersion === removePlaceholder) {
|
|
||||||
delete parsedJson.resolutions[library];
|
|
||||||
content = JSON.stringify(parsedJson, null, 2);
|
|
||||||
parsedJson = JSON.parse(content);
|
|
||||||
writeContent(file, content);
|
|
||||||
} else if (!parsedJson.resolutions[library]) {
|
|
||||||
// Add resolution if not present
|
|
||||||
parsedJson.resolutions[library] = newVersion;
|
|
||||||
content = JSON.stringify(parsedJson, null, 2);
|
|
||||||
parsedJson = JSON.parse(content);
|
|
||||||
writeContent(file, content);
|
|
||||||
} else {
|
|
||||||
// Ensure resolution version is up to date
|
|
||||||
const outdated = semver.lt(semver.coerce(parsedJson.resolutions[library]), semver.coerce(newVersion));
|
|
||||||
|
|
||||||
if (outdated) {
|
|
||||||
replaceResolution.push([parsedJson.engines.node, nodeRequirement]);
|
|
||||||
content = content.replaceAll(`"${ library }": "${ parsedJson.resolutions[library] }"`, `"${ library }": "${ newVersion }"`);
|
|
||||||
parsedJson = JSON.parse(content);
|
|
||||||
writeContent(file, content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return [content, replaceResolution];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GitHub Actions updates
|
|
||||||
* Files: .github/workflows/**.yml
|
|
||||||
*
|
|
||||||
* Verify GitHub Actions use of current node version, e.g. node-version: '<18'
|
|
||||||
*/
|
|
||||||
const gitHubActionsUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || '.github/{actions,workflows}/**.{yml,yaml}', { ignore });
|
|
||||||
|
|
||||||
files.forEach((file) => {
|
|
||||||
let content = fs.readFileSync(file, 'utf8');
|
|
||||||
const nodeVersionMatches = content.matchAll(/node-version: \'([0-9.x]+)\'/g);
|
|
||||||
const toReplace = [];
|
|
||||||
|
|
||||||
// Check all the node occurrences within the test file
|
|
||||||
if (nodeVersionMatches) {
|
|
||||||
for (const matches of nodeVersionMatches) {
|
|
||||||
for (const match of matches) {
|
|
||||||
const nodeVersion = semver.coerce(match);
|
|
||||||
|
|
||||||
if (nodeVersion && semver.lt(nodeVersion, semver.coerce(nodeRequirement))) {
|
|
||||||
content = content.replaceAll(`node-version: '${ match }'`, `node-version: '20.x'`);
|
|
||||||
|
|
||||||
writeContent(file, content);
|
|
||||||
toReplace.push([match, nodeRequirement]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toReplace.length) {
|
|
||||||
printContent(file, `Updating node`, toReplace);
|
|
||||||
stats.githubActions.push(file);
|
|
||||||
stats.total.push(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NVM updates
|
|
||||||
* Files: .nvmrc
|
|
||||||
*
|
|
||||||
* Verify presence of .nvmrc, create one if none, update if any
|
|
||||||
*/
|
|
||||||
const nvmUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || '**/.nvmrc', { ignore });
|
|
||||||
const nvmRequirement = 20;
|
|
||||||
|
|
||||||
files.forEach((file) => {
|
|
||||||
if (file) {
|
|
||||||
let content = fs.readFileSync(file, 'utf8');
|
|
||||||
const nodeVersionMatch = content.match(/([0-9.x]+)/g);
|
|
||||||
const nodeVersion = semver.coerce(nodeVersionMatch[0]);
|
|
||||||
|
|
||||||
// Ensure node version is up to date
|
|
||||||
if (nodeVersion && semver.lt(nodeVersion, semver.coerce(nodeRequirement))) {
|
|
||||||
printContent(file, `Updating node ${ [nodeVersionMatch[0], nvmRequirement] }`);
|
|
||||||
content = content.replaceAll(nodeVersionMatch[0], nvmRequirement);
|
|
||||||
|
|
||||||
writeContent(file, content);
|
|
||||||
stats.nvmrc.push(file);
|
|
||||||
stats.total.push(file);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
writeContent('.nvmrc', nvmRequirement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vue config update
|
|
||||||
* Files: vue.config.js
|
|
||||||
*
|
|
||||||
* Verify vue.config presence of deprecated Webpack5 options
|
|
||||||
* - devServer.public: 'path' -> client: { webSocketURL: 'path' }
|
|
||||||
*/
|
|
||||||
const vueConfigUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || 'vue.config**.js', { ignore });
|
|
||||||
|
|
||||||
files.forEach((file) => {
|
|
||||||
const content = fs.readFileSync(file, 'utf8');
|
|
||||||
|
|
||||||
// Verify vue.config presence of deprecated Webpack5 options
|
|
||||||
if (content.includes('devServer.public: \'path\'')) {
|
|
||||||
stats.webpack.push(file);
|
|
||||||
stats.total.push(file);
|
|
||||||
// TODO: Add replacement
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vue syntax update (to do not mix with tests)
|
|
||||||
* Files: .vue, .js, .ts (not .spec.ts, not .test.ts)
|
|
||||||
*/
|
|
||||||
const vueSyntaxUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || '**/*.{vue,js,ts}', { ignore: [...ignore, '**/*.spec.ts', '**/__tests__/**', '**/*.test.ts', 'jest.setup.js', '**/*.d.ts', '**/vue-shim.ts'] });
|
|
||||||
const replacementCases = [
|
|
||||||
// Prioritize set and delete to be converted since removed in Vue3
|
|
||||||
[/\=\> Vue\.set\((.*?),\s*(.*?),\s*(.*?)\)/g, (_, obj, prop, val) => `=> (${ obj.trim() }[${ prop.trim() }] = ${ val.trim() })`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/\=\> Vue\.set\((.*?),\s*'([^']*?)',\s*\{([\s\S]*?)\}\)/g, (_, obj, prop, val) => `=> (${ obj.trim() }['${ prop }'] = {${ val.trim() }})`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/\=\> Vue\.set\((.*?),\s*'([^']*?)',\s*(.*?)\)/g, (_, obj, prop, val) => `=> (${ obj.trim() }.${ prop } = ${ val.trim() })`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
|
|
||||||
[/Vue\.set\((.*?),\s*(.*?),\s*(.*?)\)/g, (_, obj, prop, val) => `${ obj.trim() }[${ prop.trim() }] = ${ val.trim() }`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/Vue\.set\((.*?),\s*'([^']*?)',\s*\{([\s\S]*?)\}\)/g, (_, obj, prop, val) => `${ obj.trim() }['${ prop }'] = {${ val.trim() }}`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/Vue\.set\((.*?),\s*'([^']*?)',\s*(.*?)\)/g, (_, obj, prop, val) => `${ obj.trim() }.${ prop } = ${ val.trim() }`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/Vue\.delete\((.*?),\s*(.*?)\)/g, (_, obj, prop) => `delete ${ obj.trim() }[${ prop.trim() }]`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
|
|
||||||
[/\=\> this\.\$set\((.*?),\s*(.*?),\s*(.*?)\)/g, (_, obj, prop, val) => `=> (${ obj.trim() }[${ prop.trim() }] = ${ val.trim() })`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/\=\> this\.\$set\((.*?),\s*'([^']*?)',\s*\{([\s\S]*?)\}\)/g, (_, obj, prop, val) => `=> (${ obj.trim() }['${ prop }'] = {${ val.trim() }})`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/\=\> this\.\$set\((.*?),\s*'([^']*?)',\s*(.*?)\)/g, (_, obj, prop, val) => `=> (${ obj.trim() }.${ prop } = ${ val.trim() })`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
|
|
||||||
[/this.\$set\((.*?),\s*'([^']*?)',\s*(.*?)\)/g, (_, obj, prop, val) => obj.trim() === 'this' ? `this['${ prop }'] = ${ val }` : `${ obj.trim() }['${ prop }'] = ${ val }`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
|
|
||||||
[/this\.\$set\((.*?),\s*(.*?),\s*(.*?)\)/g, (_, obj, prop, val) => `${ obj.trim() }[${ prop.trim() }] = ${ val.trim() }`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/this\.\$set\((.*?),\s*'([^']*?)',\s*\{([\s\S]*?)\}\)/g, (_, obj, prop, val) => `${ obj.trim() }['${ prop }'] = {${ val.trim() }}`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/this\.\$set\((.*?),\s*'([^']*?)',\s*(.*?)\)/g, (_, obj, prop, val) => `${ obj.trim() }.${ prop } = ${ val.trim() }`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
[/this\.\$delete\((.*?),\s*(.*?)\)/g, (_, obj, prop) => `delete ${ obj.trim() }[${ prop.trim() }]`, 'removed and unnecessary due new reactivity https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
||||||
|
|
||||||
// Replace imports for all the cases where createApp is needed, before the rest of the replacements
|
|
||||||
[/import { createApp } from 'vue';
|
|
||||||
const vueApp = createApp({});?/g, `import { createApp } from \'vue\';\nconst vueApp = createApp({});`, `https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp`],
|
|
||||||
[`createApp(`, `createApp(`, `https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp`],
|
|
||||||
[`vueApp.config`, `vueApp.config`, `https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp`],
|
|
||||||
[`vueApp.directive`, `vueApp.directive`, `https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp`],
|
|
||||||
[`vueApp.filter(`, `vueApp.filter(`, `https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp`],
|
|
||||||
[`vueApp.mixin(`, `vueApp.mixin(`, `https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp`],
|
|
||||||
[`vueApp.component(`, `vueApp.component(`, `https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp`],
|
|
||||||
[`vueApp.use(`, `vueApp.use(`, `https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp`],
|
|
||||||
[`vueApp.config.globalProperties`, `vueApp.config.globalProperties`, `https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp`],
|
|
||||||
['Vue.util', '', 'Vue.util is private and no longer available https://v3-migration.vuejs.org/migration-build.html#partially-compatible-with-caveats'],
|
|
||||||
// [`Vue.extend`, removePlaceholder, 'https://v3-migration.vuejs.org/breaking-changes/global-api.html#vue-extend-removed'],
|
|
||||||
// [`Vue.extend`, `createApp({})`], // (mixins)
|
|
||||||
|
|
||||||
[`vue3-virtual-scroll-list`, `vue3-virtual-scroll-list`, 'library update'],
|
|
||||||
|
|
||||||
[`nextTick`, `nextTick`, 'https://v3-migration.vuejs.org/breaking-changes/global-api-treeshaking.html#global-api-treeshaking'],
|
|
||||||
[`nextTick`, `nextTick`, 'https://v3-migration.vuejs.org/breaking-changes/global-api-treeshaking.html#global-api-treeshaking'],
|
|
||||||
// TODO: Add missing import
|
|
||||||
|
|
||||||
[/( {4,}default)\(\)\s*\{([\s\S]*?)this\.([\s\S]*?\}\s*\})/g, (_, before, middle, after) => `${ before }(props) {${ middle }props.${ after }`, 'https://v3-migration.vuejs.org/breaking-changes/props-default-this.html'],
|
|
||||||
// [`value=`, `modelValue=`],
|
|
||||||
// [`@input=`, `@update:modelValue=`],
|
|
||||||
[/\@input=\"((?!.*plainInputEvent).+)\"/g, (_, betweenQuotes) => `@update:value="${ betweenQuotes }"`], // Matches @input while avoiding `@input="($plainInputEvent) => onInput($plainInputEvent)"` which we used on plain <input elements since they differ in vue3
|
|
||||||
[`v-model:value=`, `v-model:value=`],
|
|
||||||
// [`v-bind.sync=`, `:modelValue=`, `https://v3-migration.vuejs.org/breaking-changes/v-model.html#using-v-bind-sync`],
|
|
||||||
// ['v-model:value=', ':modelValue=', ''],
|
|
||||||
[/:([a-z-0-9]+)\.sync/g, (_, propName) => `v-model:${ propName }`, `https://v3-migration.vuejs.org/breaking-changes/v-model.html#migration-strategy`],
|
|
||||||
[`click`, `click`, `https://v3-migration.vuejs.org/breaking-changes/v-model.html#using-v-bind-sync`],
|
|
||||||
[``, removePlaceholder, `removed and integrated with $attrs https://v3-migration.vuejs.org/breaking-changes/listeners-removed.html`],
|
|
||||||
[`:v-bind="$attrs"`, `:v-bind="$attrs"`, `removed and integrated with $attrs https://v3-migration.vuejs.org/breaking-changes/listeners-removed.html`],
|
|
||||||
|
|
||||||
[/this\.\$scopedSlots\[(\w+)\]|this\.\$scopedSlots\.(\w+)/, (match, key1, key2) => `this.$slots.${ key1 || key2 }()`, `(many components loop through them) https://v3-migration.vuejs.org/breaking-changes/slots-unification.html`],
|
|
||||||
[` $slots`, ` $slots`, `(many components loop through them) https://v3-migration.vuejs.org/breaking-changes/slots-unification.html`],
|
|
||||||
[/slot="(\w+:\w+)"\s+slot-scope="(\w+)"/g, `$1="$2"`, `not mentioned in migration https://vuejs.org/guide/components/slots.html#scoped-slots`],
|
|
||||||
[/this\.\$slots\['([^']+)'\]/g, `this.$slots[\'$1\']()`, `not mentioned in migration https://vuejs.org/guide/components/slots.html#scoped-slots`],
|
|
||||||
// [/this\.\$slots\.([^']+)'/g, `this.$slots.$1()`, `not mentioned in migration https://vuejs.org/guide/components/slots.html#scoped-slots`],
|
|
||||||
// [/this\.\$slots(?!\s*\(\))(\b|\?|['"\[])/g, `this.$slots()$1`, `https://eslint.vuejs.org/rules/require-slots-as-functions.html`], // TODO: Add exception for existing brackets
|
|
||||||
|
|
||||||
// Portals are now Vue3 Teleports
|
|
||||||
[/<portal|<portal-target|<\/portal|<\/portal-target/g, '', `https://v3.vuejs.org/guide/teleport.html`],
|
|
||||||
|
|
||||||
// TODO: probably requires JSDom
|
|
||||||
// [/<template v-for="([\s\S]*?)">\s*<([\s\S]*?)\s*([\s\S]*?):key="([\s\S]*?)"([\s\S]*?)<\/([\s\S]*?)>\s*<\/template>/gs, '<template v-for="$1" :key="$4"><$3 $5>$6</$7>\n</template>', `https://v3-migration.vuejs.org/breaking-changes/key-attribute.html#with-template-v-for`],
|
|
||||||
// [/(<\w+(?!.*?v-for=)[^>]*?)\s*:key="[^"]*"\s*([^>]*>)/g, '$1$2', `https://v3-migration.vuejs.org/breaking-changes/key-attribute.html#with-template-v-for`],
|
|
||||||
[/(<\w+[^>v\-for]*?):key="[^"]*"\s*([^>]*>)/g, '$1$2', `https://v3-migration.vuejs.org/breaking-changes/key-attribute.html#with-template-v-for`],
|
|
||||||
[/(\<\w+\s+(?:[^>]*?\s+)?v-for="\(.*?,\s*(\w+)\s*\).*?")(?:\s*:key=".*"\s*)?([^>]*>)/g, '$1 :key="$2"$3', `https://v3-migration.vuejs.org/breaking-changes/key-attribute.html#with-template-v-for`],
|
|
||||||
[/(\<\w+\s+(?:[^>]*?\s+)?)v-for="(?!\*?\s+.*?\s+.*?)([^,]+?)\s+in\s+([^"]+?)"(?:\s*:key=".*"\s*)?([^>]*>)/g, '$1 v-for="($2, i) in $3" :key="i" $4', `https://v3-migration.vuejs.org/breaking-changes/key-attribute.html#with-template-v-for`],
|
|
||||||
|
|
||||||
// TODO: except for <components /> elements, probably requires JSDom
|
|
||||||
// [' is=', ``, `https://v3-migration.vuejs.org/breaking-changes/custom-elements-interop.html#customized-built-in-elements`],
|
|
||||||
// [' :is=', ``, `https://v3-migration.vuejs.org/breaking-changes/custom-elements-interop.html#customized-built-in-elements`],
|
|
||||||
|
|
||||||
// Directive updates
|
|
||||||
// [`bind(`, '', `beforeMount( but there's too many bind cases https://v3-migration.vuejs.org/breaking-changes/custom-directives.html`], // TODO: Restrict to directives and context
|
|
||||||
// [`update(`, '', `removed, also common term https://v3-migration.vuejs.org/breaking-changes/custom-directives.html`], // TODO: Restrict to directives and context
|
|
||||||
[`mounted(`, `mounted(`, 'https://v3-migration.vuejs.org/breaking-changes/custom-directives.html'],
|
|
||||||
[`updated(`, `updated(`, 'https://v3-migration.vuejs.org/breaking-changes/custom-directives.html'],
|
|
||||||
[`unmounted`, `unmounted`, 'https://v3-migration.vuejs.org/breaking-changes/custom-directives.html'],
|
|
||||||
|
|
||||||
// [`propsData (app creation)`, ``, `use second argument of createApp({}) https://v3-migration.vuejs.org/breaking-changes/props-data.html`],
|
|
||||||
[`@vue:lifecycleHook`, `@vue:lifecycleHook`, `https://v3-migration.vuejs.org/breaking-changes/vnode-lifecycle-events.html`],
|
|
||||||
|
|
||||||
// Nuxt and initalize case only
|
|
||||||
// TODO: Use eventbus replacement as temporary solution?
|
|
||||||
[`$on('event', callback)`, '', `no migration, existing lib, https://v3-migration.vuejs.org/breaking-changes/events-api.html`],
|
|
||||||
[`$off('event', callback)`, '', `no migration, existing lib, https://v3-migration.vuejs.org/breaking-changes/events-api.html`],
|
|
||||||
[`$once('event', callback)`, '', `no migration, existing lib, https://v3-migration.vuejs.org/breaking-changes/events-api.html`],
|
|
||||||
|
|
||||||
// [`$children`, ``, `no migration, $refs are suggested as communication https://v3-migration.vuejs.org/breaking-changes/children.html`],
|
|
||||||
|
|
||||||
// Vuex
|
|
||||||
[`createStore(`, `createStore(`, 'To install Vuex to a Vue instance, pass the store instead of Vuex https://vuex.vuejs.org/guide/migrating-to-4-0-from-3-x.html#installation-process'],
|
|
||||||
[`import { createStore } from 'vuex'`, `import { createStore } from 'vuex'`, 'To install Vuex to a Vue instance, pass the store instead of Vuex https://vuex.vuejs.org/guide/migrating-to-4-0-from-3-x.html#installation-process'],
|
|
||||||
|
|
||||||
// Extra cases TBD (it seems like we already use the suggested way for arrays)
|
|
||||||
// watch option used on arrays not triggered by mutations https://v3-migration.vuejs.org/breaking-changes/watch.html
|
|
||||||
];
|
|
||||||
|
|
||||||
replaceCases('vueSyntax', files, replacementCases, `Updating Vue syntax`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vue Router
|
|
||||||
* Files: .vue, .js, .ts
|
|
||||||
*/
|
|
||||||
const routerUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || '**/*.{vue,js,ts}', { ignore });
|
|
||||||
const replacementCases = [
|
|
||||||
[`import { createRouter } from 'vue-router'`, `import { createRouter } from 'vue-router'`],
|
|
||||||
[`vueApp.use(Router)`, `const router = createRouter({})`],
|
|
||||||
// [`currentRoute`, '', 'The currentRoute property is now a ref() https://router.vuejs.org/guide/migration/#The-currentRoute-property-is-now-a-ref-'],
|
|
||||||
[/import\s*\{([^}]*)\s* RouteConfig\s*([^}]*)\}\s*from\s*'vue-router'/g, (match, before, after) => `import {${ before.trim() } RouteRecordRaw ${ after.trim() }} from 'vue-router'`],
|
|
||||||
[/import\s*\{([^}]*)\s* Location\s*([^}]*)\}\s*from\s*'vue-router'/g, (match, before, after) => `import {${ before.trim() } RouteLocation ${ after.trim() }} from 'vue-router'`],
|
|
||||||
['imported Router', ''],
|
|
||||||
['router.name', '', 'now string | Symbol'],
|
|
||||||
[`mode: \'history\'`, 'history: createWebHistory()'],
|
|
||||||
// ['getMatchedComponents', '', 'https://router.vuejs.org/guide/migration/#Removal-of-router-getMatchedComponents-'],
|
|
||||||
];
|
|
||||||
|
|
||||||
replaceCases('router', files, replacementCases, `Updating Vue Router`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Jest update
|
|
||||||
* https://test-utils.vuejs.org/migration
|
|
||||||
* Files: .spec.js, .spec.ts, .test.js, .test.ts
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const jestUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || '**/*.{test.js,test.ts}', { ignore });
|
|
||||||
|
|
||||||
const cases = [
|
|
||||||
['config.mocks.$myGlobal', '', ''],
|
|
||||||
['createLocalVue', '', 'https://test-utils.vuejs.org/migration/#No-more-createLocalVue'],
|
|
||||||
['new Vuex.Store', '', ''],
|
|
||||||
['store', '', ''],
|
|
||||||
['propsData', 'props', 'https://test-utils.vuejs.org/migration/#propsData-is-now-props'],
|
|
||||||
['localVue.extend({})', '', ''],
|
|
||||||
['nextTick', '', ''],
|
|
||||||
['$destroy', '$unmount', 'https://test-utils.vuejs.org/migration/#destroy-is-now-unmount-to-match-Vue-3'],
|
|
||||||
['mocks', '', 'https://test-utils.vuejs.org/migration/#mocks-and-stubs-are-now-in-global'],
|
|
||||||
['stubs', '', 'https://test-utils.vuejs.org/migration/#mocks-and-stubs-are-now-in-global'],
|
|
||||||
['mixins', '', 'https://test-utils.vuejs.org/migration/#mocks-and-stubs-are-now-in-global'],
|
|
||||||
['plugins', '', 'https://test-utils.vuejs.org/migration/#mocks-and-stubs-are-now-in-global'],
|
|
||||||
['component', '', 'https://test-utils.vuejs.org/migration/#mocks-and-stubs-are-now-in-global'],
|
|
||||||
['directives', '', 'https://test-utils.vuejs.org/migration/#mocks-and-stubs-are-now-in-global'],
|
|
||||||
['slots', '', 'slots‘s scope is now exposed as params https://test-utils.vuejs.org/migration/#slots-s-scope-is-now-exposed-as-params'],
|
|
||||||
['scopedSlots', '', 'scopedSlots is now merged with slots https://test-utils.vuejs.org/migration/#scopedSlots-is-now-merged-with-slots'],
|
|
||||||
['parentComponent', '', 'deprecated '],
|
|
||||||
['contains', 'find', 'deprecated '],
|
|
||||||
['config.global.renderStubDefaultSlot = false', '', ''],
|
|
||||||
['findAll().at(0)', '', '']
|
|
||||||
];
|
|
||||||
|
|
||||||
replaceCases('jest', files, cases, `Updating Jest`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Jest config updates
|
|
||||||
* Files: jest.config.js, .json, .ts
|
|
||||||
*
|
|
||||||
* /node_modules/@vue/vue2-jest --> reference needs new library version
|
|
||||||
*/
|
|
||||||
const jestConfigUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || '**/jest.config.{js,ts,json}', { ignore });
|
|
||||||
const cases = [
|
|
||||||
['/node_modules/@vue/vue2-jest', '/node_modules/@vue/vue3-jest']
|
|
||||||
];
|
|
||||||
|
|
||||||
replaceCases('jest', files, cases, `Updating Jest config`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ESLint Updates
|
|
||||||
* Files: .eslintrc.js, .eslintrc.json, .eslintrc.yml
|
|
||||||
*/
|
|
||||||
const eslintUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || '**/.eslintrc.*{js,json,yml}', { ignore });
|
|
||||||
// Add cases introduced with new recommended settings
|
|
||||||
const replacePlugins = [
|
|
||||||
['plugin:vue/essential', 'plugin:vue/vue3-essential'],
|
|
||||||
['plugin:vue/strongly-recommended', 'plugin:vue/vue3-strongly-recommended'],
|
|
||||||
['plugin:vue/recommended', 'plugin:vue/vue3-recommended']
|
|
||||||
];
|
|
||||||
const newRules = {
|
|
||||||
'vue/one-component-per-file': 'off',
|
|
||||||
'vue/no-deprecated-slot-attribute': 'off',
|
|
||||||
'vue/require-explicit-emits': 'off',
|
|
||||||
'vue/v-on-event-hyphenation': 'off',
|
|
||||||
};
|
|
||||||
|
|
||||||
files.forEach((file) => {
|
|
||||||
let content = fs.readFileSync(file, 'utf8');
|
|
||||||
const matchedCases = [];
|
|
||||||
|
|
||||||
replacePlugins.forEach(([text, replacement]) => {
|
|
||||||
const isCase = content.includes(text);
|
|
||||||
|
|
||||||
if (isCase) {
|
|
||||||
content = content.replaceAll(text, replacement);
|
|
||||||
matchedCases.push([text, replacement]);
|
|
||||||
|
|
||||||
writeContent(file, content);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add the new rules if they don't exist
|
|
||||||
const eslintConfigPath = path.join(process.cwd(), `${ file }`);
|
|
||||||
const eslintConfig = require(eslintConfigPath);
|
|
||||||
|
|
||||||
Object.keys(newRules).forEach((rule) => {
|
|
||||||
if (!eslintConfig.rules[rule]) {
|
|
||||||
eslintConfig.rules[rule] = newRules[rule];
|
|
||||||
matchedCases.push(rule);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
writeContent(eslintConfigPath, `module.exports = ${ JSON.stringify(eslintConfig, null, 2) }`);
|
|
||||||
|
|
||||||
if (matchedCases.length) {
|
|
||||||
printContent(file, `Updating ESLint`, matchedCases);
|
|
||||||
stats.eslint.push(file);
|
|
||||||
stats.total.push(file);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TS Updates
|
|
||||||
* Files: tsconfig*.json
|
|
||||||
*
|
|
||||||
* Add information about TS issues, recommend @ts-nocheck as temporary solution
|
|
||||||
*/
|
|
||||||
const tsUpdates = () => {
|
|
||||||
console.warn('TS checks are stricter and may require to be fixed manually.',
|
|
||||||
'Use @ts-nocheck to give you time to fix them.',
|
|
||||||
'Add exception to your ESLint config to avoid linting errors.');
|
|
||||||
// TODO: Add case
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Styles updates
|
|
||||||
*/
|
|
||||||
const stylesUpdates = () => {
|
|
||||||
const files = glob.sync(params.paths || '**/*.{vue, scss}', { ignore });
|
|
||||||
const cases = [
|
|
||||||
['::v-deep', ':deep()'],
|
|
||||||
];
|
|
||||||
|
|
||||||
replaceCases('style', files, cases, `Updating styles`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to write content
|
|
||||||
*/
|
|
||||||
const writeContent = (...args) => {
|
|
||||||
if (!isDry) {
|
|
||||||
fs.writeFileSync(...args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to print content
|
|
||||||
*/
|
|
||||||
const printContent = (...args) => {
|
|
||||||
if (isVerbose) {
|
|
||||||
console.log(...args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace all cases for the provided files
|
|
||||||
*/
|
|
||||||
const replaceCases = (fileType, files, replacementCases, printText) => {
|
|
||||||
files.forEach((file) => {
|
|
||||||
const matchedCases = [];
|
|
||||||
let content = fs.readFileSync(file, 'utf8');
|
|
||||||
|
|
||||||
replacementCases.forEach(([text, replacement, notes]) => {
|
|
||||||
// Simple text
|
|
||||||
if (typeof text === 'string') {
|
|
||||||
if (content.includes(text)) {
|
|
||||||
// Exclude cases without replacement
|
|
||||||
if (replacement) {
|
|
||||||
// Remove discontinued functionalities which do not break
|
|
||||||
content = content.replaceAll(text, replacement === removePlaceholder ? '' : replacement);
|
|
||||||
}
|
|
||||||
if (!matchedCases.includes(`${ text }, ${ replacement }, ${ notes }`)) {
|
|
||||||
matchedCases.push(`${ text }, ${ replacement }, ${ notes }`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Regex case
|
|
||||||
// TODO: Fix issue not replacing all
|
|
||||||
if (text.test(content) && replacement) {
|
|
||||||
content = content.replace(new RegExp(text, 'g'), replacement);
|
|
||||||
if (!matchedCases.includes(`${ text }, ${ replacement }, ${ notes }`)) {
|
|
||||||
matchedCases.push(`${ text }, ${ replacement }, ${ notes }`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (matchedCases.length) {
|
|
||||||
writeContent(file, content);
|
|
||||||
printContent(file, printText, matchedCases);
|
|
||||||
stats[fileType].push(file);
|
|
||||||
stats.total.push(file);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Print log
|
|
||||||
*/
|
|
||||||
const printLog = () => {
|
|
||||||
if (process.argv.includes('--files')) {
|
|
||||||
console.dir(stats, { compact: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const statsCount = Object.entries(stats).reduce((acc, [key, value]) => ({
|
|
||||||
...acc,
|
|
||||||
[key]: value.length
|
|
||||||
}), {});
|
|
||||||
|
|
||||||
console.table(statsCount);
|
|
||||||
|
|
||||||
if (process.argv.includes('--log')) {
|
|
||||||
fs.writeFileSync('stats.json', JSON.stringify(stats, null, 2));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setParams = () => {
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
const paramKeys = ['paths'];
|
|
||||||
|
|
||||||
args.forEach((val) => {
|
|
||||||
paramKeys.forEach((key) => {
|
|
||||||
if (val.startsWith(`--${ key }=`)) {
|
|
||||||
params[key] = val.split('=')[1];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init application
|
|
||||||
*/
|
|
||||||
(function() {
|
|
||||||
setParams();
|
|
||||||
|
|
||||||
packageUpdates();
|
|
||||||
gitHubActionsUpdates();
|
|
||||||
nvmUpdates();
|
|
||||||
vueConfigUpdates();
|
|
||||||
vueSyntaxUpdates();
|
|
||||||
routerUpdates();
|
|
||||||
// jestUpdates();
|
|
||||||
jestConfigUpdates();
|
|
||||||
eslintUpdates();
|
|
||||||
tsUpdates();
|
|
||||||
stylesUpdates();
|
|
||||||
|
|
||||||
printLog();
|
|
||||||
})();
|
|
||||||
Loading…
x
Reference in New Issue
Block a user