mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-01 03:36:04 +08:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
270977f884 | ||
|
|
50b4cf5248 | ||
|
|
cba37194b6 | ||
|
|
bbe0c4643f | ||
|
|
00584f1d19 | ||
|
|
4fa6470343 | ||
|
|
d550046189 | ||
|
|
5f32a9ade5 | ||
|
|
3f39d561be | ||
|
|
7c84269ed6 | ||
|
|
f63be73b11 | ||
|
|
fabc1cec21 |
+7
-5
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-class-static-block": "7.29.7",
|
||||
"@rancher/shell": "3.0.12-rc.7",
|
||||
"@rancher/shell": "3.0.13",
|
||||
"@vue-flow/background": "^1.3.0",
|
||||
"@vue-flow/controls": "^1.1.1",
|
||||
"@vue-flow/core": "^1.33.5",
|
||||
@@ -39,7 +39,7 @@
|
||||
"merge": "2.1.1",
|
||||
"node-forge": "1.4.0",
|
||||
"nth-check": "2.1.1",
|
||||
"qs": "6.15.3",
|
||||
"qs": "6.16.0",
|
||||
"roarr": "7.21.7",
|
||||
"semver": "7.8.5",
|
||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||
@@ -48,15 +48,17 @@
|
||||
"dev": "NODE_ENV=dev ./node_modules/.bin/vue-cli-service serve",
|
||||
"build": "./node_modules/.bin/vue-cli-service build",
|
||||
"clean": "./node_modules/@rancher/shell/scripts/clean",
|
||||
"lint": "./node_modules/.bin/eslint --max-warnings 0 --ext .js,.ts,.vue .",
|
||||
"lint:fix": "./node_modules/.bin/eslint --fix --max-warnings 0 --ext .js,.ts,.vue .",
|
||||
"lint": "./node_modules/@rancher/shell/scripts/lint --max-warnings 0 --ext .js,.ts,.vue .",
|
||||
"lint:fix": "./node_modules/@rancher/shell/scripts/lint --fix --max-warnings 0 --ext .js,.ts,.vue .",
|
||||
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
|
||||
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
|
||||
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
|
||||
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
|
||||
"commitlint": "commitlint --edit",
|
||||
"prepare": "husky",
|
||||
"agents:generate": "./scripts/generate-agent-and-persona-mds.sh"
|
||||
"agents:generate": "./scripts/generate-agent-and-persona-mds.sh",
|
||||
"prelint": "npm --prefix node_modules/@rancher/shell/eslint-legacy install eslint-plugin-jest@^24 --no-save --no-audit --no-fund --legacy-peer-deps --loglevel=error",
|
||||
"prelint:fix": "npm --prefix node_modules/@rancher/shell/eslint-legacy install eslint-plugin-jest@^24 --no-save --no-audit --no-fund --legacy-peer-deps --loglevel=error"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/load": "^19.8.1",
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.8 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.8 KiB |
@@ -53,7 +53,7 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
||||
computed: { ...mapGetters({ t: 'i18n/t', isHarvesterPrime: 'harvester-common/isHarvesterPrime' }) },
|
||||
|
||||
watch: {
|
||||
settings: {
|
||||
@@ -231,14 +231,21 @@ export default {
|
||||
v-if="setting.customized"
|
||||
class="modified"
|
||||
>
|
||||
Modified
|
||||
{{ t('advancedSettings.tags.modified.label') }}
|
||||
</span>
|
||||
<span
|
||||
v-if="setting.experimental"
|
||||
v-clean-tooltip="t('advancedSettings.experimental')"
|
||||
v-clean-tooltip="t('advancedSettings.tags.experimental.description')"
|
||||
class="experimental"
|
||||
>
|
||||
Experimental
|
||||
{{ t('advancedSettings.tags.experimental.label') }}
|
||||
</span>
|
||||
<span
|
||||
v-if="setting.preview"
|
||||
v-clean-tooltip="t('advancedSettings.tags.preview.description')"
|
||||
class="preview"
|
||||
>
|
||||
{{ t('advancedSettings.tags.preview.label') }}
|
||||
</span>
|
||||
</h1>
|
||||
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
|
||||
@@ -262,7 +269,8 @@ export default {
|
||||
v-if="!setting.hide"
|
||||
class="settings-value"
|
||||
>
|
||||
<pre v-if="setting.kind === 'json'">{{ setting.json }}</pre>
|
||||
<pre v-if="setting.id === 'branding' && isHarvesterPrime">{{ $store.getters['harvester-common/privateLabel'] }}</pre>
|
||||
<pre v-else-if="setting.kind === 'json'">{{ setting.json }}</pre>
|
||||
<pre v-else-if="setting.kind === 'multiline'">{{ setting.data.value || setting.data.default }}</pre>
|
||||
<pre v-else-if="setting.kind === 'enum'">{{ t(setting.enum) }}</pre>
|
||||
<pre v-else-if="setting.kind === 'custom' && setting.custom">{{ setting.custom }}</pre>
|
||||
@@ -352,21 +360,19 @@ export default {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modified {
|
||||
.modified,
|
||||
.experimental,
|
||||
.preview {
|
||||
margin-left: 10px;
|
||||
border: 1px solid var(--primary);
|
||||
border: 1px solid;
|
||||
border-radius: 5px;
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.experimental {
|
||||
margin-left: 10px;
|
||||
border: 1px solid var(--error);
|
||||
border-radius: 5px;
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.modified { border-color: var(--primary); }
|
||||
.experimental { border-color: var(--error); }
|
||||
.preview { border-color: var(--warning); }
|
||||
|
||||
.no-search-match {
|
||||
text-align: center;
|
||||
|
||||
@@ -15,7 +15,7 @@ import MappingColumn from './MappingColumn.vue';
|
||||
import StorageDefaultsModal from './StorageDefaultsModal.vue';
|
||||
|
||||
const DEFAULT_VOLUME_MODE = VOLUME_MODE.FILE_SYSTEM;
|
||||
const DEFAULT_ACCESS_MODES = [ACCESS_MODE.READ_WRITE_MANY];
|
||||
const DEFAULT_ACCESS_MODE = ACCESS_MODE?.READ_WRITE_MANY ?? 'ReadWriteMany';
|
||||
|
||||
const props = defineProps({
|
||||
providerName: { type: String, default: '' },
|
||||
@@ -268,9 +268,9 @@ const buildStorageEntries = () => {
|
||||
capacity: 0,
|
||||
target: '',
|
||||
volumeMode: DEFAULT_VOLUME_MODE,
|
||||
accessModes: [...DEFAULT_ACCESS_MODES],
|
||||
accessModes: [DEFAULT_ACCESS_MODE],
|
||||
inheritedVolumeMode: DEFAULT_VOLUME_MODE,
|
||||
inheritedAccessModes: [...DEFAULT_ACCESS_MODES],
|
||||
inheritedAccessModes: [DEFAULT_ACCESS_MODE],
|
||||
inheritedFromProvider: false,
|
||||
overridden: false,
|
||||
usedBy: [],
|
||||
@@ -310,9 +310,9 @@ const buildStorageEntriesFromProvider = (datastoresData) => {
|
||||
capacity: ds.capacity || 0,
|
||||
target: '',
|
||||
volumeMode: DEFAULT_VOLUME_MODE,
|
||||
accessModes: [...DEFAULT_ACCESS_MODES],
|
||||
accessModes: [DEFAULT_ACCESS_MODE],
|
||||
inheritedVolumeMode: DEFAULT_VOLUME_MODE,
|
||||
inheritedAccessModes: [...DEFAULT_ACCESS_MODES],
|
||||
inheritedAccessModes: [DEFAULT_ACCESS_MODE],
|
||||
inheritedFromProvider: false,
|
||||
overridden: false,
|
||||
usedBy: [],
|
||||
|
||||
@@ -8,16 +8,22 @@ import { useI18n } from '@shell/composables/useI18n';
|
||||
import { VOLUME_MODE, ACCESS_MODE } from '../../config/types';
|
||||
|
||||
const VOLUME_MODE_OPTIONS = [VOLUME_MODE.FILE_SYSTEM, VOLUME_MODE.BLOCK];
|
||||
const ACCESS_MODE_OPTIONS = [ACCESS_MODE.READ_WRITE_ONCE, ACCESS_MODE.READ_WRITE_MANY, ACCESS_MODE.READ_ONLY_MANY];
|
||||
const {
|
||||
READ_WRITE_ONCE = 'ReadWriteOnce',
|
||||
READ_ONLY_MANY = 'ReadOnlyMany',
|
||||
READ_WRITE_MANY = 'ReadWriteMany',
|
||||
READ_WRITE_ONCE_POD = 'ReadWriteOncePod'
|
||||
} = ACCESS_MODE || {};
|
||||
const ACCESS_MODE_OPTIONS = [READ_WRITE_ONCE, READ_WRITE_MANY, READ_ONLY_MANY, READ_WRITE_ONCE_POD];
|
||||
|
||||
const props = defineProps({
|
||||
storageClassName: { type: String, default: '' },
|
||||
providerName: { type: String, default: '' },
|
||||
showInherited: { type: Boolean, default: false },
|
||||
volumeMode: { type: String, default: VOLUME_MODE.FILE_SYSTEM },
|
||||
accessModes: { type: Array, default: () => [ACCESS_MODE.READ_WRITE_MANY] },
|
||||
accessModes: { type: Array, default: () => ['ReadWriteMany'] },
|
||||
inheritedVolumeMode: { type: String, default: VOLUME_MODE.FILE_SYSTEM },
|
||||
inheritedAccessModes: { type: Array, default: () => [ACCESS_MODE.READ_WRITE_MANY] },
|
||||
inheritedAccessModes: { type: Array, default: () => ['ReadWriteMany'] },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['apply', 'close']);
|
||||
@@ -26,7 +32,7 @@ const store = useStore();
|
||||
const { t } = useI18n(store);
|
||||
|
||||
const localVolumeMode = ref(props.volumeMode || VOLUME_MODE.FILE_SYSTEM);
|
||||
const localAccessMode = ref(props.accessModes?.[0] || ACCESS_MODE.READ_WRITE_MANY);
|
||||
const localAccessMode = ref(props.accessModes?.[0] || READ_WRITE_MANY);
|
||||
|
||||
const volumeModeOptions = VOLUME_MODE_OPTIONS.map((value) => ({ label: value, value }));
|
||||
const accessModeOptions = ACCESS_MODE_OPTIONS.map((value) => ({ label: value, value }));
|
||||
@@ -42,7 +48,7 @@ const apply = () => {
|
||||
// Repopulate the dropdowns with the provider default values; the user then applies.
|
||||
const reset = () => {
|
||||
localVolumeMode.value = props.inheritedVolumeMode || VOLUME_MODE.FILE_SYSTEM;
|
||||
localAccessMode.value = props.inheritedAccessModes?.[0] || ACCESS_MODE.READ_WRITE_MANY;
|
||||
localAccessMode.value = props.inheritedAccessModes?.[0] || READ_WRITE_MANY;
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
} from './table-headers';
|
||||
import { ADD_ONS } from './harvester-map';
|
||||
import { registerAddonSideNav } from '../utils/dynamic-nav';
|
||||
import { setVendor } from '@shell/config/private-label';
|
||||
|
||||
const TEMPLATE = HCI.VM_VERSION;
|
||||
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
|
||||
@@ -109,15 +110,31 @@ export function init($plugin, store) {
|
||||
}
|
||||
};
|
||||
|
||||
store.dispatch('setIsSingleProduct', {
|
||||
productName: PRODUCT_NAME,
|
||||
logo: require(`@shell/assets/images/providers/harvester.svg`),
|
||||
productNameKey: 'harvester.productLabel',
|
||||
getVersionInfo: (store) => store.getters[`${ PRODUCT_NAME }/byId`]?.(HCI.SETTING, 'server-version')?.value || 'unknown',
|
||||
afterLoginRoute: home,
|
||||
logoRoute: home,
|
||||
supportCustomLogo: true
|
||||
});
|
||||
store.watch(
|
||||
(_state, getters) => ({
|
||||
isHarvesterPrime: !!getters['harvester-common/isHarvesterPrime'],
|
||||
isDark: getters['prefs/theme'] === 'dark',
|
||||
privateLabel: getters['harvester-common/privateLabel']
|
||||
}),
|
||||
({ isHarvesterPrime, isDark, privateLabel }) => {
|
||||
const primeLogo = isDark ? require('../assets/images/dark/suse-virtualization.svg') : require('../assets/images/suse-virtualization.svg');
|
||||
|
||||
setVendor(privateLabel || '');
|
||||
|
||||
const primeLabelKey = privateLabel ? 'harvester.branding.primeLabel' : 'harvester.branding.primeEmptyLabel';
|
||||
|
||||
store.dispatch('setIsSingleProduct', {
|
||||
productName: PRODUCT_NAME,
|
||||
logo: isHarvesterPrime ? primeLogo : require(`@shell/assets/images/providers/harvester.svg`),
|
||||
productNameKey: isHarvesterPrime ? primeLabelKey : 'harvester.productLabel',
|
||||
getVersionInfo: (store) => store.getters[`${ PRODUCT_NAME }/byId`]?.(HCI.SETTING, 'server-version')?.value || 'unknown',
|
||||
afterLoginRoute: home,
|
||||
logoRoute: home,
|
||||
supportCustomLogo: !isHarvesterPrime
|
||||
});
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
}
|
||||
|
||||
product({
|
||||
|
||||
@@ -111,15 +111,17 @@ export const HCI_ALLOWED_SETTINGS = {
|
||||
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { kind: 'number', featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
|
||||
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: {
|
||||
kind: 'boolean',
|
||||
experimental: true,
|
||||
preview: true,
|
||||
featureFlag: 'longhornV2LVMSupport'
|
||||
},
|
||||
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_HUGEPAGE_ENABLED]: {
|
||||
kind: 'boolean',
|
||||
preview: true,
|
||||
featureFlag: 'longhornV2HugepageSettings'
|
||||
},
|
||||
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_MEMORY_SIZE]: {
|
||||
kind: 'number',
|
||||
preview: true,
|
||||
featureFlag: 'longhornV2HugepageSettings'
|
||||
},
|
||||
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
||||
|
||||
@@ -390,14 +390,17 @@ export default {
|
||||
:vmi-resource="vmi"
|
||||
:vmim-resource="vmim"
|
||||
/>
|
||||
<DashboardMetrics
|
||||
<div
|
||||
v-if="showVmMetrics && liveMigrationProgressEnabled"
|
||||
:detail-url="VM_MIGRATION_DETAIL_URL"
|
||||
graph-height="640px"
|
||||
:has-summary-and-detail="false"
|
||||
:vars="graphVars"
|
||||
class="mb-30"
|
||||
/>
|
||||
class="migration-metrics-scroll mb-30"
|
||||
>
|
||||
<DashboardMetrics
|
||||
:detail-url="VM_MIGRATION_DETAIL_URL"
|
||||
graph-height="960px"
|
||||
:has-summary-and-detail="false"
|
||||
:vars="graphVars"
|
||||
/>
|
||||
</div>
|
||||
<Events
|
||||
v-if="liveMigrationProgressEnabled"
|
||||
:resource="vmi"
|
||||
@@ -436,3 +439,10 @@ export default {
|
||||
</Tabbed>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.migration-metrics-scroll {
|
||||
max-height: 640px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
<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 = {
|
||||
rancher: {
|
||||
hostname: '',
|
||||
version: '',
|
||||
bootstrapPassword: '',
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEnableK3k',
|
||||
|
||||
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 = merge({}, DEFAULT_VALUE);
|
||||
}
|
||||
|
||||
return { valuesContentJson };
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
buttonDisabled() {
|
||||
const { rancher } = this.valuesContentJson;
|
||||
|
||||
return !(rancher?.hostname || '').trim() || !(rancher?.version || '').trim() || !(rancher?.bootstrapPassword || '').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.rancherK3k.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.rancher.hostname"
|
||||
:required="true"
|
||||
:label="t('harvester.addons.rancherK3k.hostname')"
|
||||
placeholder="rancher.$vip.nip.io"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="valuesContentJson.rancher.version"
|
||||
:required="true"
|
||||
:label="t('harvester.addons.rancherK3k.version')"
|
||||
placeholder="v1.14.0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-15">
|
||||
<div class="col span-12">
|
||||
<LabeledInput
|
||||
v-model:value="valuesContentJson.rancher.bootstrapPassword"
|
||||
:required="true"
|
||||
type="password"
|
||||
:label="t('harvester.addons.rancherK3k.password')"
|
||||
:tooltip="t('harvester.addons.rancherK3k.passwordTooltip')"
|
||||
/>
|
||||
</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>
|
||||
@@ -0,0 +1,167 @@
|
||||
<script>
|
||||
import merge from 'lodash/merge';
|
||||
import jsyaml from 'js-yaml';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
|
||||
const DEFAULT_VALUE = {
|
||||
certManagerVersion: 'v1.20.2',
|
||||
rancher: {
|
||||
hostname: '',
|
||||
version: '',
|
||||
bootstrapPassword: '',
|
||||
},
|
||||
k3kCluster: {
|
||||
servers: 1,
|
||||
version: 'v1.35.4-k3s1',
|
||||
storageClassName: 'harvester-longhorn',
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'EditAddonRancherK3k',
|
||||
components: { LabeledInput, RadioGroup },
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
mode: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
registerBeforeHook: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
let valuesContentJson = {};
|
||||
|
||||
try {
|
||||
valuesContentJson = merge({}, DEFAULT_VALUE, jsyaml.load(this.value.spec.valuesContent));
|
||||
} catch (err) {
|
||||
valuesContentJson = merge({}, DEFAULT_VALUE);
|
||||
|
||||
this.$store.dispatch('growl/fromError', {
|
||||
title: this.$store.getters['i18n/t']('generic.notification.title.error'),
|
||||
err: err.data || err,
|
||||
}, { root: true });
|
||||
}
|
||||
|
||||
return { valuesContentJson };
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.registerBeforeHook) {
|
||||
this.registerBeforeHook(this.willSave, 'willSave');
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
willSave() {
|
||||
const errors = [];
|
||||
|
||||
if (!this.value.spec.enabled) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (!this.valuesContentJson.rancher.hostname) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.addons.rancherK3k.hostname') }, true));
|
||||
}
|
||||
|
||||
if (!this.valuesContentJson.rancher.version) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.addons.rancherK3k.version') }, true));
|
||||
}
|
||||
|
||||
if (!this.valuesContentJson.rancher.bootstrapPassword) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.addons.rancherK3k.password') }, true));
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Promise.reject(errors);
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
valuesContentJson: {
|
||||
handler(neu) {
|
||||
this.value.spec['valuesContent'] = jsyaml.dump(neu);
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col span-12">
|
||||
<RadioGroup
|
||||
v-model:value="value.spec.enabled"
|
||||
class="mb-20"
|
||||
name="model"
|
||||
:mode="mode"
|
||||
:options="[true,false]"
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="value.spec.enabled">
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="valuesContentJson.rancher.hostname"
|
||||
label-key="harvester.addons.rancherK3k.hostname"
|
||||
:required="true"
|
||||
:mode="mode"
|
||||
placeholder="rancher.$vip.nip.io"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="valuesContentJson.rancher.version"
|
||||
label-key="harvester.addons.rancherK3k.version"
|
||||
:required="true"
|
||||
:mode="mode"
|
||||
placeholder="v1.14.0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="valuesContentJson.rancher.bootstrapPassword"
|
||||
label-key="harvester.addons.rancherK3k.password"
|
||||
:mode="mode"
|
||||
:required="true"
|
||||
type="password"
|
||||
:tooltip="t('harvester.addons.rancherK3k.passwordTooltip')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep() .radio-group {
|
||||
display: flex;
|
||||
.radio-container {
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -246,4 +246,27 @@ export default {
|
||||
.group-actions {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
// Match the enable/disable passthrough bulk-action buttons height to the .btn (40px) on the right.
|
||||
:deep(.bulk .rc-button.btn-medium.bulk-action:not(.btn-sm)) {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
// Make the collapsed "Actions" dropdown button 40px too.
|
||||
:deep(.bulk .rc-button.btn-medium.bulk-actions-dropdown:not(.btn-sm)) {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
// Lay the bulk row out with flex so the "N selected" label can wrap below the buttons.
|
||||
:deep(.bulk) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.bulk .action-availability) {
|
||||
flex-basis: 100%;
|
||||
margin-left: 0;
|
||||
margin-top: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,6 +5,7 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import Banner from '@components/Banner/Banner.vue';
|
||||
import remove from 'lodash/remove';
|
||||
import { set } from '@shell/utils/object';
|
||||
import { uniq } from '@shell/utils/array';
|
||||
import { HCI } from '../../../types';
|
||||
import DeviceList from './DeviceList';
|
||||
import CompatibilityMatrix from '../CompatibilityMatrix';
|
||||
@@ -60,7 +61,15 @@ export default {
|
||||
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
|
||||
});
|
||||
|
||||
vmDevices.forEach(({ name }) => {
|
||||
// When off, read from spec; otherwise the spec name is a placeholder ('provisioned'),
|
||||
// so read the real allocated device names from the deviceAllocationDetails annotation.
|
||||
const hostDeviceNames = this.vm.isOff ? [
|
||||
...vmDevices.map(({ name }) => name),
|
||||
] : [
|
||||
...Object.values(this.vm?.provisionedHostDevices || {}).reduce((acc, devices) => [...acc, ...devices], []),
|
||||
];
|
||||
|
||||
uniq(hostDeviceNames).forEach((name) => {
|
||||
if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
|
||||
selectedDevices.push(name);
|
||||
}
|
||||
|
||||
@@ -84,6 +84,8 @@ dialog:
|
||||
|
||||
harvester:
|
||||
branding:
|
||||
primeLabel: '{vendor}'
|
||||
primeEmptyLabel: ''
|
||||
logos:
|
||||
tip: 'Upload a logo to replace the Harvester logo in the top-level navigation header. Image height should be 21 pixels with a max width of 200 pixels. Max file size is 20KB. Accepted formats: JPEG, PNG, SVG.'
|
||||
favicon:
|
||||
@@ -1832,6 +1834,8 @@ harvester:
|
||||
'rancher-logging': rancher-logging is an add-on to collect versatile logs, events, and audits from the Harvester cluster and route them to many kinds of servers based on flows.
|
||||
'rancher-monitoring': rancher-monitoring is an add-on to collect Harvester cluster and virtual machine metrics, view them on the embedded dashboard, and send alert(s) to remote servers.
|
||||
'rancher-vcluster': rancher-vcluster deploys a virtual cluster (vcluster) with Rancher installed.
|
||||
'k3k': k3k is an add-on to deploy the k3k controller.
|
||||
'rancher-k3k': rancher-k3k deploys a k3k virtual cluster with Rancher installed.
|
||||
'harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
|
||||
'harvester-system/harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
|
||||
'harvester-csi-driver-lvm': harvester-csi-driver-lvm is an add-on allowing users to create PVC through the LVM with local devices.
|
||||
@@ -2136,6 +2140,14 @@ harvester:
|
||||
hostname: Hostname
|
||||
rancherVersion: Rancher Version
|
||||
password: Bootstrap Password
|
||||
rancherK3k:
|
||||
accessRancher: Access the Rancher Dashboard
|
||||
hostname: Hostname
|
||||
version: Rancher Version
|
||||
password: Bootstrap Password
|
||||
passwordTooltip: The initial password in Rancher for the first time login.
|
||||
enable:
|
||||
title: Enable K3k
|
||||
switchFailed: "{action} {name} failed!"
|
||||
nvidiaDriverToolkit:
|
||||
titles:
|
||||
@@ -2417,7 +2429,15 @@ typeDescription:
|
||||
harvester: "Branding allows administrators to globally re-brand the UI by customizing the Harvester product name, logos and color scheme."
|
||||
|
||||
advancedSettings:
|
||||
experimental: 'Experimental features allow users to test and evaluate early-access functionality prior to official supported releases'
|
||||
tags:
|
||||
modified:
|
||||
label: Modified
|
||||
experimental:
|
||||
label: Experimental
|
||||
description: 'Experimental: The feature is incomplete but its essential functionality is operational. The feature is not recommended for use in production environments.'
|
||||
preview:
|
||||
label: Technical Preview
|
||||
description: 'Technical Preview: The feature is almost complete and its functionality is not expected to change significantly. Explore the feature extensively before using in production environments.'
|
||||
descriptions:
|
||||
'harv-vlan': Default Network Interface name of the VLAN network.
|
||||
'harv-backup-target': Custom backup target to store virtual machine backups.
|
||||
|
||||
@@ -72,7 +72,7 @@ export default {
|
||||
/>
|
||||
|
||||
<a
|
||||
v-if="scope.row.metadata.name === 'rancher-vcluster' && scope.row.spec.enabled"
|
||||
v-if="['rancher-vcluster', 'rancher-k3k'].includes(scope.row.metadata.name) && scope.row.spec.enabled"
|
||||
v-tooltip="t('harvester.addons.rancherVcluster.accessRancher')"
|
||||
class="ml-5"
|
||||
rel="nofollow noopener noreferrer"
|
||||
|
||||
@@ -38,7 +38,7 @@ export const SSH_EXISTING_TYPE = {
|
||||
export default {
|
||||
methods: {
|
||||
hasCloudConfigComment(userScript) {
|
||||
if (typeof userScript === 'string' && /(^|\n)\s*#cloud-config(\s|$)/.test(userScript)) {
|
||||
if (typeof userScript === 'string' && /^\s*#cloud-config(\s|$)/.test(userScript)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1317,8 +1317,9 @@ export default {
|
||||
case SOURCE_TYPE.IMAGE: {
|
||||
const image = this.images.find( (I) => R.image === I.id);
|
||||
|
||||
out.spec.storageClassName = R.realName ? R.storageClassName : (R.storageClassName || image?.storageClassName);
|
||||
|
||||
if (image) {
|
||||
out.spec.storageClassName = image.storageClassName;
|
||||
out.metadata.annotations = { [HCI_ANNOTATIONS.IMAGE_ID]: image.id };
|
||||
} else {
|
||||
out.metadata.annotations = { [HCI_ANNOTATIONS.IMAGE_ID]: '' };
|
||||
|
||||
@@ -5,6 +5,7 @@ import SYSTEM_NAMESPACES from '@shell/config/system-namespaces';
|
||||
import { get } from '@shell/utils/object';
|
||||
import { NAMESPACE } from '@shell/config/types';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
|
||||
import { FORKLIFT_NAMESPACE } from '@pkg/harvester/config/harvester-map';
|
||||
import { HCI } from '../../types';
|
||||
|
||||
const OBSCURE_NAMESPACE_PREFIX = [
|
||||
@@ -93,6 +94,10 @@ export default class HciNamespace extends namespace {
|
||||
}
|
||||
|
||||
get isSystem() {
|
||||
if (this.metadata?.name === FORKLIFT_NAMESPACE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const systemNamespaces = ['fleet-default'];
|
||||
|
||||
if (systemNamespaces.includes(this.metadata.name)) {
|
||||
|
||||
@@ -5,12 +5,14 @@ import HarvesterResource from './harvester';
|
||||
import { HCI } from '../types';
|
||||
|
||||
const HARVESTER_NVIDIA_DRIVER_TOOLKIT = 'harvester-system/nvidia-driver-toolkit';
|
||||
const RANCHER_VCLUSTER = 'harvester-system/rancher-vcluster';
|
||||
const RANCHER_K3K_ADDON = 'rancher-k3k/rancher-k3k';
|
||||
|
||||
export default class HciAddonConfig extends HarvesterResource {
|
||||
get availableActions() {
|
||||
const out = super._availableActions;
|
||||
|
||||
if (this.id === 'harvester-system/rancher-vcluster') {
|
||||
if (this.id === RANCHER_VCLUSTER || this.id === RANCHER_K3K_ADDON) {
|
||||
const rancherDashboard = {
|
||||
action: 'goToRancher',
|
||||
enabled: this.spec.enabled,
|
||||
@@ -57,6 +59,15 @@ export default class HciAddonConfig extends HarvesterResource {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.spec.enabled && this.id === RANCHER_K3K_ADDON) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources: [this],
|
||||
component: 'HarvesterEnableK3k',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.spec.enabled = !this.spec.enabled;
|
||||
await this.save();
|
||||
} catch (err) {
|
||||
@@ -69,18 +80,19 @@ export default class HciAddonConfig extends HarvesterResource {
|
||||
}
|
||||
|
||||
goToRancher() {
|
||||
const valuesContent = jsyaml.load(this.spec.valuesContent);
|
||||
|
||||
window.open(
|
||||
`https://${ valuesContent.hostname }`,
|
||||
this.rancherHostname,
|
||||
'_blank',
|
||||
);
|
||||
}
|
||||
|
||||
get rancherHostname() {
|
||||
const valuesContent = jsyaml.load(this.spec.valuesContent);
|
||||
const valuesContent = jsyaml.load(this.spec.valuesContent) || {};
|
||||
|
||||
return `https://${ valuesContent.hostname }`;
|
||||
// rancher-k3k nests the hostname under `rancher`, rancher-vcluster keeps it at the top level
|
||||
const hostname = this.id === RANCHER_K3K_ADDON ? valuesContent.rancher?.hostname : valuesContent.hostname;
|
||||
|
||||
return `https://${ hostname }`;
|
||||
}
|
||||
|
||||
get stateColor() {
|
||||
|
||||
@@ -1315,6 +1315,18 @@ export default class VirtVm extends HarvesterResource {
|
||||
}
|
||||
}
|
||||
|
||||
get provisionedHostDevices() {
|
||||
// Rancher-provisioned VMs store hostDevices.name as a placeholder ('provisioned'); the
|
||||
// real allocated PCI device names live in this annotation, keyed by resourceName.
|
||||
try {
|
||||
const deviceAllocationDetails = JSON.parse(this.metadata?.annotations[HCI_ANNOTATIONS.VM_DEVICE_ALLOCATION_DETAILS] || '{}');
|
||||
|
||||
return deviceAllocationDetails?.hostdevices || {};
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
get schedulingVMBackupFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('schedulingVMBackup');
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ export default {
|
||||
return {
|
||||
vendor: getVendor(),
|
||||
uiPLSetting: {},
|
||||
privateLabelDraft: null,
|
||||
uiLogoDarkSetting: {},
|
||||
uiLogoDark: '',
|
||||
uiLogoLightSetting: {},
|
||||
@@ -81,6 +82,17 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isHarvesterPrime() {
|
||||
return !!this.$store.getters['harvester-common/isHarvesterPrime'];
|
||||
},
|
||||
privateLabel: {
|
||||
get() {
|
||||
return this.privateLabelDraft ?? (this.isHarvesterPrime ? this.$store.getters['harvester-common/privateLabel'] : this.uiPLSetting.value);
|
||||
},
|
||||
set(value) {
|
||||
this.privateLabelDraft = value;
|
||||
}
|
||||
},
|
||||
mode() {
|
||||
const schema = this.$store.getters[`management/schemaFor`](MANAGEMENT.SETTING);
|
||||
|
||||
@@ -112,7 +124,7 @@ export default {
|
||||
this.errors.push(e);
|
||||
},
|
||||
async save(btnCB) {
|
||||
this.uiPLSetting.value = this.uiPLSetting.value.replaceAll(/[\<>&=#()"]/gm, '');
|
||||
this.uiPLSetting.value = (this.privateLabel || '').replaceAll(/[\<>&=#()"]/gm, '');
|
||||
if (this.customizeLogo) {
|
||||
this.uiLogoLightSetting.value = this.uiLogoLight;
|
||||
this.uiLogoDarkSetting.value = this.uiLogoDark;
|
||||
@@ -169,7 +181,7 @@ export default {
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="uiPLSetting.value"
|
||||
v-model:value="privateLabel"
|
||||
:label="t('branding.uiPL.label')"
|
||||
:mode="mode"
|
||||
:maxlength="100"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import Parse from 'url-parse';
|
||||
import { MANAGEMENT } from '@shell/config/types';
|
||||
import { SETTING } from '@shell/config/settings';
|
||||
import { HCI } from '../types';
|
||||
import { PRODUCT_NAME } from '../config/harvester';
|
||||
import { featureEnabled, getVersion } from '../utils/feature-flags';
|
||||
@@ -64,6 +66,23 @@ const mutations = {
|
||||
};
|
||||
|
||||
const getters = {
|
||||
isHarvesterPrime: (_state, _getters, _rootState, rootGetters) => {
|
||||
const setting = rootGetters['harvester/byId']?.(HCI.SETTING, 'server-flavor');
|
||||
|
||||
return (setting?.value || setting?.default) === 'prime';
|
||||
},
|
||||
|
||||
privateLabel: (_state, getters, _rootState, rootGetters) => {
|
||||
const setting = rootGetters['management/byId']?.(MANAGEMENT.SETTING, SETTING.PL);
|
||||
const value = setting?.value || '';
|
||||
|
||||
if (getters.isHarvesterPrime && value === (setting?.default || 'Harvester')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
getBundleId(state) {
|
||||
return state.latestBundleId;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user