mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-01 03:36:04 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c8033dfdd | ||
|
|
e9a9035cb8 | ||
|
|
1ff6137a9d | ||
|
|
311de06483 | ||
|
|
37ddf83f42 | ||
|
|
76bd9195f9 | ||
|
|
50d66a521d | ||
|
|
c81d2a87fd | ||
|
|
2e996d95da | ||
|
|
bdfa44186d | ||
|
|
ab2aa79037 | ||
|
|
cd86909b73 | ||
|
|
c73aca870f | ||
|
|
447be1f75d | ||
|
|
d958f443bf | ||
|
|
c43c3915b7 | ||
|
|
2e3547d0a2 | ||
|
|
1cbb6a6d54 | ||
|
|
3a3f8f60dc | ||
|
|
f7d2f1c586 | ||
|
|
4d4e3699a8 | ||
|
|
9423cf4c11 | ||
|
|
73b6193bf7 | ||
|
|
21ff6b0307 | ||
|
|
b887e5ee01 | ||
|
|
bf9b5866a7 | ||
|
|
b97017e076 | ||
|
|
3740c9c424 | ||
|
|
af8e8b2eeb | ||
|
|
7906033287 | ||
|
|
5584c86789 | ||
|
|
1dcb528717 | ||
|
|
36fc21ccbc | ||
|
|
8803b79524 | ||
|
|
3f80a71303 | ||
|
|
13e4154335 | ||
|
|
a80b87ce4f | ||
|
|
809b9925fa | ||
|
|
7788161d94 | ||
|
|
90b8a821a1 | ||
|
|
87ff014bd8 | ||
|
|
20328b06e6 | ||
|
|
bbc415a2b4 | ||
|
|
c12d4b5fba | ||
|
|
dc91be11f9 | ||
|
|
e5b72499b5 | ||
|
|
e66037b007 | ||
|
|
c676cad057 | ||
|
|
254abd4648 | ||
|
|
1df1d3534a | ||
|
|
ff1e6e6e7f | ||
|
|
622242e4a5 | ||
|
|
d8ea9be174 | ||
|
|
f8a479cfcf | ||
|
|
6d5e584113 | ||
|
|
a9eb04195e | ||
|
|
0441c1f6e3 | ||
|
|
dde58269d5 | ||
|
|
b8111f0ad7 | ||
|
|
6d627f82e9 | ||
|
|
cfc7a76fe7 | ||
|
|
71d3067354 | ||
|
|
9ecc372009 |
+4
-13
@@ -39,20 +39,11 @@
|
||||
"reviewers": ["a110605", "houhoucoop"]
|
||||
},
|
||||
{
|
||||
"matchUpdateTypes": ["patch"],
|
||||
"automerge": false,
|
||||
"matchUpdateTypes": ["patch", "digest"],
|
||||
"automerge": true,
|
||||
"minimumReleaseAge": "7 days",
|
||||
"groupName": "patch dependencies",
|
||||
"labels": ["patch-update"],
|
||||
"reviewers": ["a110605", "houhoucoop"]
|
||||
},
|
||||
{
|
||||
"matchUpdateTypes": ["digest", "pinDigest"],
|
||||
"automerge": false,
|
||||
"groupName": "digest dependencies",
|
||||
"labels": ["digest-update"],
|
||||
"schedule": ["on the first day of the month"],
|
||||
"reviewers": ["a110605", "houhoucoop"]
|
||||
"groupName": "patch digest dependencies",
|
||||
"labels": ["patch-update", "automerge"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
- name: Setup Helm
|
||||
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
|
||||
with:
|
||||
version: v3.8.0
|
||||
version: v3.22.0
|
||||
|
||||
- name: Setup Nodejs with yarn install
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
- name: Setup Helm
|
||||
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
|
||||
with:
|
||||
version: v3.8.0
|
||||
version: v3.22.0
|
||||
|
||||
- name: Build Helm charts
|
||||
run: |
|
||||
|
||||
@@ -43,12 +43,10 @@ jobs:
|
||||
needs:
|
||||
- setup-release-tag
|
||||
- check-version
|
||||
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@3b26a36bad555e5e2b8634b24823be29732f287c # master
|
||||
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@9bf23dff6650116673346447bcf0b088e0edb27f # creators-pkg-v3.0.12
|
||||
permissions:
|
||||
actions: write
|
||||
contents: write
|
||||
deployments: write
|
||||
pages: write
|
||||
pull-requests: write
|
||||
with:
|
||||
target_branch: gh-pages
|
||||
tagged_release: '${{ needs.setup-release-tag.outputs.release_tag }}'
|
||||
|
||||
@@ -33,7 +33,7 @@ module.exports = {
|
||||
'subject-full-stop': [2, 'never', '.'],
|
||||
'subject-max-length': [0, 'never'],
|
||||
'body-leading-blank': [2, 'always'],
|
||||
'body-max-line-length': [0, 'always', 100],
|
||||
'body-max-line-length': [2, 'always', 100],
|
||||
'footer-leading-blank': [2, 'always'],
|
||||
'footer-max-line-length': [2, 'always', 100],
|
||||
},
|
||||
|
||||
+9
-9
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "harvester-ui-extension",
|
||||
"version": "1.9.0-dev",
|
||||
"version": "1.8.2",
|
||||
"private": false,
|
||||
"engines": {
|
||||
"node": ">=24.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-class-static-block": "7.28.6",
|
||||
"@rancher/shell": "3.0.12-rc.3",
|
||||
"@babel/plugin-transform-class-static-block": "7.29.7",
|
||||
"@rancher/shell": "3.0.12-rc.1",
|
||||
"@vue-flow/background": "^1.3.0",
|
||||
"@vue-flow/controls": "^1.1.1",
|
||||
"@vue-flow/core": "^1.33.5",
|
||||
@@ -16,12 +16,12 @@
|
||||
"color": "4.2.3",
|
||||
"ip": "2.0.1",
|
||||
"node-polyfill-webpack-plugin": "^3.0.0",
|
||||
"elkjs": "^0.11.0",
|
||||
"elkjs": "^0.12.0",
|
||||
"vue-draggable-next": "^2.2.1",
|
||||
"yaml": "^2.5.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "25.9.1",
|
||||
"@types/node": "25.9.6",
|
||||
"cronstrue": "2.59.0",
|
||||
"d3-color": "3.1.0",
|
||||
"ejs": "3.1.10",
|
||||
@@ -29,13 +29,13 @@
|
||||
"glob": "7.2.3",
|
||||
"glob-parent": "6.0.2",
|
||||
"json5": "2.2.3",
|
||||
"@types/lodash": "4.17.24",
|
||||
"@types/lodash": "4.17.25",
|
||||
"merge": "2.1.1",
|
||||
"node-forge": "1.4.0",
|
||||
"nth-check": "2.1.1",
|
||||
"qs": "6.15.2",
|
||||
"roarr": "7.21.5",
|
||||
"semver": "7.8.1",
|
||||
"qs": "6.16.0",
|
||||
"roarr": "7.21.7",
|
||||
"semver": "7.8.5",
|
||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -122,7 +122,7 @@ export default {
|
||||
return this.$store.getters['currentCluster'].isLocal;
|
||||
},
|
||||
canEditClusterMembers() {
|
||||
return this.schema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
|
||||
return this.normanClusterRTBSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -58,14 +58,11 @@ export default {
|
||||
|
||||
const url = `https://${ host }${ prefix }/${ PRODUCT_NAME }/c/${ params.cluster }/console/${ uid }/${ type }`;
|
||||
|
||||
// Defer so v-select can finish closing the dropdown before the popup steals focus
|
||||
this.$nextTick(() => {
|
||||
window.open(
|
||||
url,
|
||||
'_blank',
|
||||
`toolbars=0,width=${ screen.width - 200 },height=${ screen.height - 200 },left=0,top=0,noreferrer`
|
||||
);
|
||||
});
|
||||
window.open(
|
||||
url,
|
||||
'_blank',
|
||||
`toolbars=0,width=${ screen.width - 200 },height=${ screen.height - 200 },left=0,top=0,noreferrer`
|
||||
);
|
||||
},
|
||||
|
||||
isEmpty(o) {
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
<script>
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { NAMESPACE } from '@shell/config/types';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterClusterPodSecurityStandard',
|
||||
|
||||
components: {
|
||||
RadioGroup,
|
||||
LabeledSelect,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
await this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE });
|
||||
},
|
||||
|
||||
data() {
|
||||
let enabled = false;
|
||||
let whitelistedNamespaces = [];
|
||||
let privilegedNamespaces = [];
|
||||
let restrictedNamespaces = [];
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(this.value.value || this.value.default || '{}');
|
||||
|
||||
enabled = !!parsed.enabled;
|
||||
whitelistedNamespaces = parsed.whitelistedNamespacesList ? parsed.whitelistedNamespacesList.split(',') : [];
|
||||
privilegedNamespaces = parsed.privilegedNamespacesList ? parsed.privilegedNamespacesList.split(',') : [];
|
||||
restrictedNamespaces = parsed.restrictedNamespacesList ? parsed.restrictedNamespacesList.split(',') : [];
|
||||
} catch (e) {
|
||||
enabled = false;
|
||||
whitelistedNamespaces = [];
|
||||
privilegedNamespaces = [];
|
||||
restrictedNamespaces = [];
|
||||
}
|
||||
|
||||
return {
|
||||
enabled,
|
||||
whitelistedNamespaces,
|
||||
privilegedNamespaces,
|
||||
restrictedNamespaces,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
enabledOptions() {
|
||||
return [
|
||||
{ label: this.t('generic.enabled'), value: true },
|
||||
{ label: this.t('generic.disabled'), value: false },
|
||||
];
|
||||
},
|
||||
|
||||
allNamespaces() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return this.$store.getters[`${ inStore }/all`](NAMESPACE).filter((ns) => !ns.isSystem).map((ns) => ns.id);
|
||||
},
|
||||
|
||||
whitelistedOptions() {
|
||||
const excluded = new Set([...this.privilegedNamespaces, ...this.restrictedNamespaces]);
|
||||
|
||||
return this.allNamespaces.filter((ns) => !excluded.has(ns));
|
||||
},
|
||||
|
||||
privilegedOptions() {
|
||||
const excluded = new Set([...this.whitelistedNamespaces, ...this.restrictedNamespaces]);
|
||||
|
||||
return this.allNamespaces.filter((ns) => !excluded.has(ns));
|
||||
},
|
||||
|
||||
restrictedOptions() {
|
||||
const excluded = new Set([...this.whitelistedNamespaces, ...this.privilegedNamespaces]);
|
||||
|
||||
return this.allNamespaces.filter((ns) => !excluded.has(ns));
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
useDefault() {
|
||||
try {
|
||||
const parsed = JSON.parse(this.value.default || '{}');
|
||||
|
||||
this.enabled = !!parsed.enabled;
|
||||
this.whitelistedNamespaces = parsed.whitelistedNamespacesList ? parsed.whitelistedNamespacesList.split(',') : [];
|
||||
this.privilegedNamespaces = parsed.privilegedNamespacesList ? parsed.privilegedNamespacesList.split(',') : [];
|
||||
this.restrictedNamespaces = parsed.restrictedNamespacesList ? parsed.restrictedNamespacesList.split(',') : [];
|
||||
} catch (e) {
|
||||
this.enabled = false;
|
||||
this.whitelistedNamespaces = [];
|
||||
this.privilegedNamespaces = [];
|
||||
this.restrictedNamespaces = [];
|
||||
}
|
||||
this.save();
|
||||
},
|
||||
|
||||
onUpdateEnabled() {
|
||||
if (!this.enabled) {
|
||||
this.whitelistedNamespaces = [];
|
||||
this.privilegedNamespaces = [];
|
||||
this.restrictedNamespaces = [];
|
||||
}
|
||||
this.save();
|
||||
},
|
||||
|
||||
updateWhitelisted(selected) {
|
||||
this.whitelistedNamespaces = selected;
|
||||
this.save();
|
||||
},
|
||||
|
||||
updatePrivileged(selected) {
|
||||
this.privilegedNamespaces = selected;
|
||||
this.save();
|
||||
},
|
||||
|
||||
updateRestricted(selected) {
|
||||
this.restrictedNamespaces = selected;
|
||||
this.save();
|
||||
},
|
||||
|
||||
save() {
|
||||
this.value.value = JSON.stringify({
|
||||
enabled: this.enabled,
|
||||
whitelistedNamespacesList: this.whitelistedNamespaces.join(','),
|
||||
privilegedNamespacesList: this.privilegedNamespaces.join(','),
|
||||
restrictedNamespacesList: this.restrictedNamespaces.join(','),
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<RadioGroup
|
||||
v-model:value="enabled"
|
||||
name="enabled"
|
||||
:options="enabledOptions"
|
||||
@update:value="onUpdateEnabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="enabled">
|
||||
<div class="row mb-20">
|
||||
<div class="col span-12">
|
||||
<LabeledSelect
|
||||
v-model:value="whitelistedNamespaces"
|
||||
:label="t('harvester.setting.clusterPodSecurityStandard.whitelistedNamespaces.label')"
|
||||
:options="whitelistedOptions"
|
||||
:multiple="true"
|
||||
:mode="mode"
|
||||
@update:value="updateWhitelisted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-12">
|
||||
<LabeledSelect
|
||||
v-model:value="privilegedNamespaces"
|
||||
:label="t('harvester.setting.clusterPodSecurityStandard.privilegedNamespaces.label')"
|
||||
:options="privilegedOptions"
|
||||
:multiple="true"
|
||||
:mode="mode"
|
||||
@update:value="updatePrivileged"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-12">
|
||||
<LabeledSelect
|
||||
v-model:value="restrictedNamespaces"
|
||||
:label="t('harvester.setting.clusterPodSecurityStandard.restrictedNamespaces.label')"
|
||||
:options="restrictedOptions"
|
||||
:multiple="true"
|
||||
:mode="mode"
|
||||
@update:value="updateRestricted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -65,14 +65,7 @@ const FEATURE_FLAGS = {
|
||||
'vGPUAsPCIDevice',
|
||||
'instanceManagerResourcesSetting',
|
||||
'rwxNetworkSetting',
|
||||
'createPVCWithDataVolume',
|
||||
'clusterPodSecurityStandardSetting',
|
||||
],
|
||||
'v1.8.1': [],
|
||||
'v1.9.0': [
|
||||
'supportFilesystem',
|
||||
'disableResourcePooling',
|
||||
'expandOnlineEncryptedVolume'
|
||||
'createPVCWithDataVolume'
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -237,7 +237,6 @@ export function init($plugin, store) {
|
||||
labelKey: 'harvester.addons.vmImport.labels.vmimport',
|
||||
group: 'vmimport',
|
||||
namespaced: true,
|
||||
ifHaveType: HCI.VMIMPORT,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VMIMPORT }
|
||||
@@ -267,7 +266,6 @@ export function init($plugin, store) {
|
||||
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare',
|
||||
group: 'vmimport',
|
||||
namespaced: true,
|
||||
ifHaveType: HCI.VMIMPORT_SOURCE_V,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VMIMPORT_SOURCE_V }
|
||||
@@ -297,7 +295,6 @@ export function init($plugin, store) {
|
||||
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack',
|
||||
group: 'vmimport',
|
||||
namespaced: true,
|
||||
ifHaveType: HCI.VMIMPORT_SOURCE_O,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VMIMPORT_SOURCE_O }
|
||||
@@ -326,7 +323,6 @@ export function init($plugin, store) {
|
||||
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA',
|
||||
group: 'vmimport',
|
||||
namespaced: true,
|
||||
ifHaveType: HCI.VMIMPORT_SOURCE_OVA,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
|
||||
@@ -488,7 +484,6 @@ export function init($plugin, store) {
|
||||
});
|
||||
|
||||
virtualType({
|
||||
ifHaveType: LOGGING.CLUSTER_FLOW,
|
||||
labelKey: 'harvester.logging.clusterFlow.label',
|
||||
name: HCI.CLUSTER_FLOW,
|
||||
namespaced: true,
|
||||
@@ -512,7 +507,6 @@ export function init($plugin, store) {
|
||||
});
|
||||
|
||||
virtualType({
|
||||
ifHaveType: LOGGING.CLUSTER_OUTPUT,
|
||||
labelKey: 'harvester.logging.clusterOutput.label',
|
||||
name: HCI.CLUSTER_OUTPUT,
|
||||
namespaced: true,
|
||||
@@ -536,7 +530,6 @@ export function init($plugin, store) {
|
||||
});
|
||||
|
||||
virtualType({
|
||||
ifHaveType: LOGGING.FLOW,
|
||||
labelKey: 'harvester.logging.flow.label',
|
||||
name: HCI.FLOW,
|
||||
namespaced: true,
|
||||
@@ -560,7 +553,6 @@ export function init($plugin, store) {
|
||||
});
|
||||
|
||||
virtualType({
|
||||
ifHaveType: LOGGING.OUTPUT,
|
||||
labelKey: 'harvester.logging.output.label',
|
||||
name: HCI.OUTPUT,
|
||||
namespaced: true,
|
||||
@@ -694,7 +686,7 @@ export function init($plugin, store) {
|
||||
},
|
||||
resource: NETWORK_ATTACHMENT,
|
||||
resourceDetail: HCI.NETWORK_ATTACHMENT,
|
||||
resourceEdit: HCI.NETWORK_ATTACHMENT,
|
||||
resourceEdit: HCI.NETWORK_ATTACHMENT
|
||||
});
|
||||
|
||||
virtualType({
|
||||
|
||||
@@ -88,8 +88,6 @@ export const CSI_SECRETS = {
|
||||
CSI_NODE_PUBLISH_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-publish-secret-namespace',
|
||||
CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name',
|
||||
CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace',
|
||||
CSI_NODE_EXPAND_SECRET_NAME: 'csi.storage.k8s.io/node-expand-secret-name',
|
||||
CSI_NODE_EXPAND_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-expand-secret-namespace'
|
||||
};
|
||||
|
||||
// Some harvester CRD type is not equal to model file name, define the mapping here
|
||||
|
||||
@@ -19,7 +19,6 @@ export const HCI = {
|
||||
NETWORK_TYPE: 'network.harvesterhci.io/type',
|
||||
VM_NAME: 'harvesterhci.io/vmName',
|
||||
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
|
||||
VM_DISPLAY_NAME: 'harvesterhci.io/vmDisplayName',
|
||||
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
|
||||
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
|
||||
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
|
||||
|
||||
@@ -41,8 +41,7 @@ export const HCI_SETTING = {
|
||||
RANCHER_CLUSTER: 'rancher-cluster',
|
||||
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
|
||||
KUBEVIRT_MIGRATION: 'kubevirt-migration',
|
||||
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources',
|
||||
CLUSTER_POD_SECURITY_STANDARD: 'cluster-pod-security-standard'
|
||||
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources'
|
||||
};
|
||||
|
||||
export const HCI_ALLOWED_SETTINGS = {
|
||||
@@ -131,9 +130,6 @@ export const HCI_ALLOWED_SETTINGS = {
|
||||
},
|
||||
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
|
||||
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
|
||||
},
|
||||
[HCI_SETTING.CLUSTER_POD_SECURITY_STANDARD]: {
|
||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'clusterPodSecurityStandardSetting'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -46,9 +46,3 @@ export const CDI_POPULATOR_KIND = {
|
||||
VOLUME_IMPORT_SOURCE: 'VolumeImportSource',
|
||||
VOLUME_CLONE_SOURCE: 'VolumeCloneSource',
|
||||
};
|
||||
|
||||
export const FILESYSTEM_SOURCE_TYPE = {
|
||||
CONFIGMAP: 'configmap',
|
||||
SECRET: 'secret',
|
||||
SERVICEACCOUNT: 'serviceaccount',
|
||||
};
|
||||
|
||||
@@ -27,11 +27,15 @@ export default {
|
||||
await allHash({
|
||||
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
|
||||
vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }),
|
||||
vmims: this.$store.dispatch('harvester/findAll', { type: HCI.VMIM }),
|
||||
allClusterNetwork: this.$store.dispatch('harvester/findAll', { type: HCI.CLUSTER_NETWORK }),
|
||||
});
|
||||
},
|
||||
|
||||
computed: {
|
||||
allClusterNetwork() {
|
||||
return this.$store.getters['harvester/all'](HCI.CLUSTER_NETWORK);
|
||||
},
|
||||
|
||||
rows() {
|
||||
const vms = this.$store.getters['harvester/all'](HCI.VM);
|
||||
|
||||
@@ -104,6 +108,7 @@ export default {
|
||||
<HarvesterVmState
|
||||
class="vmstate"
|
||||
:row="scope.row"
|
||||
:all-cluster-network="allClusterNetwork"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { Card } from '@components/Card';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { escapeHtml } from '@shell/utils/string';
|
||||
import { HCI } from '../types';
|
||||
@@ -15,9 +13,7 @@ export default {
|
||||
|
||||
components: {
|
||||
AsyncButton,
|
||||
Banner,
|
||||
Card,
|
||||
Checkbox,
|
||||
},
|
||||
|
||||
props: {
|
||||
@@ -28,16 +24,10 @@ export default {
|
||||
},
|
||||
|
||||
data() {
|
||||
return { disableResourcePooling: false };
|
||||
return {};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
disableResourcePoolingEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('disableResourcePooling');
|
||||
},
|
||||
},
|
||||
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
@@ -62,10 +52,9 @@ export default {
|
||||
}]
|
||||
},
|
||||
spec: {
|
||||
address: actionResource.status.address,
|
||||
nodeName: actionResource.status.nodeName,
|
||||
userName,
|
||||
disableResourcePooling: this.disableResourcePooling,
|
||||
address: actionResource.status.address,
|
||||
nodeName: actionResource.status.nodeName,
|
||||
userName
|
||||
}
|
||||
} );
|
||||
|
||||
@@ -96,19 +85,7 @@ export default {
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<p class="mb-20">
|
||||
{{ t('harvester.pci.enablePassthroughWarning') }}
|
||||
</p>
|
||||
<template v-if="disableResourcePoolingEnabled">
|
||||
<Checkbox
|
||||
v-model:value="disableResourcePooling"
|
||||
label-key="harvester.pci.disableResourcePooling"
|
||||
/>
|
||||
<Banner
|
||||
color="info"
|
||||
:label="t('harvester.pci.disableResourcePoolingDescription')"
|
||||
/>
|
||||
</template>
|
||||
{{ t('harvester.pci.enablePassthroughWarning') }}
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
<script>
|
||||
import merge from 'lodash/merge';
|
||||
import jsyaml from 'js-yaml';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { Card } from '@components/Card';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { escapeHtml } from '@shell/utils/string';
|
||||
|
||||
const DEFAULT_VALUE = { image: { repository: 'rancher/harvester-nvidia-driver-toolkit' } };
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEnableNvidiaDriverToolkit',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
components: {
|
||||
AsyncButton,
|
||||
Card,
|
||||
LabeledInput,
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const addon = this.resources[0];
|
||||
let valuesContentJson;
|
||||
|
||||
try {
|
||||
valuesContentJson = merge({}, DEFAULT_VALUE, jsyaml.load(addon.spec.valuesContent));
|
||||
} catch (e) {
|
||||
valuesContentJson = { ...DEFAULT_VALUE };
|
||||
}
|
||||
|
||||
return { valuesContentJson };
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
buttonDisabled() {
|
||||
const { image, driverLocation } = this.valuesContentJson;
|
||||
|
||||
return !(image?.repository || '').trim() || !(image?.tag || '').trim() || !(driverLocation || '').trim();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async enable(buttonCb) {
|
||||
const addon = this.resources[0];
|
||||
|
||||
try {
|
||||
addon.spec.valuesContent = jsyaml.dump(this.valuesContentJson);
|
||||
addon.spec.enabled = true;
|
||||
await addon.save();
|
||||
buttonCb(true);
|
||||
this.close();
|
||||
} catch (err) {
|
||||
addon.spec.enabled = false;
|
||||
this.$store.dispatch('growl/fromError', {
|
||||
title: this.t('generic.notification.title.error', { name: escapeHtml(addon.metadata.name) }),
|
||||
err,
|
||||
}, { root: true });
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
<h4
|
||||
v-clean-html="t('harvester.addons.nvidiaDriverToolkit.enable.title')"
|
||||
class="text-default-text"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="body">
|
||||
<div class="row mb-15">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="valuesContentJson.image.repository"
|
||||
:required="true"
|
||||
:label="t('harvester.addons.nvidiaDriverToolkit.image.repository')"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="valuesContentJson.image.tag"
|
||||
:required="true"
|
||||
:label="t('harvester.addons.nvidiaDriverToolkit.image.tag')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-15">
|
||||
<div class="col span-12">
|
||||
<LabeledInput
|
||||
v-model:value="valuesContentJson.driverLocation"
|
||||
:required="true"
|
||||
:label="t('harvester.addons.nvidiaDriverToolkit.driver.location')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="buttons actions">
|
||||
<button
|
||||
class="btn role-secondary mr-10"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="enable"
|
||||
:disabled="buttonDisabled"
|
||||
@click="enable"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,15 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { NODE } from '@shell/config/types';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { HCI } from '../types';
|
||||
|
||||
export default {
|
||||
emits: ['close'],
|
||||
@@ -59,46 +62,28 @@ export default {
|
||||
return this.resources[0];
|
||||
},
|
||||
|
||||
anyCpuPinning() {
|
||||
return this.resources.some((r) => r.isCpuPinning);
|
||||
},
|
||||
vmi() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const vmiResources = this.$store.getters[`${ inStore }/all`](HCI.VMI);
|
||||
const resource = vmiResources.find((VMI) => VMI.id === this.actionResource?.id) || null;
|
||||
|
||||
vmsByNode() {
|
||||
const groups = {};
|
||||
|
||||
for (const r of this.resources) {
|
||||
const node = r.nodeName || '';
|
||||
const name = r.nameDisplay || r.name || r.id;
|
||||
|
||||
if (!groups[node]) {
|
||||
groups[node] = [];
|
||||
}
|
||||
groups[node].push(name);
|
||||
}
|
||||
|
||||
return Object.entries(groups).map(([node, vms]) => ({ node, vms })).sort((a, b) => a.node.localeCompare(b.node));
|
||||
return resource;
|
||||
},
|
||||
|
||||
cpuPinningAlertMessage() {
|
||||
return this.t('harvester.virtualMachine.cpuPinning.migrationMessage');
|
||||
},
|
||||
|
||||
allVmsOnTargetNode() {
|
||||
if (!this.nodeName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.resources.every((r) => r.nodeName === this.nodeName);
|
||||
},
|
||||
|
||||
nodeNameList() {
|
||||
const nodes = this.$store.getters['harvester/all'](NODE);
|
||||
|
||||
return nodes.filter((n) => {
|
||||
const isNotSelfNode = !!this.availableNodes.includes(n.id);
|
||||
const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node
|
||||
const matchingCpuManagerConfig = !this.anyCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
|
||||
const isCpuPinning = this.actionResource?.isCpuPinning;
|
||||
const matchingCpuManagerConfig = !isCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
|
||||
|
||||
return isNotWitnessNode && matchingCpuManagerConfig;
|
||||
return isNotSelfNode && isNotWitnessNode && matchingCpuManagerConfig;
|
||||
}).map((n) => {
|
||||
let label = n?.metadata?.name;
|
||||
const value = n?.metadata?.name;
|
||||
@@ -141,29 +126,7 @@ export default {
|
||||
}
|
||||
|
||||
try {
|
||||
// Filter out VMs already running on the selected node
|
||||
const toMigrate = this.resources.filter((r) => r.nodeName !== this.nodeName);
|
||||
|
||||
// await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
|
||||
// We want to show all migration errors if there are multiple VMs, so we use allSettled here and handle the results accordingly.
|
||||
const results = await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
|
||||
|
||||
const failedMigrations = results
|
||||
.map((result, index) => ({ resource: toMigrate[index], result }))
|
||||
.filter(({ result }) => result.status === 'rejected');
|
||||
|
||||
if (failedMigrations.length) {
|
||||
this['errors'] = failedMigrations.flatMap(({ resource, result }) => {
|
||||
const vmName = resource?.nameDisplay || resource?.name || resource?.metadata?.name || this.$store.getters['i18n/t']('generic.unknown');
|
||||
const error = result.reason?.data || result.reason;
|
||||
const messages = exceptionToErrorsArray(error);
|
||||
|
||||
return messages.map((message) => `${ vmName }: ${ message }`);
|
||||
});
|
||||
buttonDone(false);
|
||||
|
||||
return;
|
||||
}
|
||||
await this.actionResource.doAction('migrate', { nodeName: this.nodeName }, {}, false);
|
||||
|
||||
buttonDone(true);
|
||||
this.close();
|
||||
@@ -183,35 +146,17 @@ export default {
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.migration.vmMigrationTitle', { count: resources.length }) }}
|
||||
{{ t('harvester.modal.migration.title') }}
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<Banner
|
||||
v-if="anyCpuPinning"
|
||||
v-if="actionResource?.isCpuPinning"
|
||||
color="warning"
|
||||
:label="cpuPinningAlertMessage"
|
||||
/>
|
||||
<p>
|
||||
{{ t('harvester.modal.migration.selectedVMs') }}
|
||||
</p>
|
||||
<ul class="vm-list">
|
||||
<li
|
||||
v-for="group in vmsByNode"
|
||||
:key="group.node"
|
||||
>
|
||||
{{ group.node || t('harvester.modal.migration.unknownNode') }}: {{ group.vms.join(', ') }}
|
||||
<span
|
||||
v-if="nodeName && group.node === nodeName"
|
||||
class="already-on-target"
|
||||
>
|
||||
({{ t('harvester.modal.migration.alreadyOnTarget') }})
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<LabeledSelect
|
||||
v-model:value="nodeName"
|
||||
class="mt-15"
|
||||
:label="t('harvester.modal.migration.fields.nodeName.label')"
|
||||
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
|
||||
:options="nodeNameList"
|
||||
@@ -238,7 +183,7 @@ export default {
|
||||
|
||||
<AsyncButton
|
||||
mode="apply"
|
||||
:disabled="!nodeName || allVmsOnTargetNode"
|
||||
:disabled="!nodeName"
|
||||
@click="apply"
|
||||
/>
|
||||
</div>
|
||||
@@ -256,16 +201,4 @@ export default {
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.already-on-target {
|
||||
color: var(--warning);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.vm-list {
|
||||
list-style: disc;
|
||||
padding-left: 1.5em;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,7 +8,6 @@ import FileSelector, { createOnSelected } from '@shell/components/form/FileSelec
|
||||
|
||||
import { randomStr } from '@shell/utils/string';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { getLoginAwareErrors } from '../utils/error';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditKeypair',
|
||||
@@ -64,14 +63,6 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
normalizedErrors() {
|
||||
const message = this.t('harvester.virtualMachine.genericLoginError');
|
||||
|
||||
return getLoginAwareErrors(this.errors, message);
|
||||
}
|
||||
},
|
||||
|
||||
methods: { onKeySelected: createOnSelected('publicKey') },
|
||||
};
|
||||
</script>
|
||||
@@ -81,9 +72,10 @@ export default {
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:errors="normalizedErrors"
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<div class="header mb-20">
|
||||
<FileSelector
|
||||
|
||||
@@ -3,9 +3,8 @@ 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 Checkbox from '@components/Form/Checkbox/Checkbox';
|
||||
import { SECRET, LONGHORN } from '@shell/config/types';
|
||||
import { _CREATE, _VIEW, _EDIT } from '@shell/config/query-params';
|
||||
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';
|
||||
@@ -17,9 +16,7 @@ const {
|
||||
CSI_NODE_PUBLISH_SECRET_NAME,
|
||||
CSI_NODE_PUBLISH_SECRET_NAMESPACE,
|
||||
CSI_NODE_STAGE_SECRET_NAME,
|
||||
CSI_NODE_STAGE_SECRET_NAMESPACE,
|
||||
CSI_NODE_EXPAND_SECRET_NAME,
|
||||
CSI_NODE_EXPAND_SECRET_NAMESPACE
|
||||
CSI_NODE_STAGE_SECRET_NAMESPACE
|
||||
} = CSI_SECRETS;
|
||||
|
||||
export default {
|
||||
@@ -30,7 +27,6 @@ export default {
|
||||
LabeledSelect,
|
||||
LabeledInput,
|
||||
RadioGroup,
|
||||
Checkbox,
|
||||
},
|
||||
|
||||
props: {
|
||||
@@ -61,10 +57,7 @@ export default {
|
||||
};
|
||||
}
|
||||
|
||||
const hasExpandSecret = !!(this.value.parameters?.[CSI_NODE_EXPAND_SECRET_NAME] && this.value.parameters?.[CSI_NODE_EXPAND_SECRET_NAMESPACE]);
|
||||
const volumeExpansionCheckBoxEnabled = this.realMode === _CREATE ? true : hasExpandSecret;
|
||||
|
||||
return { volumeExpansionCheckBoxEnabled };
|
||||
return { };
|
||||
},
|
||||
|
||||
computed: {
|
||||
@@ -105,11 +98,8 @@ export default {
|
||||
}, []);
|
||||
},
|
||||
|
||||
isEdit() {
|
||||
return this.realMode === _EDIT;
|
||||
},
|
||||
isView() {
|
||||
return this.realMode === _VIEW;
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
|
||||
migratableOptions() {
|
||||
@@ -162,10 +152,6 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
enableOnlineExpansionVolumeEncryption() {
|
||||
return this.value.expandOnlineEncryptedVolumeFeatureEnabled;
|
||||
},
|
||||
|
||||
volumeEncryption: {
|
||||
set(neu) {
|
||||
this.value['parameters'] = {
|
||||
@@ -194,11 +180,6 @@ export default {
|
||||
set(selectedSecret) {
|
||||
const [namespace, name] = selectedSecret.split('/');
|
||||
|
||||
const expandSecretParams = (this.enableOnlineExpansionVolumeEncryption && this.volumeExpansionCheckBoxEnabled) ? {
|
||||
[CSI_NODE_EXPAND_SECRET_NAME]: name,
|
||||
[CSI_NODE_EXPAND_SECRET_NAMESPACE]: namespace,
|
||||
} : {};
|
||||
|
||||
this.value['parameters'] = {
|
||||
...this.value.parameters,
|
||||
[CSI_PROVISIONER_SECRET_NAME]: name,
|
||||
@@ -206,8 +187,7 @@ export default {
|
||||
[CSI_NODE_STAGE_SECRET_NAME]: name,
|
||||
[CSI_PROVISIONER_SECRET_NAMESPACE]: namespace,
|
||||
[CSI_NODE_PUBLISH_SECRET_NAMESPACE]: namespace,
|
||||
[CSI_NODE_STAGE_SECRET_NAMESPACE]: namespace,
|
||||
...expandSecretParams,
|
||||
[CSI_NODE_STAGE_SECRET_NAMESPACE]: namespace
|
||||
};
|
||||
}
|
||||
},
|
||||
@@ -260,32 +240,6 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
volumeExpansionCheckBoxEnabled(enabled) {
|
||||
const currentSecret = this.secret;
|
||||
|
||||
if (!currentSecret) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [namespace, name] = currentSecret.split('/');
|
||||
|
||||
if (enabled && this.enableOnlineExpansionVolumeEncryption) {
|
||||
this.value['parameters'] = {
|
||||
...this.value.parameters,
|
||||
[CSI_NODE_EXPAND_SECRET_NAME]: name,
|
||||
[CSI_NODE_EXPAND_SECRET_NAMESPACE]: namespace,
|
||||
};
|
||||
} else {
|
||||
const params = { ...this.value.parameters };
|
||||
|
||||
delete params[CSI_NODE_EXPAND_SECRET_NAME];
|
||||
delete params[CSI_NODE_EXPAND_SECRET_NAMESPACE];
|
||||
this.value['parameters'] = params;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
@@ -383,16 +337,6 @@ export default {
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="enableOnlineExpansionVolumeEncryption"
|
||||
class="col span-6 flex items-center mt-20"
|
||||
>
|
||||
<Checkbox
|
||||
v-model:value="volumeExpansionCheckBoxEnabled"
|
||||
:label="t('harvester.storage.volumeExpansionCheckbox')"
|
||||
:disabled="isEdit || isView"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<KeyValue
|
||||
|
||||
@@ -254,24 +254,15 @@ export default {
|
||||
v-if="restoreNewVm"
|
||||
v-model:value="restoreResource.spec.keepMacAddress"
|
||||
type="checkbox"
|
||||
class="check mb-20"
|
||||
:label="t('harvester.backup.restore.keepMacAddress')"
|
||||
/>
|
||||
|
||||
<LabeledSelect
|
||||
v-if="!restoreNewVm"
|
||||
v-model:value="deletionPolicy"
|
||||
class="mb-20"
|
||||
:label="t('harvester.backup.restore.deletePreviousVolumes')"
|
||||
:options="deletionPolicyOption"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
v-model:value="restoreResource.spec.haltAfterRestore"
|
||||
type="checkbox"
|
||||
class="check mb-20"
|
||||
:label="t('harvester.backup.restore.haltAfterRestore')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Footer
|
||||
|
||||
@@ -26,7 +26,6 @@ import CpuMemory from './kubevirt.io.virtualmachine/VirtualMachineCpuMemory';
|
||||
import CpuModel from './kubevirt.io.virtualmachine/VirtualMachineCpuModel';
|
||||
import CloudConfig from './kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
||||
import SSHKey from './kubevirt.io.virtualmachine/VirtualMachineSSHKey';
|
||||
import Filesystem from './kubevirt.io.virtualmachine/VirtualMachineFilesystem';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditVMTemplate',
|
||||
@@ -52,7 +51,6 @@ export default {
|
||||
UnitInput,
|
||||
Banner,
|
||||
KeyValue,
|
||||
Filesystem,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView, VM_MIXIN],
|
||||
@@ -97,10 +95,6 @@ export default {
|
||||
secretNamePrefix() {
|
||||
return this.templateValue?.metadata?.name;
|
||||
},
|
||||
|
||||
filesystemEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('supportFilesystem');
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -160,7 +154,6 @@ export default {
|
||||
|
||||
mounted() {
|
||||
this.imageId = this.diskRows[0]?.image || '';
|
||||
this['filesystemRows'] = this.getFilesystemRows(this.value.spec.vm);
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -356,19 +349,6 @@ export default {
|
||||
</template>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
v-if="filesystemEnabled"
|
||||
name="filesystem"
|
||||
:label="t('harvester.tab.filesystem')"
|
||||
:weight="-8"
|
||||
>
|
||||
<Filesystem
|
||||
v-model:value="filesystemRows"
|
||||
:mode="mode"
|
||||
:namespace="templateValue.metadata.namespace"
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="labels"
|
||||
:label="t('generic.labels')"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import Footer from '@shell/components/form/Footer';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import Checkbox from '@components/Form/Checkbox/Checkbox';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
@@ -33,7 +32,6 @@ const createObject = {
|
||||
export default {
|
||||
name: 'CreateRestore',
|
||||
components: {
|
||||
Checkbox,
|
||||
Footer,
|
||||
RadioGroup,
|
||||
LabeledInput,
|
||||
@@ -251,17 +249,9 @@ export default {
|
||||
<LabeledSelect
|
||||
v-if="!restoreNewVm"
|
||||
v-model:value="deletionPolicy"
|
||||
class="mb-20"
|
||||
:label="t('harvester.backup.restore.deletePreviousVolumes')"
|
||||
:options="deletionPolicyOption"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
v-model:value="restoreResource.spec.haltAfterRestore"
|
||||
type="checkbox"
|
||||
class="check mb-20"
|
||||
:label="t('harvester.backup.restore.haltAfterRestore')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Footer
|
||||
|
||||
@@ -43,20 +43,18 @@ export default {
|
||||
created() {
|
||||
const vpc = this.$route.query.vpc || '';
|
||||
const enableDHCP = this.value?.spec?.enableDHCP || false;
|
||||
const natOutgoing = this.value?.spec?.natOutgoing || false;
|
||||
|
||||
set(this.value.spec, 'enableDHCP', enableDHCP);
|
||||
set(this.value, 'spec', this.value.spec || {
|
||||
cidrBlock: '',
|
||||
protocol: NETWORK_PROTOCOL.IPv4,
|
||||
provider: '',
|
||||
cidrBlock: '',
|
||||
protocol: NETWORK_PROTOCOL.IPv4,
|
||||
provider: '',
|
||||
vpc,
|
||||
gateway: '',
|
||||
excludeIps: [],
|
||||
private: false,
|
||||
gatewayIP: '',
|
||||
excludeIps: [],
|
||||
private: false,
|
||||
enableDHCP,
|
||||
natOutgoing,
|
||||
acls: []
|
||||
acls: []
|
||||
});
|
||||
},
|
||||
|
||||
@@ -131,10 +129,6 @@ export default {
|
||||
label: n.id,
|
||||
value: n.id,
|
||||
}));
|
||||
},
|
||||
natOutgoingDisabled() {
|
||||
// Disable the NAT Outgoing option when the subnet belongs to the ovn-cluster VPC and its name is join or ovn-default.
|
||||
return this.value?.spec?.vpc === 'ovn-cluster' && ['join', 'ovn-default'].includes(this.value?.metadata?.name);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -310,20 +304,6 @@ export default {
|
||||
</Banner>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<RadioGroup
|
||||
v-model:value="value.spec.natOutgoing"
|
||||
name="enableExternalConnectivity"
|
||||
:disabled="natOutgoingDisabled"
|
||||
:options="[true, false]"
|
||||
:label="t('harvester.subnet.externalConnectivity.label')"
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
:mode="mode"
|
||||
:tooltip="t('harvester.subnet.externalConnectivity.tooltip')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<RadioGroup
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { Banner } from '@components/Banner';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import { CONFIG_MAP, SECRET, SERVICE_ACCOUNT } from '@shell/config/types';
|
||||
import { _VIEW } from '@shell/config/query-params';
|
||||
import CopyToClipboard from '@shell/components/CopyToClipboard';
|
||||
import MessageLink from '@shell/components/MessageLink';
|
||||
import { FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
|
||||
|
||||
const MAX_FILESYSTEMS = 3;
|
||||
|
||||
const { CONFIGMAP: FS_TYPE_CONFIGMAP, SECRET: FS_TYPE_SECRET, SERVICEACCOUNT: FS_TYPE_SERVICEACCOUNT } = FILESYSTEM_SOURCE_TYPE;
|
||||
|
||||
const DEFAULT_VOLUME_NAMES = {
|
||||
[FS_TYPE_CONFIGMAP]: 'appconfigfs',
|
||||
[FS_TYPE_SECRET]: 'appsecretfs',
|
||||
[FS_TYPE_SERVICEACCOUNT]: 'appserviceaccountfs',
|
||||
};
|
||||
|
||||
const FS_TYPE_OPTIONS = [
|
||||
{ label: 'ConfigMap', value: FS_TYPE_CONFIGMAP },
|
||||
{ label: 'Secret', value: FS_TYPE_SECRET },
|
||||
{ label: 'ServiceAccount', value: FS_TYPE_SERVICEACCOUNT },
|
||||
];
|
||||
|
||||
function emptyRow() {
|
||||
return {
|
||||
fsType: FS_TYPE_CONFIGMAP,
|
||||
volumeName: DEFAULT_VOLUME_NAMES[FS_TYPE_CONFIGMAP],
|
||||
resourceName: '',
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'VirtualMachineFilesystem',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
components: {
|
||||
Banner,
|
||||
LabeledSelect,
|
||||
LabeledInput,
|
||||
CopyToClipboard,
|
||||
MessageLink,
|
||||
},
|
||||
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'create',
|
||||
},
|
||||
namespace: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return { rows: this.value.length > 0 ? this.value.map((r) => ({ ...r })) : [emptyRow()] };
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(newVal) {
|
||||
if (newVal) {
|
||||
const incoming = JSON.stringify(newVal);
|
||||
const current = JSON.stringify(this.rows);
|
||||
|
||||
if (incoming !== current) {
|
||||
this.rows = newVal.map((r) => ({ ...r }));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
rows: {
|
||||
deep: true,
|
||||
handler(val) {
|
||||
this.$emit('update:value', val.map((r) => ({ ...r })));
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
inStore() {
|
||||
return this.$store.getters['currentProduct'].inStore;
|
||||
},
|
||||
|
||||
configMaps() {
|
||||
return this.$store.getters[`${ this.inStore }/all`](CONFIG_MAP)
|
||||
.filter((cm) => !this.namespace || cm.metadata.namespace === this.namespace)
|
||||
.map((cm) => ({ label: cm.metadata.name, value: cm.metadata.name }));
|
||||
},
|
||||
|
||||
secrets() {
|
||||
return this.$store.getters[`${ this.inStore }/all`](SECRET)
|
||||
.filter((s) => !this.namespace || s.metadata.namespace === this.namespace)
|
||||
.map((s) => ({ label: s.metadata.name, value: s.metadata.name }));
|
||||
},
|
||||
|
||||
serviceAccounts() {
|
||||
return this.$store.getters[`${ this.inStore }/all`](SERVICE_ACCOUNT)
|
||||
.filter((sa) => !this.namespace || sa.metadata.namespace === this.namespace)
|
||||
.map((sa) => ({ label: sa.metadata.name, value: sa.metadata.name }));
|
||||
},
|
||||
|
||||
canAddRow() {
|
||||
return this.rows.length < MAX_FILESYSTEMS;
|
||||
},
|
||||
|
||||
isView() {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
|
||||
completedRows() {
|
||||
return this.rows.filter((r) => r.fsType && r.volumeName && r.resourceName);
|
||||
},
|
||||
|
||||
allMountCommands() {
|
||||
return this.completedRows.map((r) => this.mountCommands(r)).join('\n');
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
fsTypeOptions(currentIndex) {
|
||||
const usedTypes = this.rows
|
||||
.filter((_, i) => i !== currentIndex)
|
||||
.map((r) => r.fsType);
|
||||
|
||||
return FS_TYPE_OPTIONS.filter((opt) => !usedTypes.includes(opt.value));
|
||||
},
|
||||
|
||||
resourceOptions(fsType) {
|
||||
if (fsType === FS_TYPE_CONFIGMAP) return this.configMaps;
|
||||
if (fsType === FS_TYPE_SECRET) return this.secrets;
|
||||
if (fsType === FS_TYPE_SERVICEACCOUNT) return this.serviceAccounts;
|
||||
|
||||
return [];
|
||||
},
|
||||
|
||||
onFsTypeChange(row, newType) {
|
||||
row.fsType = newType;
|
||||
row.volumeName = DEFAULT_VOLUME_NAMES[newType] || '';
|
||||
row.resourceName = '';
|
||||
},
|
||||
|
||||
addRow() {
|
||||
if (this.canAddRow) {
|
||||
const usedTypes = this.rows.map((r) => r.fsType);
|
||||
const nextType = FS_TYPE_OPTIONS.find((opt) => !usedTypes.includes(opt.value))?.value || FS_TYPE_CONFIGMAP;
|
||||
|
||||
this.rows.push({
|
||||
fsType: nextType,
|
||||
volumeName: DEFAULT_VOLUME_NAMES[nextType] || '',
|
||||
resourceName: '',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
removeRow(index) {
|
||||
this.rows.splice(index, 1);
|
||||
},
|
||||
|
||||
mountCommands(row) {
|
||||
const vol = row.volumeName || '<volume-name>';
|
||||
|
||||
return `- mkdir -p /mnt/${ vol }\n- mount -t virtiofs ${ vol } /mnt/${ vol }`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vm-filesystem">
|
||||
<p class="mb-20">
|
||||
{{ t('harvester.virtualMachine.filesystem.description') }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-for="(row, index) in rows"
|
||||
:key="index"
|
||||
class="filesystem-row mb-15"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col span-3">
|
||||
<LabeledSelect
|
||||
:value="row.fsType"
|
||||
:label="t('harvester.virtualMachine.filesystem.type')"
|
||||
:options="fsTypeOptions(index)"
|
||||
:mode="mode"
|
||||
required
|
||||
@update:value="onFsTypeChange(row, $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col span-3">
|
||||
<LabeledInput
|
||||
v-model:value="row.volumeName"
|
||||
:label="t('harvester.virtualMachine.filesystem.volume')"
|
||||
:mode="mode"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col span-5">
|
||||
<LabeledSelect
|
||||
v-model:value="row.resourceName"
|
||||
:label="t('harvester.virtualMachine.filesystem.resource')"
|
||||
:options="resourceOptions(row.fsType)"
|
||||
:mode="mode"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isView"
|
||||
class="col span-1 remove-col"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn role-link remove-btn"
|
||||
@click="removeRow(index)"
|
||||
>
|
||||
{{ t('generic.remove') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Banner
|
||||
v-if="completedRows.length > 0"
|
||||
color="warning"
|
||||
class="mt-10"
|
||||
>
|
||||
<div>
|
||||
<MessageLink
|
||||
:to="{ hash: '#advanced' }"
|
||||
prefix-label="harvester.virtualMachine.filesystem.mountBannerHint"
|
||||
middle-label="harvester.virtualMachine.filesystem.mountBannerHintLink"
|
||||
suffix-label="harvester.virtualMachine.filesystem.mountBannerHintSuffix"
|
||||
/>
|
||||
<div class="pre-wrapper mt-10">
|
||||
<pre class="mt-5 mb-0">{{ allMountCommands }}</pre>
|
||||
<CopyToClipboard
|
||||
:text="allMountCommands"
|
||||
:show-label="false"
|
||||
class="icon-btn"
|
||||
action-color="bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Banner>
|
||||
|
||||
<button
|
||||
v-if="!isView && canAddRow"
|
||||
type="button"
|
||||
class="btn role-tertiary add"
|
||||
@click="addRow"
|
||||
>
|
||||
{{ t('harvester.virtualMachine.filesystem.add') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vm-filesystem {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.filesystem-row {
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.remove-col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pre-wrapper {
|
||||
position: relative;
|
||||
|
||||
pre {
|
||||
padding-right: 36px;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 5px;
|
||||
padding: 2px 4px;
|
||||
line-height: 1;
|
||||
|
||||
&:active {
|
||||
background-color: var(--success) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+15
-30
@@ -152,10 +152,6 @@ export default {
|
||||
return !rows.find((device) => !device.passthroughClaim);
|
||||
},
|
||||
|
||||
canManageGroup(rows = []) {
|
||||
return rows.length > 0 && rows.every((row) => row.canUpdate === true);
|
||||
},
|
||||
|
||||
changeRows(filterRows, parentSriov) {
|
||||
this['filterRows'] = filterRows;
|
||||
this['parentSriov'] = parentSriov;
|
||||
@@ -189,27 +185,22 @@ export default {
|
||||
v-trim-whitespace
|
||||
class="group-tab"
|
||||
>
|
||||
<div
|
||||
v-if="canManageGroup(group.rows)"
|
||||
class="group-actions"
|
||||
<button
|
||||
v-if="groupIsAllEnabled(group.rows)"
|
||||
type="button"
|
||||
class="btn btn-sm role-secondary mr-5"
|
||||
@click="e=>{disableGroup(group.rows); e.target.blur()}"
|
||||
>
|
||||
<button
|
||||
v-if="groupIsAllEnabled(group.rows)"
|
||||
type="button"
|
||||
class="btn btn-sm role-secondary mr-5"
|
||||
@click="e=>{disableGroup(group.rows); e.target.blur()}"
|
||||
>
|
||||
{{ t('harvester.pci.disableGroup') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="btn btn-sm role-secondary mr-5"
|
||||
@click="e=>{enableGroup(group.rows); e.target.blur()}"
|
||||
>
|
||||
{{ t('harvester.pci.enableGroup') }}
|
||||
</button>
|
||||
</div>
|
||||
{{ t('harvester.pci.disableGroup') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="btn btn-sm role-secondary mr-5"
|
||||
@click="e=>{enableGroup(group.rows); e.target.blur()}"
|
||||
>
|
||||
{{ t('harvester.pci.enableGroup') }}
|
||||
</button>
|
||||
<span v-clean-html="group.key" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -241,9 +232,3 @@ export default {
|
||||
</template>
|
||||
</ResourceTable>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-actions {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -52,21 +52,31 @@ export default {
|
||||
}
|
||||
|
||||
const selectedDevices = [];
|
||||
const oldFormatDevices = [];
|
||||
|
||||
const vmDevices = this.value?.domain?.devices?.hostDevices || [];
|
||||
const otherDevices = this.otherDevices(vmDevices).map(({ name }) => name);
|
||||
const vmDeviceNames = vmDevices.map(({ name }) => name);
|
||||
|
||||
this.pciDevices.forEach((row) => {
|
||||
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
|
||||
});
|
||||
|
||||
vmDevices.forEach(({ name }) => {
|
||||
if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
|
||||
vmDevices.forEach(({ name, deviceName }) => {
|
||||
const checkName = (deviceName || '').split('/')?.[1];
|
||||
|
||||
if (checkName && name.includes(checkName) && !otherDevices.includes(name)) {
|
||||
oldFormatDevices.push(name);
|
||||
} else if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
|
||||
selectedDevices.push(name);
|
||||
}
|
||||
});
|
||||
|
||||
this.selectedDevices = selectedDevices;
|
||||
if (oldFormatDevices.length > 0) {
|
||||
this.oldFormatDevices = oldFormatDevices;
|
||||
} else {
|
||||
this.selectedDevices = selectedDevices;
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
@@ -77,6 +87,7 @@ export default {
|
||||
selectedDevices: [],
|
||||
pciDeviceSchema: this.$store.getters['harvester/schemaFor'](HCI.PCI_DEVICE),
|
||||
showMatrix: false,
|
||||
oldFormatDevices: [],
|
||||
};
|
||||
},
|
||||
|
||||
@@ -188,6 +199,11 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
oldFormatDevicesHTML() {
|
||||
return this.oldFormatDevices.map((device) => {
|
||||
return `<li>${ device }</li>`;
|
||||
}).join('');
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -211,89 +227,101 @@ export default {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div
|
||||
v-if="oldFormatDevices.length > 0"
|
||||
class="row"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<Banner color="info">
|
||||
<MessageLink
|
||||
:to="toVGpuDevicesPage"
|
||||
prefix-label="harvester.pci.howToUseDeviceInVMCreation.prefix"
|
||||
middle-label="harvester.pci.howToUseDeviceInVMCreation.middle"
|
||||
suffix-label="harvester.pci.howToUseDeviceInVMCreation.suffix"
|
||||
/>
|
||||
</Banner>
|
||||
<Banner
|
||||
v-if="selectedDevices.length > 0"
|
||||
color="info"
|
||||
>
|
||||
<t k="harvester.pci.deviceInTheSameHost" />
|
||||
<Banner color="warning">
|
||||
<p v-clean-html="t('harvester.pci.oldFormatDevices.help', {oldFormatDevicesHTML}, true)" />
|
||||
</Banner>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="enabledDevices.length">
|
||||
<div v-else>
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model:value="selectedDevices"
|
||||
label="Available PCI Devices"
|
||||
searchable
|
||||
multiple
|
||||
taggable
|
||||
:options="deviceOpts"
|
||||
:mode="mode"
|
||||
>
|
||||
<template #option="option">
|
||||
<span>{{ option.value }} <span class="text-label">({{ option.displayLabel }})</span></span>
|
||||
</template>
|
||||
</LabeledSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="compatibleNodes.length && selectedDevices.length"
|
||||
class="row"
|
||||
>
|
||||
<div class="col span-12 text-muted">
|
||||
Compatible hosts:
|
||||
<!-- eslint-disable-next-line vue/no-parsing-error -->
|
||||
<span
|
||||
v-for="(node, idx) in compatibleNodes"
|
||||
:key="idx"
|
||||
>{{ node }}{{ idx < compatibleNodes.length-1 ? ', ' : '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="selectedDevices.length"
|
||||
class="text-error"
|
||||
>
|
||||
{{ t('harvester.pci.impossibleSelection') }}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm role-link pl-0"
|
||||
@click="e=>{showMatrix = !showMatrix; e.target.blur()}"
|
||||
>
|
||||
{{ showMatrix ? t('harvester.pci.hideCompatibility') : t('harvester.pci.showCompatibility') }}
|
||||
</button>
|
||||
<div
|
||||
v-if="showMatrix"
|
||||
class="row mt-20"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<CompatibilityMatrix
|
||||
:enabled-devices="enabledDevices"
|
||||
:devices-by-node="devicesByNode"
|
||||
:devices-in-use="devicesInUse"
|
||||
<Banner color="info">
|
||||
<MessageLink
|
||||
:to="toVGpuDevicesPage"
|
||||
prefix-label="harvester.pci.howToUseDeviceInVMCreation.prefix"
|
||||
middle-label="harvester.pci.howToUseDeviceInVMCreation.middle"
|
||||
suffix-label="harvester.pci.howToUseDeviceInVMCreation.suffix"
|
||||
/>
|
||||
</Banner>
|
||||
<Banner
|
||||
v-if="selectedDevices.length > 0"
|
||||
color="info"
|
||||
>
|
||||
<t k="harvester.pci.deviceInTheSameHost" />
|
||||
</Banner>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="enabledDevices.length">
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model:value="selectedDevices"
|
||||
label="Available PCI Devices"
|
||||
searchable
|
||||
multiple
|
||||
taggable
|
||||
:options="deviceOpts"
|
||||
:mode="mode"
|
||||
>
|
||||
<template #option="option">
|
||||
<span>{{ option.value }} <span class="text-label">({{ option.displayLabel }})</span></span>
|
||||
</template>
|
||||
</LabeledSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="compatibleNodes.length && selectedDevices.length"
|
||||
class="row"
|
||||
>
|
||||
<div class="col span-12 text-muted">
|
||||
Compatible hosts:
|
||||
<!-- eslint-disable-next-line vue/no-parsing-error -->
|
||||
<span
|
||||
v-for="(node, idx) in compatibleNodes"
|
||||
:key="idx"
|
||||
>{{ node }}{{ idx < compatibleNodes.length-1 ? ', ' : '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="selectedDevices.length"
|
||||
class="text-error"
|
||||
>
|
||||
{{ t('harvester.pci.impossibleSelection') }}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm role-link pl-0"
|
||||
@click="e=>{showMatrix = !showMatrix; e.target.blur()}"
|
||||
>
|
||||
{{ showMatrix ? t('harvester.pci.hideCompatibility') : t('harvester.pci.showCompatibility') }}
|
||||
</button>
|
||||
<div
|
||||
v-if="showMatrix"
|
||||
class="row mt-20"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<CompatibilityMatrix
|
||||
:enabled-devices="enabledDevices"
|
||||
:devices-by-node="devicesByNode"
|
||||
:devices-in-use="devicesInUse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-12">
|
||||
<DeviceList
|
||||
:schema="pciDeviceSchema"
|
||||
:devices="pciDevices"
|
||||
@submit.prevent
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-12">
|
||||
<DeviceList
|
||||
:schema="pciDeviceSchema"
|
||||
:devices="pciDevices"
|
||||
@submit.prevent
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -10,7 +10,6 @@ import { _VIEW } from '@shell/config/query-params';
|
||||
|
||||
import { NAMESPACE } from '@shell/config/types';
|
||||
import { HCI } from '../../types';
|
||||
import { getLoginAwareErrors } from '../../utils/error';
|
||||
|
||||
const _NEW = '_NEW';
|
||||
|
||||
@@ -215,9 +214,7 @@ export default {
|
||||
buttonCb(true);
|
||||
this.cancel();
|
||||
} catch (err) {
|
||||
const message = this.t('harvester.virtualMachine.genericLoginError');
|
||||
|
||||
this.errors = getLoginAwareErrors(err, message);
|
||||
this.errors = [err.message];
|
||||
buttonCb(false);
|
||||
}
|
||||
},
|
||||
|
||||
+15
-37
@@ -64,12 +64,6 @@ export default {
|
||||
value: 'status.productID',
|
||||
sort: ['status.productID', 'status.vendorID']
|
||||
},
|
||||
{
|
||||
name: 'classType',
|
||||
labelKey: 'harvester.usb.classType',
|
||||
value: 'status.classType',
|
||||
sort: ['status.classType']
|
||||
},
|
||||
];
|
||||
|
||||
if (!isSingleProduct) {
|
||||
@@ -113,11 +107,6 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
canManageGroup(rows = []) {
|
||||
return rows.every((row) => row.canUpdate === true);
|
||||
},
|
||||
|
||||
groupIsAllEnabled(rows = []) {
|
||||
return !rows.find((device) => !device.passthroughClaim);
|
||||
},
|
||||
@@ -158,27 +147,22 @@ export default {
|
||||
v-trim-whitespace
|
||||
class="group-tab"
|
||||
>
|
||||
<div
|
||||
v-if="canManageGroup(group.rows)"
|
||||
class="group-actions"
|
||||
<button
|
||||
v-if="groupIsAllEnabled(group.rows)"
|
||||
type="button"
|
||||
class="btn btn-sm role-secondary mr-5"
|
||||
@click="e=>{disableGroup(group.rows); e.target.blur()}"
|
||||
>
|
||||
<button
|
||||
v-if="groupIsAllEnabled(group.rows)"
|
||||
type="button"
|
||||
class="btn btn-sm role-secondary mr-5"
|
||||
@click="e=>{disableGroup(group.rows); e.target.blur()}"
|
||||
>
|
||||
{{ t('harvester.usb.disableGroup') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="btn btn-sm role-secondary mr-5"
|
||||
@click="e=>{enableGroup(group.rows); e.target.blur()}"
|
||||
>
|
||||
{{ t('harvester.usb.enableGroup') }}
|
||||
</button>
|
||||
</div>
|
||||
{{ t('harvester.usb.disableGroup') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="btn btn-sm role-secondary mr-5"
|
||||
@click="e=>{enableGroup(group.rows); e.target.blur()}"
|
||||
>
|
||||
{{ t('harvester.usb.enableGroup') }}
|
||||
</button>
|
||||
<span v-clean-html="group.key" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -191,9 +175,3 @@ export default {
|
||||
</template>
|
||||
</ResourceTable>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-actions {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -475,6 +475,7 @@ export default {
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm bg-primary mr-15 mb-10"
|
||||
:disabled="rows.length === 0"
|
||||
@click="addVolume(SOURCE_TYPE.NEW)"
|
||||
>
|
||||
{{ t('harvester.virtualMachine.volume.addVolume') }}
|
||||
|
||||
@@ -37,7 +37,6 @@ import Network from './VirtualMachineNetwork';
|
||||
import Volume from './VirtualMachineVolume';
|
||||
import SSHKey from './VirtualMachineSSHKey';
|
||||
import Reserved from './VirtualMachineReserved';
|
||||
import Filesystem from './VirtualMachineFilesystem';
|
||||
import { Banner } from '@components/Banner';
|
||||
import MessageLink from '@shell/components/MessageLink';
|
||||
|
||||
@@ -73,7 +72,6 @@ export default {
|
||||
Banner,
|
||||
MessageLink,
|
||||
UsbDevices,
|
||||
Filesystem,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView, VM_MIXIN],
|
||||
@@ -93,36 +91,22 @@ export default {
|
||||
|
||||
const hostname = this.value.spec.template.spec.hostname || '';
|
||||
|
||||
const customizeDisplayName = !!(this.value.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME]);
|
||||
|
||||
return {
|
||||
cloneVM,
|
||||
count: 2,
|
||||
templateId: '',
|
||||
templateVersionId: '',
|
||||
namePrefix: '',
|
||||
isSingle: true,
|
||||
isOpen: false,
|
||||
count: 2,
|
||||
templateId: '',
|
||||
templateVersionId: '',
|
||||
namePrefix: '',
|
||||
isSingle: true,
|
||||
isOpen: false,
|
||||
hostname,
|
||||
isRestartImmediately,
|
||||
customizeDisplayName,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
// VM display name is stored as an annotation; bind a dedicated input to it
|
||||
// so we don't have to mutate metadata.name (which would break the k8s PUT).
|
||||
displayName: {
|
||||
get() {
|
||||
return this.value.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME] || '';
|
||||
},
|
||||
set(val) {
|
||||
this.value.setAnnotation(HCI_ANNOTATIONS.VM_DISPLAY_NAME, val);
|
||||
},
|
||||
},
|
||||
|
||||
to() {
|
||||
return {
|
||||
name: 'harvester-c-cluster-resource',
|
||||
@@ -234,9 +218,6 @@ export default {
|
||||
usbPassthroughEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
|
||||
},
|
||||
filesystemEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('supportFilesystem');
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -308,12 +289,6 @@ export default {
|
||||
this.getInitConfig({ value: this.value, init: this.isCreate });
|
||||
}
|
||||
},
|
||||
|
||||
customizeDisplayName(neu) {
|
||||
if (!neu) {
|
||||
this.value.setAnnotation(HCI_ANNOTATIONS.VM_DISPLAY_NAME, '');
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
@@ -346,7 +321,6 @@ export default {
|
||||
const diskRows = this.getDiskRows(this.value);
|
||||
|
||||
this['diskRows'] = diskRows;
|
||||
this['filesystemRows'] = this.getFilesystemRows(this.value);
|
||||
const templateId = this.$route.query.templateId;
|
||||
const templateVersionId = this.$route.query.versionId;
|
||||
|
||||
@@ -636,33 +610,6 @@ export default {
|
||||
</template>
|
||||
</NameNsDescription>
|
||||
|
||||
<div v-if="isSingle">
|
||||
<div class="row mb-20">
|
||||
<div class="col span-12">
|
||||
<Checkbox
|
||||
v-model:value="customizeDisplayName"
|
||||
class="check"
|
||||
type="checkbox"
|
||||
:label="t('harvester.virtualMachine.input.customizeDisplayName')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="customizeDisplayName"
|
||||
class="row mb-20"
|
||||
>
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="displayName"
|
||||
:mode="mode"
|
||||
:label="t('harvester.virtualMachine.input.displayName')"
|
||||
:placeholder="t('harvester.virtualMachine.input.displayNamePlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Checkbox
|
||||
v-if="isCreate"
|
||||
v-model:value="useTemplate"
|
||||
@@ -836,23 +783,10 @@ export default {
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
v-if="filesystemEnabled"
|
||||
name="filesystem"
|
||||
:label="t('harvester.tab.filesystem')"
|
||||
:weight="-9"
|
||||
>
|
||||
<Filesystem
|
||||
v-model:value="filesystemRows"
|
||||
:mode="mode"
|
||||
:namespace="value.metadata.namespace"
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="labels"
|
||||
:label="t('generic.labels')"
|
||||
:weight="-10"
|
||||
:weight="-9"
|
||||
>
|
||||
<Banner color="info">
|
||||
<t k="harvester.virtualMachine.labels.banner" />
|
||||
@@ -871,7 +805,7 @@ export default {
|
||||
<Tab
|
||||
name="instanceLabel"
|
||||
:label="t('harvester.tab.instanceLabel')"
|
||||
:weight="-11"
|
||||
:weight="-10"
|
||||
>
|
||||
<Banner color="info">
|
||||
<t k="harvester.virtualMachine.instanceLabels.banner" />
|
||||
@@ -892,7 +826,7 @@ export default {
|
||||
<Tab
|
||||
name="annotations"
|
||||
:label="t('harvester.tab.annotations')"
|
||||
:weight="-12"
|
||||
:weight="-11"
|
||||
>
|
||||
<Banner color="info">
|
||||
<t k="harvester.virtualMachine.annotations.banner" />
|
||||
@@ -913,7 +847,7 @@ export default {
|
||||
<Tab
|
||||
name="advanced"
|
||||
:label="t('harvester.tab.advanced')"
|
||||
:weight="-13"
|
||||
:weight="-12"
|
||||
>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
|
||||
@@ -20,7 +20,12 @@ export default {
|
||||
|
||||
computed: {
|
||||
vmiResource() {
|
||||
return this.$store.getters['harvester/byId'](HCI.VMI, this.vmResource?.id) || null;
|
||||
const vmiList = this.$store.getters['harvester/all'](HCI.VMI) || [];
|
||||
const vmi = vmiList.find( (VMI) => {
|
||||
return VMI?.metadata?.ownerReferences?.[0]?.uid === this.vmResource?.metadata?.uid;
|
||||
});
|
||||
|
||||
return vmi;
|
||||
},
|
||||
migrationState() {
|
||||
return this.vmiResource?.migrationState?.status || '';
|
||||
|
||||
@@ -14,6 +14,20 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
|
||||
allNodeNetwork: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
allClusterNetwork: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
|
||||
@@ -144,10 +144,6 @@ harvester:
|
||||
migration:
|
||||
failedMessage: Latest migration failed!
|
||||
title: Migration
|
||||
vmMigrationTitle: '{count, plural, one {Migrating # VM} other {Migrating # VMs}}'
|
||||
selectedVMs: "The following virtual machine(s) will be migrated to the target node"
|
||||
unknownNode: (unknown node)
|
||||
alreadyOnTarget: Already on Target
|
||||
fields:
|
||||
nodeName:
|
||||
label: Target Node
|
||||
@@ -252,7 +248,6 @@ harvester:
|
||||
suspendSchedule: Suspend
|
||||
restoreExistingVM: Replace Existing
|
||||
migrate: Migrate
|
||||
vmMigrate: Virtual Machine Migration
|
||||
cpuAndMemoryHotplug: Edit CPU and Memory
|
||||
abortMigration: Abort Migration
|
||||
storageMigration: Storage Migration
|
||||
@@ -303,7 +298,6 @@ harvester:
|
||||
phase: Phase
|
||||
attachedVM: Attached Virtual Machine
|
||||
cpuManager: CPU Manager
|
||||
routeConnectivityTooltip: Connectivity between the VM network and the management network, which the Harvester nodes are connected to.
|
||||
fingerprint: Fingerprint
|
||||
value: Value
|
||||
actions: Actions
|
||||
@@ -355,7 +349,6 @@ harvester:
|
||||
snapshots: Snapshots
|
||||
instanceLabel: Instance Labels
|
||||
annotations: Annotations
|
||||
filesystem: Filesystem Volume
|
||||
fields:
|
||||
version: Version
|
||||
name: Name
|
||||
@@ -392,6 +385,21 @@ harvester:
|
||||
middle: vGPU Devices
|
||||
suffix: page.
|
||||
deviceInTheSameHost: 'You can only select devices on the same host.'
|
||||
oldFormatDevices:
|
||||
help: |-
|
||||
<p>
|
||||
The following PCI devices are using the old naming convention and need to be updated in the YAML file:
|
||||
</p>
|
||||
<ul>
|
||||
{oldFormatDevicesHTML}
|
||||
</ul>
|
||||
<p>
|
||||
Please use the following instructions to update the virtual machine:
|
||||
</p>
|
||||
<ol>
|
||||
<li>Stop the virtual machine, edit the virtual machine YAML, and remove the <Code>hostDevices</Code> section, and save virtual machine the changes to the YAML file.</li>
|
||||
<li>Edit the virtual machine, and add the already enabled PCI Device from the list of available PCIDevices, and save and start VM.</li>
|
||||
</ol>
|
||||
showCompatibility: Show device compatibility matrix
|
||||
hideCompatibility: Hide device compatibility matrix
|
||||
claimError: Error enabling passthrough on {name}
|
||||
@@ -409,8 +417,6 @@ harvester:
|
||||
suffix: to enable the add-on to successfully manage your PCI devices.
|
||||
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.
|
||||
disableResourcePooling: Disable Resource Pooling
|
||||
disableResourcePoolingDescription: Assigns this device a unique resource name so it can be pinned to a specific VM, instead of sharing the pool with other identical devices.
|
||||
|
||||
devices:
|
||||
matrixHostName: Host Name
|
||||
@@ -720,7 +726,6 @@ harvester:
|
||||
other {Start}
|
||||
} Now
|
||||
createSSHKey: Create a New...
|
||||
genericLoginError: Authentication failed. Please re-log in and try again.
|
||||
installAgent: Install guest agent
|
||||
enableUsb: Enable USB Tablet
|
||||
advancedOptions:
|
||||
@@ -818,18 +823,6 @@ harvester:
|
||||
username: Username
|
||||
password: Password
|
||||
reservedMemory: Reserved Memory
|
||||
customizeDisplayName: Customize virtual machine display name
|
||||
displayNamePlaceholder: Virtual machine alias name
|
||||
displayName: Display Name
|
||||
filesystem:
|
||||
description: Harvester supports filesystem volumes for VM via virtiofs.
|
||||
type: Filesystem Type
|
||||
volume: Volume
|
||||
resource: Resource
|
||||
add: Add
|
||||
mountBannerHint: "Please update the mount path (e.g. /mnt/appconfigfs) to your preferred location, then add the corresponding commands to the"
|
||||
mountBannerHintLink: "runcmd"
|
||||
mountBannerHintSuffix: "in Advanced tab User Data."
|
||||
machineTypeTip: 'Specify a processor architecture to emulate. To see a list of supported architectures, run: qemu-system-x86_64 -cpu ?'
|
||||
detail:
|
||||
tabs:
|
||||
@@ -1118,7 +1111,6 @@ harvester:
|
||||
replaceExisting: Replace existing
|
||||
virtualMachineName: Virtual Machine Name
|
||||
keepMacAddress: Keep MAC Address
|
||||
haltAfterRestore: Keep powered off after restore
|
||||
matchTarget: The current backup target does not match the existing one.
|
||||
progress:
|
||||
details: Volume details
|
||||
@@ -1156,9 +1148,6 @@ harvester:
|
||||
placeholder: key1=value1, key2=value2
|
||||
dhcpOptionBanner: DHCP options is a key/value string concatenate with comma. For more detail, please refer to <a href="https://kubeovn.github.io/docs/v1.13.x/en/kubevirt/dhcp/" target="_blank">KubOVN document</a>
|
||||
tooltip: Enable DHCP server for this subnet. When enabled, VMs can automatically obtain IP addresses from this subnet.
|
||||
externalConnectivity:
|
||||
label: NAT Outgoing
|
||||
tooltip: Enable NAT for VMs using this subnet to access external networks.
|
||||
private:
|
||||
label: Private Subnet
|
||||
tooltip: Enable network isolation for this Subnet. When enabled, VMs can only communicate within this subnet, even if other subnets exist under the same VPC.
|
||||
@@ -1296,13 +1285,6 @@ harvester:
|
||||
rancherCluster:
|
||||
kubeConfig: Rancher KubeConfig
|
||||
removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted
|
||||
clusterPodSecurityStandard:
|
||||
whitelistedNamespaces:
|
||||
label: 'Whitelisted Namespaces'
|
||||
privilegedNamespaces:
|
||||
label: 'Privileged Namespaces'
|
||||
restrictedNamespaces:
|
||||
label: 'Restricted Namespaces'
|
||||
storageNetwork:
|
||||
range:
|
||||
placeholder: e.g. 172.16.0.0/24
|
||||
@@ -1526,7 +1508,6 @@ harvester:
|
||||
vmSnapshot:
|
||||
label: Virtual Machine Snapshots
|
||||
createText: Restore Snapshot
|
||||
title: Restore Virtual Machine
|
||||
snapshot: Snapshot
|
||||
|
||||
storage:
|
||||
@@ -1534,7 +1515,6 @@ harvester:
|
||||
useDefault: Use the default storage
|
||||
volumeEncryption: Volume Encryption
|
||||
secret: Secret
|
||||
volumeExpansionCheckbox: Enable Expansion
|
||||
migratable:
|
||||
label: Migratable
|
||||
numberOfReplicas:
|
||||
@@ -1784,8 +1764,6 @@ harvester:
|
||||
repository: Image Repository
|
||||
driver:
|
||||
location: Driver Location
|
||||
enable:
|
||||
title: Enable NVIDIA Driver Toolkit
|
||||
parsingSpecError:
|
||||
The field 'spec.valuesContent' has invalid format.
|
||||
usbController:
|
||||
@@ -1915,8 +1893,6 @@ harvester:
|
||||
|
||||
migconfiguration:
|
||||
label: vGPU MIG Configurations
|
||||
status:
|
||||
outOfSync: Out of Sync
|
||||
infoBanner: To configure the MIG configuration, please disable it first and re-enable after editing the configuration.
|
||||
profileSpec: Profile Specs
|
||||
profileStatus: Profile Status
|
||||
@@ -1979,7 +1955,6 @@ harvester:
|
||||
title: Cannot Disable Passthrough
|
||||
message: Please detach the device from the VM and save it first before disabling passthrough.
|
||||
enablePassthroughWarning: 'Please re-enable the USB device if the device path changes in the following situations:<br/> 1) Re-plugging the USB device.<br/> 2) Rebooting the node.<br/><br/>An incorrect device path may cause passthrough to fail.'
|
||||
classType: Class Type
|
||||
|
||||
harvesterVlanConfigMigrateDialog:
|
||||
targetClusterNetwork:
|
||||
@@ -2064,7 +2039,6 @@ advancedSettings:
|
||||
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
|
||||
'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
|
||||
'harv-instance-manager-resources': 'Configure resource percentage reservations for Longhorn instance manager V1 and V2. Valid instance manager CPU range between 0 - 40.'
|
||||
'harv-cluster-pod-security-standard': 'Enforce Kubernetes Pod Security Standards (PSS) at the cluster level.'
|
||||
|
||||
typeLabel:
|
||||
kubevirt.io.virtualmachine: |-
|
||||
@@ -2187,7 +2161,11 @@ typeLabel:
|
||||
one { PCI Device }
|
||||
other { PCI Devices }
|
||||
}
|
||||
|
||||
persistentvolumeclaim: |-
|
||||
{count, plural,
|
||||
one { Volume }
|
||||
other { Volumes }
|
||||
}
|
||||
network.harvesterhci.io.clusternetwork: |-
|
||||
{count, plural,
|
||||
one { Cluster Network }
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Banner } from '@components/Banner';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
import BadgeState from '@shell/components/formatter/BadgeStateFormatter';
|
||||
|
||||
import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers';
|
||||
import { NETWORK_ATTACHMENT, SCHEMA } from '@shell/config/types';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
@@ -112,7 +113,6 @@ export default {
|
||||
value: 'connectivity',
|
||||
labelKey: 'tableHeaders.routeConnectivity',
|
||||
formatter: 'NetworkRouteConnectivity',
|
||||
tooltip: 'harvester.tableHeaders.routeConnectivityTooltip',
|
||||
formatterOpts: { arbitrary: true },
|
||||
width: 130,
|
||||
},
|
||||
|
||||
@@ -12,18 +12,11 @@ import { HCI } from '../types';
|
||||
import HarvesterVmState from '../formatters/HarvesterVmState';
|
||||
import ConsoleBar from '../components/VMConsoleBar';
|
||||
|
||||
const ENCRYPTED_VOLUME_TOOLTIP_KEYS = {
|
||||
all: 'harvester.virtualMachine.volume.lockTooltip.all',
|
||||
partial: 'harvester.virtualMachine.volume.lockTooltip.partial',
|
||||
};
|
||||
|
||||
export const VM_HEADERS = [
|
||||
STATE,
|
||||
{
|
||||
...NAME,
|
||||
width: 350,
|
||||
value: 'nameDisplay',
|
||||
sort: ['nameDisplay'],
|
||||
},
|
||||
NAMESPACE,
|
||||
{
|
||||
@@ -100,9 +93,19 @@ export default {
|
||||
this.hasNode = true;
|
||||
}
|
||||
|
||||
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.NODE_NETWORK)) {
|
||||
_hash.nodeNetworks = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.NODE_NETWORK });
|
||||
}
|
||||
|
||||
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.CLUSTER_NETWORK)) {
|
||||
_hash.clusterNetworks = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK });
|
||||
}
|
||||
|
||||
const hash = await allHash(_hash);
|
||||
|
||||
this.allVMs = hash.vms;
|
||||
this.allNodeNetworks = hash.nodeNetworks || [];
|
||||
this.allClusterNetworks = hash.clusterNetworks || [];
|
||||
},
|
||||
|
||||
data() {
|
||||
@@ -110,6 +113,8 @@ export default {
|
||||
hasNode: false,
|
||||
allVMs: [],
|
||||
allVMIs: [],
|
||||
allNodeNetworks: [],
|
||||
allClusterNetworks: [],
|
||||
restartNotificationDisplayed: false,
|
||||
HCI
|
||||
};
|
||||
@@ -158,12 +163,6 @@ export default {
|
||||
*/
|
||||
hasBackUpRestoreInProgress() {
|
||||
return !!this.rows.find((r) => r.restoreResource && !r.restoreResource.fromSnapshot && !r.restoreResource.isComplete);
|
||||
},
|
||||
|
||||
vmRestartRequiredNames() {
|
||||
return this.allVMs
|
||||
.filter((vm) => vm.isRestartRequired)
|
||||
.map((vm) => vm.metadata.name);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -182,35 +181,53 @@ export default {
|
||||
},
|
||||
|
||||
watch: {
|
||||
vmRestartRequiredNames(vmNames) {
|
||||
const count = vmNames.length;
|
||||
allVMs: {
|
||||
handler(neu) {
|
||||
const vmNames = [];
|
||||
|
||||
if (count === 0 && this.restartNotificationDisplayed) {
|
||||
this.restartNotificationDisplayed = false;
|
||||
neu.forEach((vm) => {
|
||||
if (vm.isRestartRequired) {
|
||||
vmNames.push(vm.metadata.name);
|
||||
}
|
||||
});
|
||||
const count = vmNames.length;
|
||||
|
||||
return;
|
||||
}
|
||||
if ( count === 0 && this.restartNotificationDisplayed) {
|
||||
this.restartNotificationDisplayed = false;
|
||||
|
||||
if (count > 0) {
|
||||
// clear old notification before showing new one
|
||||
if (this.restartNotificationDisplayed) {
|
||||
this.$store.dispatch('growl/clear');
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.dispatch('growl/warning', {
|
||||
title: this.t('harvester.notification.restartRequired.title', { count }),
|
||||
message: this.t('harvester.notification.restartRequired.message', { vmNames: vmNames.join(', ') }),
|
||||
timeout: 10000,
|
||||
}, { root: true });
|
||||
this.restartNotificationDisplayed = true;
|
||||
}
|
||||
if (count > 0) {
|
||||
// clear old notification before showing new one
|
||||
if (this.restartNotificationDisplayed) {
|
||||
this.$store.dispatch('growl/clear');
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0 && vmNames.length > 0) {
|
||||
this.$store.dispatch('growl/warning', {
|
||||
title: this.t('harvester.notification.restartRequired.title', { count }),
|
||||
message: this.t('harvester.notification.restartRequired.message', { vmNames: vmNames.join(', ') }),
|
||||
timeout: 10000,
|
||||
}, { root: true });
|
||||
this.restartNotificationDisplayed = true;
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
lockIconTooltipMessage(row) {
|
||||
const key = ENCRYPTED_VOLUME_TOOLTIP_KEYS[row.encryptedVolumeType];
|
||||
const message = '';
|
||||
|
||||
return key ? this.t(key) : '';
|
||||
if (row.encryptedVolumeType === 'all') {
|
||||
return this.t('harvester.virtualMachine.volume.lockTooltip.all');
|
||||
} else if (row.encryptedVolumeType === 'partial') {
|
||||
return this.t('harvester.virtualMachine.volume.lockTooltip.partial');
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -236,6 +253,8 @@ export default {
|
||||
<HarvesterVmState
|
||||
class="vmstate"
|
||||
:row="scope.row"
|
||||
:all-node-network="allNodeNetworks"
|
||||
:all-cluster-network="allClusterNetworks"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -246,16 +265,16 @@ export default {
|
||||
v-if="scope.row.type !== HCI.VMI"
|
||||
:to="scope.row.detailLocation"
|
||||
>
|
||||
{{ scope.row.nameDisplay }}
|
||||
{{ scope.row.metadata.name }}
|
||||
<i
|
||||
v-if="scope.row.encryptedVolumeType !== 'none'"
|
||||
v-if="lockIconTooltipMessage(scope.row)"
|
||||
v-tooltip="lockIconTooltipMessage(scope.row)"
|
||||
class="icon icon-lock"
|
||||
:class="{'green-icon': scope.row.encryptedVolumeType === 'all', 'yellow-icon': scope.row.encryptedVolumeType === 'partial'}"
|
||||
/>
|
||||
</router-link>
|
||||
<span v-else>
|
||||
{{ scope.row.nameDisplay }}
|
||||
{{ scope.row.metadata.name }}
|
||||
</span>
|
||||
<ConsoleBar
|
||||
:resource-type="scope.row"
|
||||
|
||||
@@ -12,7 +12,7 @@ import { base64Decode } from '@shell/utils/crypto';
|
||||
import { formatSi, parseSi } from '@shell/utils/units';
|
||||
import { _CLONE, _CREATE, _VIEW } from '@shell/config/query-params';
|
||||
import {
|
||||
PV, PVC, STORAGE_CLASS, NODE, SECRET, CONFIG_MAP, SERVICE_ACCOUNT, NETWORK_ATTACHMENT, NAMESPACE, LONGHORN
|
||||
PV, PVC, STORAGE_CLASS, NODE, SECRET, CONFIG_MAP, NETWORK_ATTACHMENT, NAMESPACE, LONGHORN
|
||||
} from '@shell/config/types';
|
||||
import { HOSTNAME } from '@shell/config/labels-annotations';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
@@ -25,7 +25,7 @@ import { HCI } from '../../types';
|
||||
import { parseVolumeClaimTemplates, EMPTY_IMAGE } from '../../utils/vm';
|
||||
import impl, { QGA_JSON, USB_TABLET } from './impl';
|
||||
import { GIBIBYTE } from '../../utils/unit';
|
||||
import { VOLUME_MODE, FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
|
||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||
|
||||
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
||||
|
||||
@@ -102,8 +102,6 @@ export default {
|
||||
vmims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM }),
|
||||
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
|
||||
secrets: this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }),
|
||||
configMaps: this.$store.dispatch(`${ inStore }/findAll`, { type: CONFIG_MAP }),
|
||||
serviceAccounts: this.$store.dispatch(`${ inStore }/findAll`, { type: SERVICE_ACCOUNT }),
|
||||
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
|
||||
longhornV2Engine: this.$store.dispatch(`${ inStore }/find`, { type: LONGHORN.SETTINGS, id: LONGHORN_V2_DATA_ENGINE }),
|
||||
};
|
||||
@@ -158,7 +156,6 @@ export default {
|
||||
imageId: '',
|
||||
diskRows: [],
|
||||
networkRows: [],
|
||||
filesystemRows: [],
|
||||
machineType: '',
|
||||
machineTypes: [],
|
||||
secretName: '',
|
||||
@@ -443,7 +440,6 @@ export default {
|
||||
this['imageId'] = imageId;
|
||||
|
||||
this['diskRows'] = diskRows;
|
||||
this['filesystemRows'] = this.getFilesystemRows(vm);
|
||||
|
||||
this.refreshYamlEditor();
|
||||
},
|
||||
@@ -645,80 +641,6 @@ export default {
|
||||
this.parseAccessCredentials();
|
||||
this.parseNetworkRows(this.networkRows);
|
||||
this.parseDiskRows(this.diskRows);
|
||||
this.parseFilesystemRows();
|
||||
},
|
||||
|
||||
getFilesystemRows(vm) {
|
||||
const _filesystems = vm.spec.template.spec.domain.devices?.filesystems || [];
|
||||
const _volumes = vm.spec.template.spec.volumes || [];
|
||||
|
||||
return _filesystems.map((fs) => {
|
||||
const volume = _volumes.find((v) => v.name === fs.name);
|
||||
let fsType = FILESYSTEM_SOURCE_TYPE.CONFIGMAP;
|
||||
let resourceName = '';
|
||||
|
||||
if (volume?.configMap) {
|
||||
fsType = FILESYSTEM_SOURCE_TYPE.CONFIGMAP;
|
||||
resourceName = volume.configMap.name;
|
||||
} else if (volume?.secret) {
|
||||
fsType = FILESYSTEM_SOURCE_TYPE.SECRET;
|
||||
resourceName = volume.secret.secretName;
|
||||
} else if (volume?.serviceAccount) {
|
||||
fsType = FILESYSTEM_SOURCE_TYPE.SERVICEACCOUNT;
|
||||
resourceName = volume.serviceAccount.serviceAccountName;
|
||||
}
|
||||
|
||||
return {
|
||||
fsType,
|
||||
volumeName: fs.name,
|
||||
resourceName,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
parseFilesystemRows() {
|
||||
const completedRows = this.filesystemRows.filter(
|
||||
(r) => r.fsType && r.volumeName && r.resourceName
|
||||
);
|
||||
|
||||
const filesystems = completedRows.map((r) => ({
|
||||
name: r.volumeName,
|
||||
virtiofs: {},
|
||||
}));
|
||||
|
||||
const fsVolumes = completedRows.map((r) => {
|
||||
if (r.fsType === FILESYSTEM_SOURCE_TYPE.CONFIGMAP) {
|
||||
return {
|
||||
name: r.volumeName,
|
||||
configMap: { name: r.resourceName },
|
||||
};
|
||||
} else if (r.fsType === FILESYSTEM_SOURCE_TYPE.SECRET) {
|
||||
return {
|
||||
name: r.volumeName,
|
||||
secret: { secretName: r.resourceName },
|
||||
};
|
||||
} else if (r.fsType === FILESYSTEM_SOURCE_TYPE.SERVICEACCOUNT) {
|
||||
return {
|
||||
name: r.volumeName,
|
||||
serviceAccount: { serviceAccountName: r.resourceName },
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}).filter(Boolean);
|
||||
|
||||
if (filesystems.length > 0) {
|
||||
this.spec.template.spec.domain.devices['filesystems'] = filesystems;
|
||||
} else {
|
||||
delete this.spec.template.spec.domain.devices['filesystems'];
|
||||
}
|
||||
|
||||
if (fsVolumes.length > 0) {
|
||||
if (!this.spec.template.spec.volumes) {
|
||||
this.spec.template.spec['volumes'] = [];
|
||||
}
|
||||
this.spec.template.spec.volumes.push(...fsVolumes);
|
||||
}
|
||||
},
|
||||
|
||||
parseOther() {
|
||||
@@ -985,22 +907,14 @@ export default {
|
||||
const specInterfaces = this.spec?.template?.spec?.domain?.devices?.interfaces;
|
||||
const mergedInterfaces = this.mergeInterfaceList(specInterfaces, interfaces);
|
||||
|
||||
const devices = {
|
||||
...this.spec.template.spec.domain.devices,
|
||||
interfaces: mergedInterfaces,
|
||||
};
|
||||
|
||||
if (this.isEdit && networkRow.length === 0) {
|
||||
devices.autoattachPodInterface = false;
|
||||
} else {
|
||||
delete devices.autoattachPodInterface;
|
||||
}
|
||||
|
||||
const spec = {
|
||||
...this.spec.template.spec,
|
||||
domain: {
|
||||
...this.spec.template.spec.domain,
|
||||
devices,
|
||||
devices: {
|
||||
...this.spec.template.spec.domain.devices,
|
||||
interfaces: mergedInterfaces,
|
||||
},
|
||||
},
|
||||
networks
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@ import HarvesterResource from './harvester';
|
||||
export default class MIGCONFIGURATION extends HarvesterResource {
|
||||
get _availableActions() {
|
||||
let out = super._availableActions;
|
||||
const canUpdate = !!this.linkFor('update');
|
||||
|
||||
out = out.map((action) => {
|
||||
if (action.action === 'showConfiguration') {
|
||||
@@ -27,13 +26,13 @@ export default class MIGCONFIGURATION extends HarvesterResource {
|
||||
out.push(
|
||||
{
|
||||
action: 'enableConfig',
|
||||
enabled: !this.isEnabled && canUpdate,
|
||||
enabled: !this.isEnabled,
|
||||
icon: 'icon icon-fw icon-dot',
|
||||
label: 'Enable',
|
||||
},
|
||||
{
|
||||
action: 'disableConfig',
|
||||
enabled: this.isEnabled && canUpdate,
|
||||
enabled: this.isEnabled,
|
||||
icon: 'icon icon-fw icon-dot-open',
|
||||
label: 'Disable',
|
||||
},
|
||||
@@ -63,27 +62,13 @@ export default class MIGCONFIGURATION extends HarvesterResource {
|
||||
}
|
||||
|
||||
get stateDisplay() {
|
||||
if (this.configStatus === 'out-of-sync') {
|
||||
return this.t('harvester.migconfiguration.status.outOfSync');
|
||||
}
|
||||
|
||||
return this.actualState;
|
||||
}
|
||||
|
||||
get stateDescription() {
|
||||
if (this.status?.message) {
|
||||
return this.status.message;
|
||||
}
|
||||
|
||||
return super.stateDescription;
|
||||
}
|
||||
|
||||
get stateColor() {
|
||||
if (this.configStatus === 'out-of-sync') {
|
||||
return 'text-warning';
|
||||
}
|
||||
const state = this.actualState;
|
||||
|
||||
return colorForState(this.actualState);
|
||||
return colorForState(state);
|
||||
}
|
||||
|
||||
get isEnabled() {
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class PCIDevice extends SteveModel {
|
||||
out.push(
|
||||
{
|
||||
action: 'enablePassthroughBulk',
|
||||
enabled: !this.isEnabling && !this.isvGPUDevice && this.canUpdate,
|
||||
enabled: !this.isEnabling && !this.isvGPUDevice,
|
||||
icon: 'icon icon-fw icon-dot',
|
||||
label: 'Enable Passthrough',
|
||||
bulkable: true,
|
||||
@@ -43,7 +43,7 @@ export default class PCIDevice extends SteveModel {
|
||||
},
|
||||
{
|
||||
action: 'disablePassthrough',
|
||||
enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice && this.canUpdate,
|
||||
enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice,
|
||||
icon: 'icon icon-fw icon-dot-open',
|
||||
label: 'Disable Passthrough',
|
||||
bulkable: true,
|
||||
@@ -54,10 +54,6 @@ export default class PCIDevice extends SteveModel {
|
||||
return out;
|
||||
}
|
||||
|
||||
get canUpdate() {
|
||||
return !!this.linkFor('update');
|
||||
}
|
||||
|
||||
get isvGPUDevice() {
|
||||
if (!this.vGPUAsPCIDeviceFeatureEnabled) {
|
||||
return false;
|
||||
|
||||
@@ -16,13 +16,13 @@ export default class SRIOVDevice extends SteveModel {
|
||||
out.push(
|
||||
{
|
||||
action: 'enableDevice',
|
||||
enabled: !this.isEnabled && this.canUpdate,
|
||||
enabled: !this.isEnabled,
|
||||
icon: 'icon icon-fw icon-dot',
|
||||
label: 'Enable',
|
||||
},
|
||||
{
|
||||
action: 'disableDevice',
|
||||
enabled: this.isEnabled && this.canUpdate,
|
||||
enabled: this.isEnabled,
|
||||
icon: 'icon icon-fw icon-dot-open',
|
||||
label: 'Disable',
|
||||
},
|
||||
@@ -31,10 +31,6 @@ export default class SRIOVDevice extends SteveModel {
|
||||
return out;
|
||||
}
|
||||
|
||||
get canUpdate() {
|
||||
return !!this.linkFor('update');
|
||||
}
|
||||
|
||||
get canYaml() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export default class USBDevice extends SteveModel {
|
||||
out.push(
|
||||
{
|
||||
action: 'enablePassthroughBulk',
|
||||
enabled: !this.passthroughClaim && !this.status.enabled && this.canUpdate,
|
||||
enabled: !this.passthroughClaim && !this.status.enabled,
|
||||
icon: 'icon icon-fw icon-dot',
|
||||
label: 'Enable Passthrough',
|
||||
bulkable: true,
|
||||
@@ -42,7 +42,7 @@ export default class USBDevice extends SteveModel {
|
||||
},
|
||||
{
|
||||
action: 'disablePassthrough',
|
||||
enabled: this.status.enabled && this.canUpdate,
|
||||
enabled: this.status.enabled,
|
||||
icon: 'icon icon-fw icon-dot-open',
|
||||
label: 'Disable Passthrough',
|
||||
bulkable: true,
|
||||
@@ -53,10 +53,6 @@ export default class USBDevice extends SteveModel {
|
||||
return out;
|
||||
}
|
||||
|
||||
get canUpdate() {
|
||||
return !!this.linkFor('update');
|
||||
}
|
||||
|
||||
get canYaml() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -27,18 +27,17 @@ const STATUS_DISPLAY = {
|
||||
export default class VGpuDevice extends SteveModel {
|
||||
get _availableActions() {
|
||||
const out = super._availableActions;
|
||||
const canUpdate = !!this.linkFor('update');
|
||||
|
||||
out.push(
|
||||
{
|
||||
action: 'enableVGpu',
|
||||
enabled: !this.isEnabled && canUpdate,
|
||||
enabled: !this.isEnabled,
|
||||
icon: 'icon icon-fw icon-dot',
|
||||
label: 'Enable',
|
||||
},
|
||||
{
|
||||
action: 'disableVGpu',
|
||||
enabled: this.isEnabled && canUpdate,
|
||||
enabled: this.isEnabled,
|
||||
icon: 'icon icon-fw icon-dot-open',
|
||||
label: 'Disable',
|
||||
bulkable: true,
|
||||
|
||||
@@ -24,7 +24,7 @@ const OBSCURE_NAMESPACE_PREFIX = [
|
||||
|
||||
export default class HciNamespace extends namespace {
|
||||
get _availableActions() {
|
||||
let out = super._availableActions;
|
||||
const out = super._availableActions;
|
||||
const remove = out.findIndex((a) => a.action === 'promptRemove');
|
||||
|
||||
const promptRemove = {
|
||||
@@ -53,16 +53,6 @@ export default class HciNamespace extends namespace {
|
||||
insertAt(out, out.length - 1, promptRemove);
|
||||
insertAt(out, out.length - 5, editQuotaAction);
|
||||
|
||||
const canUpdate = !!this.linkFor('update');
|
||||
|
||||
out = out.map((action) => {
|
||||
if (['move'].includes(action.action)) {
|
||||
return { ...action, enabled: action.enabled && canUpdate };
|
||||
}
|
||||
|
||||
return action;
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,20 +5,6 @@ import Secret from '@shell/models/secret';
|
||||
import { NAMESPACE } from '@shell/config/types';
|
||||
|
||||
export default class HciSecret extends Secret {
|
||||
get _availableActions() {
|
||||
let out = super._availableActions;
|
||||
|
||||
out = out.map((action) => {
|
||||
if (['download'].includes(action.action)) {
|
||||
return { ...action, enabled: !!this.linkFor('update') };
|
||||
}
|
||||
|
||||
return action;
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// prevent harvester secret detail page be overridden.
|
||||
// See isFullPageOverride in https://github.com/rancher/dashboard/blob/master/shell/components/ResourceDetail/index.vue
|
||||
get fullDetailPageOverride() {
|
||||
|
||||
@@ -96,10 +96,6 @@ export default class HciStorageClass extends StorageClass {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
|
||||
}
|
||||
|
||||
get expandOnlineEncryptedVolumeFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('expandOnlineEncryptedVolume');
|
||||
}
|
||||
|
||||
get thirdPartyStorageFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||
}
|
||||
@@ -110,15 +106,6 @@ export default class HciStorageClass extends StorageClass {
|
||||
|
||||
get availableActions() {
|
||||
let out = super.availableActions || [];
|
||||
const canUpdate = !!this.linkFor('update');
|
||||
|
||||
out = out.map((action) => {
|
||||
if (['setDefault', 'setAsDefault', 'resetDefault'].includes(action.action)) {
|
||||
return { ...action, enabled: canUpdate };
|
||||
}
|
||||
|
||||
return action;
|
||||
});
|
||||
|
||||
if (this.isInternalStorageClass()) {
|
||||
out = out.filter((action) => {
|
||||
|
||||
@@ -4,8 +4,6 @@ import { HCI as HCI_ANNOTATIONS } from '../config/labels-annotations';
|
||||
import HarvesterResource from './harvester';
|
||||
import { HCI } from '../types';
|
||||
|
||||
const HARVESTER_NVIDIA_DRIVER_TOOLKIT = 'harvester-system/nvidia-driver-toolkit';
|
||||
|
||||
export default class HciAddonConfig extends HarvesterResource {
|
||||
get availableActions() {
|
||||
const out = super._availableActions;
|
||||
@@ -21,10 +19,9 @@ export default class HciAddonConfig extends HarvesterResource {
|
||||
out.push(rancherDashboard);
|
||||
}
|
||||
|
||||
const canUpdate = !!this.linkFor('update');
|
||||
const toggleAddon = {
|
||||
action: 'toggleAddon',
|
||||
enabled: canUpdate,
|
||||
enabled: true,
|
||||
icon: this.spec.enabled ? 'icon icon-pause' : 'icon icon-play',
|
||||
label: this.spec.enabled ? this.t('generic.disable') : this.t('generic.enable'),
|
||||
};
|
||||
@@ -48,15 +45,6 @@ export default class HciAddonConfig extends HarvesterResource {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.spec.enabled && this.id === HARVESTER_NVIDIA_DRIVER_TOOLKIT) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources: [this],
|
||||
component: 'HarvesterEnableNvidiaDriverToolkit',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.spec.enabled = !this.spec.enabled;
|
||||
await this.save();
|
||||
} catch (err) {
|
||||
|
||||
@@ -3,20 +3,6 @@ import { findBy } from '@shell/utils/array';
|
||||
import HarvesterResource from './harvester';
|
||||
|
||||
export default class HciKeypair extends HarvesterResource {
|
||||
get _availableActions() {
|
||||
let out = super._availableActions;
|
||||
|
||||
out = out.map((action) => {
|
||||
if (['download'].includes(action.action)) {
|
||||
return { ...action, enabled: !!this.linkFor('update') };
|
||||
}
|
||||
|
||||
return action;
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
get stateDisplay() {
|
||||
const conditions = get(this, 'status.conditions');
|
||||
const status = (findBy(conditions, 'type', 'validated') || {}).status ;
|
||||
|
||||
@@ -19,18 +19,16 @@ export default class ScheduleVmBackup extends HarvesterResource {
|
||||
}
|
||||
});
|
||||
|
||||
const canUpdate = !!this.linkFor('update');
|
||||
|
||||
return [
|
||||
{
|
||||
action: 'resumeSchedule',
|
||||
enabled: canUpdate && ucFirst(this.state) === STATES.suspended.label,
|
||||
enabled: ucFirst(this.state) === STATES.suspended.label,
|
||||
icon: 'icons icon-play',
|
||||
label: this.t('harvester.action.resumeSchedule'),
|
||||
},
|
||||
{
|
||||
action: 'suspendSchedule',
|
||||
enabled: canUpdate && ucFirst(this.state) === STATES.active.label,
|
||||
enabled: ucFirst(this.state) === STATES.active.label,
|
||||
icon: 'icons icon-pause',
|
||||
label: this.t('harvester.action.suspendSchedule'),
|
||||
},
|
||||
|
||||
@@ -39,6 +39,9 @@ function isReady() {
|
||||
export default class HciVmImage extends HarvesterResource {
|
||||
get availableActions() {
|
||||
let out = super._availableActions;
|
||||
const toFilter = ['goToEditYaml'];
|
||||
|
||||
out = out.filter( (A) => !toFilter.includes(A.action));
|
||||
|
||||
// show `Clone` only when imageSource is `download`
|
||||
if (this.imageSource !== 'download') {
|
||||
@@ -52,7 +55,6 @@ export default class HciVmImage extends HarvesterResource {
|
||||
canCreateVM = false;
|
||||
}
|
||||
|
||||
const canCreateImage = !!this.$getters?.['schemaFor']?.(HCI.IMAGE)?.collectionMethods?.some((method) => method.toLowerCase() === 'post');
|
||||
const customActions = this.isReady ? [
|
||||
{
|
||||
action: 'createFromImage',
|
||||
@@ -62,13 +64,13 @@ export default class HciVmImage extends HarvesterResource {
|
||||
},
|
||||
{
|
||||
action: 'encryptImage',
|
||||
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted && canCreateImage,
|
||||
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
|
||||
icon: 'icon icon-lock',
|
||||
label: this.t('harvester.action.encryptImage'),
|
||||
},
|
||||
{
|
||||
action: 'decryptImage',
|
||||
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted && canCreateImage,
|
||||
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
|
||||
icon: 'icon icon-unlock',
|
||||
label: this.t('harvester.action.decryptImage'),
|
||||
},
|
||||
|
||||
@@ -23,17 +23,17 @@ export default class HciVmTemplateVersion extends HarvesterResource {
|
||||
});
|
||||
|
||||
const schema = this.$getters['schemaFor'](HCI.VM);
|
||||
let canCreateVM = false;
|
||||
let canCreateVM = true;
|
||||
|
||||
if (schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())) ) {
|
||||
canCreateVM = true;
|
||||
if ( schema && !schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())) ) {
|
||||
canCreateVM = false;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
action: 'launchFromTemplate',
|
||||
icon: 'icon icon-spinner',
|
||||
enabled: canCreateVM && this.isReady,
|
||||
disabled: !canCreateVM || !this.isReady,
|
||||
label: this.t('harvester.action.launchFormTemplate'),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -83,42 +83,6 @@ const VMIPhase = {
|
||||
|
||||
let productInStore;
|
||||
|
||||
let _podOwnerMap = null;
|
||||
let _podOwnerMapSource = null;
|
||||
|
||||
function getPodByOwnerName(rootGetters, inStore, ownerName) {
|
||||
const podList = rootGetters[`${ inStore }/all`](POD);
|
||||
|
||||
if (!Array.isArray(podList)) {
|
||||
return undefined;
|
||||
}
|
||||
// if not equals (usually means the pod list has been updated), we need to rebuild the map, otherwise we can reuse the map for better performance
|
||||
if (_podOwnerMapSource !== podList) {
|
||||
_podOwnerMap = new Map(); // use Map to store ownerReference name and pod mapping
|
||||
for (const pod of podList) {
|
||||
const refName = pod.metadata?.ownerReferences?.[0]?.name;
|
||||
|
||||
if (refName) {
|
||||
_podOwnerMap.set(refName, pod);
|
||||
}
|
||||
}
|
||||
_podOwnerMapSource = podList;
|
||||
}
|
||||
|
||||
return _podOwnerMap.get(ownerName);
|
||||
}
|
||||
|
||||
function getPvcsByNames(rootGetters, inStore, names) {
|
||||
const pvcList = rootGetters[`${ inStore }/all`](PVC);
|
||||
|
||||
if (!Array.isArray(pvcList)) {
|
||||
return [];
|
||||
}
|
||||
const uniqueNames = new Set(names);
|
||||
|
||||
return pvcList.filter((pvc) => uniqueNames.has(pvc.metadata?.name));
|
||||
}
|
||||
|
||||
const IgnoreMessages = ['pod has unbound immediate PersistentVolumeClaims'];
|
||||
|
||||
export default class VirtVm extends HarvesterResource {
|
||||
@@ -130,8 +94,6 @@ export default class VirtVm extends HarvesterResource {
|
||||
clone.action = 'goToCloneVM';
|
||||
}
|
||||
|
||||
const canCreateVMSSchedule = !!this.$getters?.['schemaFor']?.(HCI.SCHEDULE_VM_BACKUP)?.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase()));
|
||||
|
||||
return [
|
||||
{
|
||||
action: 'stopVM',
|
||||
@@ -164,7 +126,6 @@ export default class VirtVm extends HarvesterResource {
|
||||
},
|
||||
{
|
||||
action: 'restartVM',
|
||||
altAction: 'altRestartVM',
|
||||
enabled: !!this.actions?.restart,
|
||||
icon: 'icon icon-refresh',
|
||||
label: this.t('harvester.action.restart'),
|
||||
@@ -172,11 +133,10 @@ export default class VirtVm extends HarvesterResource {
|
||||
bulkAction: 'restartVM'
|
||||
},
|
||||
{
|
||||
action: 'softrebootVM',
|
||||
altAction: 'doSoftReboot',
|
||||
enabled: !!this.actions?.softreboot,
|
||||
icon: 'icon icon-pipeline',
|
||||
label: this.t('harvester.action.softreboot')
|
||||
action: 'softrebootVM',
|
||||
enabled: !!this.actions?.softreboot,
|
||||
icon: 'icon icon-pipeline',
|
||||
label: this.t('harvester.action.softreboot')
|
||||
},
|
||||
{
|
||||
action: 'startVM',
|
||||
@@ -211,7 +171,7 @@ export default class VirtVm extends HarvesterResource {
|
||||
},
|
||||
{
|
||||
action: 'createSchedule',
|
||||
enabled: canCreateVMSSchedule && this.schedulingVMBackupFeatureEnabled,
|
||||
enabled: this.schedulingVMBackupFeatureEnabled,
|
||||
icon: 'icon icon-history',
|
||||
label: this.t('harvester.action.createSchedule')
|
||||
},
|
||||
@@ -228,12 +188,10 @@ export default class VirtVm extends HarvesterResource {
|
||||
label: this.t('harvester.action.ejectCDROM')
|
||||
},
|
||||
{
|
||||
action: 'migrateVM',
|
||||
enabled: !!this.actions?.migrate,
|
||||
icon: 'icon icon-copy',
|
||||
label: this.t('harvester.action.vmMigrate'),
|
||||
bulkable: true,
|
||||
bulkAction: 'migrateVM'
|
||||
action: 'migrateVM',
|
||||
enabled: !!this.actions?.migrate,
|
||||
icon: 'icon icon-copy',
|
||||
label: this.t('harvester.action.migrate')
|
||||
},
|
||||
{
|
||||
action: 'abortMigrationVM',
|
||||
@@ -373,10 +331,6 @@ export default class VirtVm extends HarvesterResource {
|
||||
this.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
||||
}
|
||||
|
||||
altRestartVM() {
|
||||
this.doActionGrowl('restart', {});
|
||||
}
|
||||
|
||||
restartVM(resources = this) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
@@ -706,13 +660,16 @@ export default class VirtVm extends HarvesterResource {
|
||||
|
||||
get podResource() {
|
||||
const inStore = this.productInStore;
|
||||
|
||||
const vmiResource = this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id);
|
||||
const podList = this.$rootGetters[`${ inStore }/all`](POD);
|
||||
|
||||
if (!vmiResource?.metadata?.name) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getPodByOwnerName(this.$rootGetters, inStore, vmiResource.metadata.name);
|
||||
return podList.find((P) => {
|
||||
return (
|
||||
vmiResource?.metadata?.name &&
|
||||
vmiResource?.metadata?.name === P.metadata?.ownerReferences?.[0].name
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
get isPaused() {
|
||||
@@ -753,13 +710,17 @@ export default class VirtVm extends HarvesterResource {
|
||||
get vmi() {
|
||||
const inStore = this.productInStore;
|
||||
|
||||
return this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id);
|
||||
const vmis = this.$rootGetters[`${ inStore }/all`](HCI.VMI);
|
||||
|
||||
return vmis.find((VMI) => VMI.id === this.id);
|
||||
}
|
||||
|
||||
get volumes() {
|
||||
const pvcs = this.$rootGetters[`${ this.productInStore }/all`](PVC);
|
||||
|
||||
const volumeClaimNames = this.spec.template.spec.volumes?.map((v) => v.persistentVolumeClaim?.claimName).filter((v) => !!v) || [];
|
||||
|
||||
return getPvcsByNames(this.$rootGetters, this.productInStore, volumeClaimNames);
|
||||
return pvcs.filter((pvc) => volumeClaimNames.includes(pvc.metadata.name));
|
||||
}
|
||||
|
||||
get lvmVolumes() {
|
||||
@@ -792,6 +753,17 @@ export default class VirtVm extends HarvesterResource {
|
||||
return { status: 'VMI error', detailedMessage: vmiFailureCond.message };
|
||||
}
|
||||
|
||||
if ((this.vmi || this.isVMCreated) && this.podResource) {
|
||||
// const podStatus = this.podResource.getPodStatus;
|
||||
// if (POD_STATUS_ALL_ERROR.includes(podStatus?.status)) {
|
||||
// return {
|
||||
// ...podStatus,
|
||||
// status: 'LAUNCHER_POD_ERROR',
|
||||
// pod: this.podResource,
|
||||
// };
|
||||
// }
|
||||
}
|
||||
|
||||
return this?.vmi?.status?.phase;
|
||||
}
|
||||
|
||||
@@ -929,7 +901,9 @@ export default class VirtVm extends HarvesterResource {
|
||||
|
||||
const inStore = this.productInStore;
|
||||
|
||||
const res = this.$rootGetters[`${ inStore }/byId`](HCI.RESTORE, id);
|
||||
const allRestore = this.$rootGetters[`${ inStore }/all`](HCI.RESTORE);
|
||||
|
||||
const res = allRestore.find((O) => O.id === id);
|
||||
|
||||
if (res) {
|
||||
const allBackups = this.$rootGetters[`${ inStore }/all`](HCI.BACKUP);
|
||||
@@ -1099,6 +1073,42 @@ export default class VirtVm extends HarvesterResource {
|
||||
return out;
|
||||
}
|
||||
|
||||
get warningCount() {
|
||||
return this.resourcesStatus.warningCount;
|
||||
}
|
||||
|
||||
get errorCount() {
|
||||
return this.resourcesStatus.errorCount;
|
||||
}
|
||||
|
||||
get resourcesStatus() {
|
||||
const inStore = this.productInStore;
|
||||
const vmList = this.$rootGetters[`${ inStore }/all`](HCI.VM);
|
||||
let warningCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
vmList.forEach((vm) => {
|
||||
const status = vm.actualState;
|
||||
|
||||
if (status === VM_ERROR) {
|
||||
errorCount += 1;
|
||||
} else if (
|
||||
status === 'Stopping' ||
|
||||
status === 'Waiting' ||
|
||||
status === 'Pending' ||
|
||||
status === 'Starting' ||
|
||||
status === 'Terminating'
|
||||
) {
|
||||
warningCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
warningCount,
|
||||
errorCount
|
||||
};
|
||||
}
|
||||
|
||||
get volumeClaimTemplates() {
|
||||
return parseVolumeClaimTemplates(this);
|
||||
}
|
||||
@@ -1116,6 +1126,7 @@ export default class VirtVm extends HarvesterResource {
|
||||
get rootImageId() {
|
||||
let imageId = '';
|
||||
const inStore = this.productInStore;
|
||||
const pvcs = this.$rootGetters[`${ inStore }/all`](PVC) || [];
|
||||
|
||||
const volumes = this.spec.template.spec.volumes || [];
|
||||
|
||||
@@ -1125,7 +1136,9 @@ export default class VirtVm extends HarvesterResource {
|
||||
});
|
||||
|
||||
if (!isNoExistingVolume) {
|
||||
const existingVolume = this.$rootGetters[`${ inStore }/byId`](PVC, `${ this.metadata.namespace }/${ firstVolumeName }`);
|
||||
const existingVolume = pvcs.find(
|
||||
(P) => P.id === `${ this.metadata.namespace }/${ firstVolumeName }`
|
||||
);
|
||||
|
||||
if (existingVolume) {
|
||||
return existingVolume?.metadata?.annotations?.[
|
||||
@@ -1274,10 +1287,6 @@ export default class VirtVm extends HarvesterResource {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('schedulingVMBackup');
|
||||
}
|
||||
|
||||
get nameDisplay() {
|
||||
return this.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME] || this.metadata?.name || this.id;
|
||||
}
|
||||
|
||||
get volumeEncryptionFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
|
||||
}
|
||||
@@ -1307,7 +1316,8 @@ export default class VirtVm extends HarvesterResource {
|
||||
}
|
||||
|
||||
get isBackupTargetUnavailable() {
|
||||
const backupTargetSetting = this.$rootGetters['harvester/byId'](HCI.SETTING, 'backup-target');
|
||||
const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || [];
|
||||
const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target');
|
||||
|
||||
return isBackupTargetSettingUnavailable(backupTargetSetting);
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
|
||||
import shellProject from '@shell/models/management.cattle.io.project';
|
||||
|
||||
// This model controls `Project / Namespace` page in rancher integration mode
|
||||
// Extend management.cattle.io.project model from shell
|
||||
export default class Project extends shellProject {
|
||||
get _availableActions() {
|
||||
const canUpdate = !!this.linkFor('update');
|
||||
|
||||
// disable `Edit Config` action if user does not have update permission.
|
||||
return super._availableActions.map((action) => {
|
||||
if (action.action === 'goToEdit') {
|
||||
return { ...action, enabled: canUpdate };
|
||||
}
|
||||
|
||||
return action;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -57,11 +57,7 @@ export default class HciVlanConfig extends HarvesterResource {
|
||||
get _availableActions() {
|
||||
const out = super._availableActions;
|
||||
|
||||
const canMigrate = !!this.$getters?.['schemaFor']?.(HCI.VLAN_CONFIG)?.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase()));
|
||||
|
||||
if (canMigrate) {
|
||||
insertAt(out, 0, this.migrateAction);
|
||||
}
|
||||
insertAt(out, 0, this.migrateAction);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "harvester",
|
||||
"description": "Rancher UI Extension for Harvester",
|
||||
"version": "1.9.0-dev",
|
||||
"version": "1.8.2",
|
||||
"private": false,
|
||||
"rancher": {
|
||||
"annotations": {
|
||||
|
||||
@@ -123,26 +123,5 @@ export default {
|
||||
const clusterId = currentCluster.id;
|
||||
|
||||
return projectsInAllClusters.filter((project: any) => project.spec.clusterName === clusterId && project.nameDisplay !== 'System');
|
||||
},
|
||||
|
||||
// Few harvester resources name and REAL resource are different. E.g. HCI.NETWORK_ATTACHMENT page resource is NETWORK_ATTACHMENT.
|
||||
// Check in config/harvester-cluster.js for more details.
|
||||
// We need to look up the schema by resource name, and fallback to find using real resource name
|
||||
schemaFor: (state, getters, rootState, rootGetters) => (type, _fuzzy = false, _allowThrow = true) => {
|
||||
// follow the same logic as type-map/schemaFor in /dashboard/shell/plugins/dashboard-store/getters.js
|
||||
const normalizedType = getters.normalizeType(type);
|
||||
const schemas = state.types['schema'];
|
||||
const out = schemas?.map?.get(normalizedType);
|
||||
|
||||
if (out) return out;
|
||||
|
||||
// if not found, use the resource mapping in configureType for a second try
|
||||
const resourceType = rootGetters['type-map/optionsFor'](type)?.resource;
|
||||
if (resourceType && resourceType !== type) {
|
||||
const normalizedResource = getters.normalizeType(resourceType);
|
||||
return schemas?.map?.get(normalizedResource) || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -31,42 +31,22 @@ export function registerAddonSideNav(store, productName, {
|
||||
}, 600);
|
||||
};
|
||||
|
||||
const hasAccessibleSchema = (t) => {
|
||||
try {
|
||||
return !!store.getters[`${ productName }/schemaFor`]?.(t);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const showTypes = (visibleTypes) => {
|
||||
store.commit('type-map/basicType', {
|
||||
product: productName,
|
||||
group: navGroup,
|
||||
types: visibleTypes
|
||||
});
|
||||
};
|
||||
|
||||
const hideTypes = () => {
|
||||
const basicTypes = store.state['type-map'].basicTypes[productName];
|
||||
|
||||
if (basicTypes) {
|
||||
types.forEach((t) => delete basicTypes[t]);
|
||||
}
|
||||
};
|
||||
|
||||
// Adds or removes the resource IDs from the product visibility whitelist.
|
||||
const setMenuVisibility = (visible) => {
|
||||
const accessibleTypes = visible ? types.filter(hasAccessibleSchema) : [];
|
||||
if (visible) {
|
||||
store.commit('type-map/basicType', {
|
||||
product: productName,
|
||||
group: navGroup,
|
||||
types
|
||||
});
|
||||
} else {
|
||||
// Manually delete the keys from the state object to hide them.
|
||||
const basicTypes = store.state['type-map'].basicTypes[productName];
|
||||
|
||||
// Always clear first to remove any previously-registered types that are
|
||||
// no longer accessible (e.g. partial permission changes like types=[A,B] where B is dropped).
|
||||
hideTypes();
|
||||
|
||||
if (accessibleTypes.length > 0) {
|
||||
showTypes(accessibleTypes);
|
||||
if (basicTypes) {
|
||||
types.forEach((t) => delete basicTypes[t]);
|
||||
}
|
||||
}
|
||||
|
||||
kickSideNav();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
const AUTH_ERROR_CODES = [401, 403, 404];
|
||||
|
||||
export function getLoginAwareErrors(err, message = '') {
|
||||
const errors = Array.isArray(err) ? err : (err ? [err] : []);
|
||||
|
||||
if (!errors.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const generic = message;
|
||||
|
||||
if (errors.some((e) => AUTH_ERROR_CODES.includes(e?._status || e?.response?.status))) {
|
||||
return [generic];
|
||||
}
|
||||
|
||||
const msgs = errors
|
||||
.map((e) => (typeof e === 'string' ? e : (e?.message || e?._statusText || '')))
|
||||
.filter(Boolean);
|
||||
|
||||
return msgs.length ? msgs : [generic];
|
||||
}
|
||||
@@ -27,6 +27,23 @@ OUTPUT_DIR=dist/${DIR}-embedded
|
||||
echo "Building..."
|
||||
COMMIT=${COMMIT} VERSION=${VERSION} OUTPUT_DIR=$OUTPUT_DIR ROUTER_BASE='/dashboard/' RESOURCE_BASE='/dashboard/' RANCHER_ENV=harvester yarn run build
|
||||
|
||||
if [ -v EMBED_PKG ]; then
|
||||
echo "Build and embed plugin from: $EMBED_PKG"
|
||||
PKG_FILE_NAME=${EMBED_PKG##*/}
|
||||
echo PKG_FILE_NAME: $PKG_FILE_NAME
|
||||
|
||||
PKG_NAME="${PKG_FILE_NAME/.tar.gz/""}"
|
||||
echo "Plugin name: '$PKG_NAME'"
|
||||
|
||||
# Fetch file, unpack and move to dist
|
||||
curl $EMBED_PKG --output $PKG_FILE_NAME
|
||||
OUTPUT_DIR_PKG=$OUTPUT_DIR/$PKG_NAME
|
||||
mkdir -p $OUTPUT_DIR_PKG
|
||||
tar xvfz $PKG_FILE_NAME -C $OUTPUT_DIR/$PKG_NAME
|
||||
echo "Plugin contents that will be served from $PKG_NAME"
|
||||
ls -alR $OUTPUT_DIR/$PKG_NAME
|
||||
fi
|
||||
|
||||
echo "Destroying..."
|
||||
find $OUTPUT_DIR -name "index.html" -mindepth 2 -exec rm {} \;
|
||||
find $OUTPUT_DIR -type d -empty -depth -exec rmdir {} \;
|
||||
|
||||
Reference in New Issue
Block a user