Compare commits

..
Author SHA1 Message Date
renovate[bot] 9233fb26f3 deps: update dependency @types/node to v25.9.8 2026-09-27 06:02:09 +00:00
freeze 3c92a06268 feat: drop striped LVM volume group type from v1.9.1 (#1167)
The LVM add-on reaches GA in v1.9.1 and only supports dm-thin.
    Gate the striped option behind a v1.9.1 feature flag, default new
    LVM storage classes to dm-thin, and reset a cloned striped type so
    older clusters keep the current behaviour.

    Related issue: harvester/harvester#11731

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2026-09-23 12:14:48 +08:00
freeze 6256cdc97a ci: pin package version to the git tag for tag builds (#1165)
Sprint tags (vX.Y.Z-dev-YYYYMMDD) only share a prefix with the
    x.y.z-dev version in package.json, so every sprint plugin reported
    the same version and the catalog image push hit "Cannot overwrite
    production image". Bump the tree to the tag before building.

    Related issue: harvester/harvester#11047

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2026-09-22 19:33:24 +08:00
renovate[bot] 4323132565 deps: update patch dependencies (#1159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-21 11:00:14 +08:00
Andy Lee d05ec68e16 chore: add v1.10.0 and v1.9.1 feature flags (#1156)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-09-18 16:34:06 +08:00
Andy Lee cb2cef5e73 fix: avoid userpreferences PUT flood from SideNav refresh trigger (#1119)
kickSideNav toggled a dummy favorite via addFavorite/removeFavorite, which persist through prefs/set and issue a PUT /v1/userpreferences on every toggle, flooding the backend and surfacing 500 errors. Switch to the in-memory prefs/load mutation so the SideNav still re-renders without any network request.

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-09-16 09:31:28 +08:00
Andy Lee 45116b568a feat: change logo based on server-flavor setting (#1151)
* feat: use prime logo when server-flavor is prime

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: expose server-flavor setting

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: copilot review

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-09-16 09:30:37 +08:00
Victor Chang f42d798d10 fix: require names for cloud configuration templates (#1138)
Signed-off-by: Victor Chang <t7902195204@gmail.com>
2026-09-16 09:17:29 +08:00
Andy Lee 50b4cf5248 feat: add rancher k3k addon UI page (#1137)
* feat: add k3k addon UI

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: add rancher-k3k addon UI

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: copilot review

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: copilot review

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: remove k3k edit page

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: k3k addon description

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-09-14 15:46:14 +08:00
Siva Kanakala cba37194b6 fix: preserve per-volume storage class when editing image-sourced volumes and fallback to the image StorageClass for new volumes (#1144)
Signed-off-by: Siva Kanakala <siva.kanakala@suse.com>
2026-09-10 13:08:53 +08:00
29 changed files with 1044 additions and 870 deletions
@@ -125,6 +125,13 @@ jobs:
node-version-file: '.nvmrc'
cache: 'yarn'
# Tag builds (RC, formal and sprint releases) ship the tag as the package
# version. RC/formal tags already match package.json; sprint tags
# (vX.Y.Z-dev-YYYYMMDD) only share its prefix, so pin it here for the build.
- name: Pin package version to tag
if: ${{ inputs.CI_BUILD_TAG != '' }}
run: ./scripts/bump_version.sh "${{ inputs.CI_BUILD_TAG }}"
- id: ci-build-pkg
name: Build pkg
run: ./scripts/ci-build-pkg.sh harvester
@@ -58,6 +58,12 @@ jobs:
- name: Setup Nodejs with yarn install
uses: ./.github/actions/setup
# The catalog image is tagged with the package.json version. Sprint tags
# (vX.Y.Z-dev-YYYYMMDD) only share its prefix, so pin it to the tag here.
- name: Pin package version to tag
if: ${{ github.ref_type == 'tag' }}
run: ./scripts/bump_version.sh "${{ github.ref_name }}"
- name: Build and push UI image
run: |
publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
+1 -3
View File
@@ -7,7 +7,6 @@
},
"dependencies": {
"@babel/plugin-transform-class-static-block": "7.29.7",
"@novnc/novnc": "1.7.0",
"@rancher/shell": "3.0.13",
"@vue-flow/background": "^1.3.0",
"@vue-flow/controls": "^1.1.1",
@@ -28,8 +27,7 @@
"yaml": "^2.5.1"
},
"resolutions": {
"@novnc/novnc": "1.7.0",
"@types/node": "25.9.5",
"@types/node": "25.9.8",
"cronstrue": "2.59.0",
"d3-color": "3.1.0",
"ejs": "3.1.10",
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

+4 -2
View File
@@ -10,6 +10,7 @@ import { docLink } from '../utils/feature-flags';
const CATEGORY = {
ui: [
'branding',
HCI_SETTING.SERVER_FLAVOR,
'ui-source',
'ui-index',
]
@@ -53,7 +54,7 @@ export default {
};
},
computed: { ...mapGetters({ t: 'i18n/t' }) },
computed: { ...mapGetters({ t: 'i18n/t', isHarvesterPrime: 'harvester-common/isHarvesterPrime' }) },
watch: {
settings: {
@@ -269,7 +270,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>
+82 -336
View File
@@ -1,99 +1,64 @@
<template>
<div>
<div v-if="connected && disconnected">
<main class="main-layout error">
<div class="text-center">
<BrandImage
file-name="error-desert-landscape.svg"
width="900"
height="300"
/>
<h1>
{{ t('generic.notification.title.warning') }}
</h1>
<h2 class="text-secondary mt-20">
{{ t('vncConsole.error.message') }}
</h2>
</div>
</main>
</div>
<div v-if="reconnecting">
<main class="main-layout">
<div class="text-center">
<h2 class="text-secondary mt-20">
{{ t('vncConsole.reconnecting.message') }}:{{ retryTimes }} of {{ maximumRetryTimes }}
</h2>
</div>
</main>
</div>
<div
ref="view"
/>
</div>
</template>
<script>
import RFB from '@novnc/novnc/core/rfb';
import BrandImage from '@shell/components/BrandImage';
import ZoomableRFB from './ZoomableRFB';
export const CONSOLE_STATUS = {
CONNECTING: 'connecting',
CONNECTED: 'connected',
RECONNECTING: 'reconnecting',
DISCONNECTED: 'disconnected',
FAILED: 'failed',
};
const MAXIMUM_RETRY_TIMES = 10;
const BASE_RECONNECT_DELAY = 1000;
const MAXIMUM_RECONNECT_DELAY = 30000;
const clampLevel = (value, fallback) => {
const level = Number.parseInt(value, 10);
if (Number.isNaN(level)) {
return fallback;
}
return Math.min(Math.max(level, 0), 9);
};
export default {
name: 'NovncConsole',
components: { BrandImage },
props: {
url: {
type: String,
default: ''
},
settings: {
type: Object,
default: () => ({})
},
}
},
emits: ['status-changed', 'capabilities'],
components: { BrandImage },
data() {
return {
rfb: null,
status: CONSOLE_STATUS.CONNECTING,
statusDetail: '',
desktopName: '',
connected: false,
disconnected: false,
reconnectDelay: 3000,
reconnecting: false,
maximumRetryTimes: 10,
retryTimes: 0,
maximumRetryTimes: MAXIMUM_RETRY_TIMES,
reconnectTimer: null,
isClosed: false,
setTimeout: null,
};
},
computed: {
statusInfo() {
return {
status: this.status,
detail: this.statusDetail,
desktopName: this.desktopName,
retryTimes: this.retryTimes,
maximumRetryTimes: this.maximumRetryTimes,
};
},
showError() {
return this.status === CONSOLE_STATUS.FAILED;
},
showReconnecting() {
return this.status === CONSOLE_STATUS.RECONNECTING;
},
},
watch: {
url() {
this.reconnect();
},
settings: {
deep: true,
handler: 'applySettings'
},
statusInfo: {
immediate: true,
handler(neu) {
this.$emit('status-changed', neu);
}
},
},
mounted() {
this.$nextTick(() => {
this.connect();
@@ -101,307 +66,88 @@ export default {
},
beforeUnmount() {
this.isClosed = true;
this.clearReconnectTimer();
this.teardown();
this.clearTimeout();
},
methods: {
connect(takeOver = false) {
if (!this.url || this.isClosed) {
return;
}
connect() {
const rfb = new RFB(this.$refs.view, this.url);
this.teardown();
rfb.addEventListener('connect', () => {
this.clearTimeout();
this.status = this.retryTimes > 0 ? CONSOLE_STATUS.RECONNECTING : CONSOLE_STATUS.CONNECTING;
this.connected = true;
this.retryTimes = 0;
this.reconnecting = false;
});
let rfb;
rfb.addEventListener('disconnect', (e) => {
this.clearTimeout();
try {
const url = new URL(this.url);
url.searchParams.set('preserveSession', takeOver === true ? 'false' : 'true');
rfb = new ZoomableRFB(this.$refs.view, url.href, { shared: true });
} catch (err) {
this.statusDetail = err?.message || `${ err }`;
this.status = CONSOLE_STATUS.FAILED;
return;
}
rfb.addEventListener('connect', this.onConnect);
rfb.addEventListener('disconnect', this.onDisconnect);
rfb.addEventListener('securityfailure', this.onSecurityFailure);
rfb.addEventListener('credentialsrequired', this.onCredentialsRequired);
rfb.addEventListener('desktopname', this.onDesktopName);
rfb.addEventListener('capabilities', this.onCapabilities);
this.disconnected = true;
this.rfb = null;
this.reconnect();
});
this.rfb = rfb;
this.applySettings();
},
reconnect() {
this.clearReconnectTimer();
this.isClosed = false;
this.retryTimes = 0;
this.statusDetail = '';
this.connect();
},
takeOver() {
if (!window.confirm(this.t('harvester.virtualMachine.detail.console.takeOver.confirm'))) {
return;
}
this.clearReconnectTimer();
this.isClosed = false;
this.retryTimes = 0;
this.statusDetail = '';
this.connect(true);
},
disconnect() {
this.isClosed = true;
this.clearReconnectTimer();
this.teardown();
this.status = CONSOLE_STATUS.DISCONNECTED;
},
onConnect() {
this.clearReconnectTimer();
this.retryTimes = 0;
this.statusDetail = '';
this.status = CONSOLE_STATUS.CONNECTED;
this.applySettings();
},
onDisconnect() {
const wasConnected = this.status === CONSOLE_STATUS.CONNECTED;
this.teardown(false);
if (this.isClosed) {
if (this.status !== CONSOLE_STATUS.FAILED) {
this.status = CONSOLE_STATUS.DISCONNECTED;
}
return;
}
if (wasConnected) {
this.scheduleReconnect();
} else {
this.clearReconnectTimer();
this.statusDetail = this.t('harvester.virtualMachine.detail.console.status.unavailable');
this.status = CONSOLE_STATUS.FAILED;
}
},
// Authentication problems will not fix themselves, so stop retrying.
onSecurityFailure(e) {
this.statusDetail = e?.detail?.reason || '';
this.isClosed = true;
this.clearReconnectTimer();
this.status = CONSOLE_STATUS.FAILED;
},
onCredentialsRequired() {
this.statusDetail = this.t('harvester.virtualMachine.detail.console.status.credentialsRequired');
this.isClosed = true;
this.clearReconnectTimer();
this.status = CONSOLE_STATUS.FAILED;
},
onDesktopName(e) {
this.desktopName = e?.detail?.name || '';
},
onCapabilities(e) {
this.$emit('capabilities', e?.detail?.capabilities || {});
},
scheduleReconnect() {
this.clearReconnectTimer();
if (this.isClosed) {
return;
}
if (this.retryTimes >= this.maximumRetryTimes) {
this.status = CONSOLE_STATUS.FAILED;
this.reconnecting = false;
this.connected = true;
this.disconnected = true;
return;
}
this.retryTimes += 1;
this.status = CONSOLE_STATUS.RECONNECTING;
this.reconnecting = true;
this.connected = false;
this.disconnected = false;
const delay = Math.min(BASE_RECONNECT_DELAY * (2 ** (this.retryTimes - 1)), MAXIMUM_RECONNECT_DELAY);
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.setTimeout = setTimeout(() => {
this.connect();
}, delay);
}, this.reconnectDelay);
},
clearReconnectTimer() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
clearTimeout() {
if (this.setTimeout) {
clearTimeout(this.setTimeout);
}
},
teardown(disconnect = true) {
const rfb = this.rfb;
if (!rfb) {
return;
}
this.rfb = null;
rfb.removeEventListener('connect', this.onConnect);
rfb.removeEventListener('disconnect', this.onDisconnect);
rfb.removeEventListener('securityfailure', this.onSecurityFailure);
rfb.removeEventListener('credentialsrequired', this.onCredentialsRequired);
rfb.removeEventListener('desktopname', this.onDesktopName);
rfb.removeEventListener('capabilities', this.onCapabilities);
if (!disconnect) {
return;
}
try {
rfb.disconnect();
} catch (err) {
// The socket may already be gone, nothing left to clean up.
}
},
applySettings() {
const rfb = this.rfb;
if (!rfb) {
return;
}
const settings = this.settings || {};
rfb.viewOnly = !!settings.viewOnly;
rfb.scaleViewport = !!settings.scaleViewport;
rfb.resizeSession = !!settings.resizeSession;
rfb.clipViewport = !!settings.clipViewport;
rfb.dragViewport = !!settings.dragViewport;
rfb.zoomPercent = settings.zoomPercent;
rfb.focusOnClick = settings.focusOnClick !== false;
rfb.qualityLevel = clampLevel(settings.qualityLevel, 6);
rfb.compressionLevel = clampLevel(settings.compressionLevel, 2);
disconnect() {
this.rfb.disconnect();
},
ctrlAltDelete() {
this.rfb?.sendCtrlAltDel();
this.rfb.sendCtrlAltDel();
},
sendKey(keysym, code, down) {
this.rfb?.sendKey(keysym, code, down);
},
this.rfb.sendKey(keysym, code, down);
}
}
};
</script>
<template>
<div class="novnc-console">
<div
ref="view"
class="novnc-screen"
/>
<div
v-if="showError || showReconnecting"
class="novnc-overlay"
>
<div
v-if="showError"
class="text-center"
>
<BrandImage
file-name="error-desert-landscape.svg"
width="900"
height="300"
/>
<h1>
{{ t('generic.notification.title.warning') }}
</h1>
<h2 class="text-secondary mt-20">
{{ t('vncConsole.error.message') }}
</h2>
<p
v-if="statusDetail"
class="text-muted mt-10"
>
{{ statusDetail }}
</p>
<button
class="btn role-primary mt-20"
@click="reconnect"
>
{{ t('harvester.action.reconnect') }}
</button>
<button
v-if="!isClosed"
class="btn role-secondary mt-20 ml-10"
@click="takeOver"
>
{{ t('harvester.virtualMachine.detail.console.takeOver.label') }}
</button>
</div>
<h2
v-else
class="text-secondary"
>
{{ t('vncConsole.reconnecting.message') }}:{{ retryTimes }} of {{ maximumRetryTimes }}
</h2>
</div>
</div>
</template>
<style lang="scss" scoped>
.novnc-console {
position: relative;
box-sizing: border-box;
height: 100%;
padding: 8px;
.error {
overflow: hidden;
}
.novnc-screen {
height: 100%;
}
.novnc-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
background: rgba(0, 0, 0, 0.75);
.row {
align-items: center;
}
h1 {
font-size: 5rem;
}
img {
max-width: 100%;
.desert-landscape {
img {
max-width: 100%;
}
}
}
</style>
@@ -0,0 +1,264 @@
<script>
import { STEVE } from '@shell/config/types';
import Banner from '@components/Banner/Banner.vue';
import AsyncButton from '@shell/components/AsyncButton';
import ModalWithCard from '@shell/components/ModalWithCard';
const PREFERED_SHORTCUT_KEYS = 'prefered-shortcut-keys';
export default {
name: 'NovncConsoleCustomKeys',
emits: ['close'],
components: {
ModalWithCard, Banner, AsyncButton
},
data() {
return {
keysRecord: [],
addedShortcutKeys: [],
preferredShortcutKeys: [],
isRecording: false,
};
},
computed: {
savedShortcutKeys() {
const preference = this.$store.getters['management/all'](STEVE.PREFERENCE);
const preferedShortcutKeys = preference?.[0]?.data?.[PREFERED_SHORTCUT_KEYS];
let out = [];
if (!preferedShortcutKeys) {
return out;
}
try {
out = JSON.parse(preferedShortcutKeys);
} catch (err) {
this.$store.dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: this.t('harvester.virtualMachine.detail.console.customShortcutKeys') }),
err,
}, { root: true });
}
return out;
},
displayedKeys() {
const out = this.addedShortcutKeys.concat(this.preferredShortcutKeys).map((item) => {
const out = item.map((K) => ` <code>${ K.key.charAt(0).toUpperCase() + K.key.slice(1) }</code>`);
return out.join(',');
});
return out;
},
recordButton() {
if (this.isRecording) {
return 'harvester.virtualMachine.detail.console.record.stop';
}
return 'harvester.virtualMachine.detail.console.record.start';
},
keysRecordFormat() {
if (!this.isRecording && this.keysRecord.length === 0) {
return this.t('harvester.virtualMachine.detail.console.record.tips');
}
const out = this.keysRecord.map((item) => ` <code>${ item.key.charAt(0).toUpperCase() + item.key.slice(1) }</code>`);
return `Keys: ${ out.join(',') }`;
},
canAdd() {
const hasRecord = this.keysRecord.length > 0;
let validationList = [].concat(this.preferredShortcutKeys, this.addedShortcutKeys);
if (!hasRecord) {
return false;
}
validationList.push(this.keysRecord);
validationList = validationList.map((item) => {
const out = item.map((K) => K.key);
return out.join(',');
});
return validationList.length === new Set(validationList).size;
},
},
watch: {
savedShortcutKeys: {
handler() {
this.preferredShortcutKeys = [].concat(this.savedShortcutKeys) || [];
},
immediate: true
},
},
methods: {
closeRecordingModal() {
window.removeEventListener('keydown', this.handleShortcut);
this.$emit('close');
},
toggleRecording() {
this.isRecording = !this.isRecording;
if (this.isRecording) {
this.keysRecord = [];
window.addEventListener('keydown', this.handleShortcut);
} else {
window.removeEventListener('keydown', this.handleShortcut);
}
},
handleShortcut(event) {
event.preventDefault();
const {
key, keyCode, code, location, charCode
} = event;
this.keysRecord.push({
key, keyCode, code, location, charCode
});
},
addShortcutKey() {
this.addedShortcutKeys.push([].concat(this.keysRecord));
},
removeKey(keys) {
const key = keys.replace(/(\s*)<code>|<\/code>/g, '').replace(/\s*,\s*/g, ',');
this.addedShortcutKeys = this.addedShortcutKeys.filter((item) => {
const formatkey = item.map((K) => K.key.charAt(0).toUpperCase() + K.key.slice(1)).join(',');
return formatkey !== key;
});
this.preferredShortcutKeys = this.preferredShortcutKeys.filter((item) => {
const formatkey = item.map((K) => K.key.charAt(0).toUpperCase() + K.key.slice(1)).join(',');
return formatkey !== key;
});
},
async saveKeys(buttonCb) {
const out = [].concat(this.preferredShortcutKeys, this.addedShortcutKeys);
const preference = this.$store.getters['management/all'](STEVE.PREFERENCE)?.[0];
try {
preference.data[PREFERED_SHORTCUT_KEYS] = JSON.stringify(out);
await preference.save();
this.closeRecordingModal();
buttonCb(true);
} catch (err) {
buttonCb(false);
}
},
}
};
</script>
<template>
<ModalWithCard
name="recordShortcutKeys"
:width="550"
>
<template #title>
<t k="harvester.virtualMachine.detail.console.customShortcutKeys" />
</template>
<template #content>
<div class="row">
<div class="col span-12">
<Banner color="info">
<span v-clean-html="keysRecordFormat"></span>
</Banner>
</div>
</div>
<div class="row">
<div class="col span-12">
<button
class="btn bg-primary"
@click="toggleRecording"
>
<t :k="recordButton" />
<i
class="icon icon-fw"
:class="isRecording ? 'icon-dot-open' : 'icon-dot'"
/>
</button>
<button
:disabled="!canAdd"
class="btn bg-primary"
@click="addShortcutKey"
>
<t k="generic.add" />
</button>
</div>
</div>
<hr>
<div class="displayed-keys mt-20">
<h4
v-clean-html="t('harvester.virtualMachine.detail.console.record.preferredKeys')"
class="text-default-text"
/>
<div class="displayed-banners">
<Banner
v-for="(keys,index) in displayedKeys"
:key="index"
>
<span v-clean-html="keys"></span>
</Banner>
</div>
</div>
</template>
<template #footer>
<div class="actions">
<button
class="btn role-secondary mr-20"
@click.prevent="closeRecordingModal"
>
<t k="generic.close" />
</button>
<AsyncButton
mode="done"
@click="saveKeys"
/>
</div>
</template>
</ModalWithCard>
</template>
<style lang="scss" scoped>
.displayed-keys {
.banner {
margin: 0;
}
}
.displayed-banners {
max-height: 155px;
overflow: auto;
}
.actions {
width: 100%;
display: flex;
justify-content: flex-end;
}
</style>
@@ -1,26 +1,15 @@
<script>
import { STEVE } from '@shell/config/types';
import { escapeHtml } from '@shell/utils/string';
import { allHash } from '@shell/utils/promise';
import KeyTable from '@novnc/novnc/core/input/keysym';
import * as KeyboardUtil from '@novnc/novnc/core/input/util';
import { HCI } from '../../types';
import NovncConsole from './NovncConsole';
import NovncConsoleItem from './NovncConsoleItem';
import { normalizeZoom } from './ZoomableRFB';
import NovncConsoleCustomKeys from './NovncConsoleCustomKeys';
const CONSOLE_SETTINGS_KEY = 'harvester-vnc-console-settings';
const VIEW_MODE = {
SCALE: 'scale',
NONE: 'none',
};
const DEFAULT_SETTINGS = {
viewMode: VIEW_MODE.NONE,
zoomPercent: 100,
viewOnly: false,
qualityLevel: 6,
compressionLevel: 2,
};
const PREFERED_SHORTCUT_KEYS = 'prefered-shortcut-keys';
const SHORT_KEYS = {
ControlLeft: {
@@ -116,7 +105,9 @@ const F_KEYS = {
export default {
name: 'NovncConsoleWrapper',
components: { NovncConsole, NovncConsoleItem },
components: {
NovncConsole, NovncConsoleItem, NovncConsoleCustomKeys
},
async fetch() {
const _hash = { vmResource: this.$store.dispatch('harvester/find', { type: HCI.VM, id: this.value.id }) };
@@ -140,21 +131,39 @@ export default {
return {
keysRecord: [],
vmResource: {},
settings: { ...DEFAULT_SETTINGS },
capabilities: {},
isFullscreen: false,
consoleStatus: {
status: 'connecting',
retryTimes: 0,
maximumRetryTimes: 0,
desktopName: '',
},
renderKeysModal: false,
currentUser: null,
hideCustomKeysBar: false,
};
},
computed: {
canSendKeys() {
return this.consoleStatus.status === 'connected' && !this.settings.viewOnly;
savedShortcutKeys() {
const preference = this.$store.getters['management/all'](STEVE.PREFERENCE);
const preferedShortcutKeys = preference?.[0]?.data?.[PREFERED_SHORTCUT_KEYS];
let out = [];
if (!preference?.[0]?.data) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.hideCustomKeysBar = true;
return out;
}
if (!preferedShortcutKeys) {
return out;
}
try {
out = JSON.parse(preferedShortcutKeys);
} catch (err) {
this.$store.dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(this.value.metadata.name) }),
err,
}, { root: true });
}
return out;
},
isDown() {
@@ -183,16 +192,13 @@ export default {
...F_KEYS,
};
out.AltLeft = { ...SHORT_KEYS.AltLeft, keys: { PrintScreen: FUNCTION_KEYS.PrintScreen, ...F_KEYS } };
out.ControlLeft = {
...SHORT_KEYS.ControlLeft,
keys: {
AltLeft: {
...SHORT_KEYS.AltLeft,
keys: { Delete: FUNCTION_KEYS.Delete }
},
...NORMAL_KEYS,
out.AltLeft.keys = { PrintScreen: FUNCTION_KEYS.PrintScreen, ...F_KEYS };
out.ControlLeft.keys = {
AltLeft: {
...Object.assign(SHORT_KEYS.AltLeft, {}),
keys: { Delete: FUNCTION_KEYS.Delete }
},
...NORMAL_KEYS,
};
return out;
@@ -202,53 +208,14 @@ export default {
return !!this.vmResource?.actions?.softreboot;
},
rfbSettings() {
return {
viewOnly: this.settings.viewOnly,
scaleViewport: this.settings.viewMode === VIEW_MODE.SCALE,
zoomPercent: normalizeZoom(this.settings.zoomPercent),
resizeSession: false,
clipViewport: false,
dragViewport: false,
qualityLevel: this.settings.qualityLevel,
compressionLevel: this.settings.compressionLevel,
};
preferredShortcutKeys() {
return (this.savedShortcutKeys || []).map((item) => {
return {
label: item.map((K) => K.key.charAt(0).toUpperCase() + K.key.slice(1)).join('+'),
value: item
};
});
},
viewModeOptions() {
return Object.values(VIEW_MODE).map((value) => ({
value,
label: this.t(`harvester.virtualMachine.detail.console.settings.viewMode.${ value }`)
}));
},
statusLabel() {
const { status, retryTimes, maximumRetryTimes } = this.consoleStatus;
if (status === 'reconnecting') {
return this.t('harvester.virtualMachine.detail.console.status.reconnecting', { retryTimes, maximumRetryTimes });
}
return this.t(`harvester.virtualMachine.detail.console.status.${ status }`);
},
},
watch: {
settings: {
deep: true,
handler(neu) {
window.localStorage.setItem(CONSOLE_SETTINGS_KEY, JSON.stringify(neu));
}
},
},
mounted() {
this.loadSettings();
document.addEventListener('fullscreenchange', this.onFullscreenChange);
},
beforeUnmount() {
document.removeEventListener('fullscreenchange', this.onFullscreenChange);
},
methods: {
@@ -266,221 +233,137 @@ export default {
// Send function key, e.g. ALT + F
sendKeys() {
if (!this.canSendKeys || !this.$refs.novncConsole) {
return;
}
this.keysRecord.forEach((key) => {
this.$refs.novncConsole.sendKey(this.allKeys[key].value, key, true);
});
[...this.keysRecord].reverse().forEach((key) => {
this.keysRecord.reverse().forEach((key) => {
this.$refs.novncConsole.sendKey(this.allKeys[key].value, key, false);
});
this.$refs.popover.hide();
this.$refs.popover.isOpen = false;
this.keysRecord = [];
},
sendCustomKeys(keys) {
const keyList = [].concat(keys);
keyList.forEach((K) => {
this.$refs.novncConsole.sendKey(KeyboardUtil.getKeysym(K), KeyboardUtil.getKeycode(K), true);
});
keyList.reverse().forEach((K) => {
this.$refs.novncConsole.sendKey(KeyboardUtil.getKeysym(K), KeyboardUtil.getKeycode(K), false);
});
},
reconnect() {
this.$refs.novncConsole.reconnect();
},
loadSettings() {
try {
const saved = JSON.parse(window.localStorage.getItem(CONSOLE_SETTINGS_KEY) || '{}');
this.settings = {
viewMode: Object.values(VIEW_MODE).includes(saved?.viewMode) ? saved.viewMode : DEFAULT_SETTINGS.viewMode,
zoomPercent: normalizeZoom(saved?.zoomPercent ?? DEFAULT_SETTINGS.zoomPercent),
viewOnly: saved?.viewOnly === true,
qualityLevel: saved?.qualityLevel ?? DEFAULT_SETTINGS.qualityLevel,
compressionLevel: saved?.compressionLevel ?? DEFAULT_SETTINGS.compressionLevel,
};
} catch (err) {
this.settings = { ...DEFAULT_SETTINGS };
}
},
onStatusChanged(info) {
this.consoleStatus = info;
},
onCapabilities(capabilities) {
this.capabilities = capabilities;
},
ctrlAltDelete() {
if (this.canSendKeys) {
this.$refs.novncConsole?.ctrlAltDelete();
}
},
async toggleFullscreen() {
try {
if (document.fullscreenElement) {
await document.exitFullscreen();
} else {
await this.$refs.consoleContainer.requestFullscreen();
// Keyboard lock lets shortcuts such as Ctrl+W and F11 reach the guest.
await navigator.keyboard?.lock?.(['KeyW', 'F11']);
}
} catch (err) {
this.$store.dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(this.value.metadata.name) }),
err,
}, { root: true });
}
},
onFullscreenChange() {
this.isFullscreen = !!document.fullscreenElement;
if (!this.isFullscreen) {
navigator.keyboard?.unlock?.();
}
},
softReboot() {
this.vmResource.doSoftReboot();
},
showKeysModal() {
this.renderKeysModal = true;
},
hideKeysModal() {
this.renderKeysModal = false;
},
}
};
</script>
<template>
<div id="app">
<div
ref="consoleContainer"
class="vm-console"
>
<div class="vm-console">
<div class="combination-keys">
<div class="toolbar-group">
<v-dropdown
ref="popover"
placement="top"
trigger="click"
:container="false"
@auto-hide="keysRecord = []"
>
<button
class="btn btn-sm bg-primary"
:disabled="!canSendKeys"
<v-dropdown
ref="popover"
placement="top"
trigger="click"
:container="false"
@auto-hide="keysRecord = []"
>
<button class="btn btn-sm bg-primary">
{{ t("harvester.virtualMachine.detail.console.shortcutKeys") }}
</button>
<template #popper>
<novnc-console-item
:items="keymap"
:path="keysRecord"
:pos="0"
@update="update"
@send-keys="sendKeys"
/>
</template>
</v-dropdown>
<button
v-if="hasSoftRebootAction"
class="btn btn-sm bg-primary"
@click="softReboot"
>
{{ t("harvester.action.softreboot") }}
</button>
<button
class="btn btn-sm bg-primary"
@click="reconnect"
>
{{ t("harvester.action.reconnect") }}
</button>
<v-dropdown
v-if="!hideCustomKeysBar"
ref="customKeyPopover"
placement="top"
trigger="click"
:container="false"
>
<button class="btn btn-sm bg-primary">
{{ t("harvester.virtualMachine.detail.console.customShortcutKeys") }}
</button>
<template #popper>
<div>
<button
class="btn btn-sm bg-primary"
@click="showKeysModal"
>
{{ t("harvester.virtualMachine.detail.console.management") }}
</button>
</div>
<hr>
<div
v-for="(keys, index) in preferredShortcutKeys"
:key="index"
>
{{ t("harvester.virtualMachine.detail.console.shortcutKeys") }}
</button>
<button
class="btn btn-sm bg-primary"
@click="sendCustomKeys(keys.value)"
>
{{ keys.label }}
</button>
</div>
</template>
</v-dropdown>
<template #popper>
<novnc-console-item
:items="keymap"
:path="keysRecord"
:pos="0"
@update="update"
@send-keys="sendKeys"
/>
</template>
</v-dropdown>
<button
class="btn btn-sm bg-primary"
:disabled="!canSendKeys"
@click="ctrlAltDelete"
>
Ctrl+Alt+Del
</button>
<v-dropdown
placement="top"
trigger="click"
:container="false"
>
<button class="btn btn-sm bg-primary">
{{ t("harvester.virtualMachine.detail.console.settings.label") }}
</button>
<template #popper>
<div class="console-panel">
<label>{{ t('harvester.virtualMachine.detail.console.settings.viewMode.label') }}</label>
<div
v-for="option in viewModeOptions"
:key="option.value"
>
<label class="console-panel__option">
<input
v-model="settings.viewMode"
type="radio"
:value="option.value"
>
{{ option.label }}
</label>
</div>
<label
for="vnc-zoom"
class="console-panel__zoom-label"
>
<span>{{ t('harvester.virtualMachine.detail.console.settings.zoom') }}</span>
<span>{{ settings.zoomPercent }}%</span>
</label>
<input
id="vnc-zoom"
v-model.number="settings.zoomPercent"
class="console-panel__zoom"
type="range"
min="50"
max="200"
step="10"
:disabled="settings.viewMode === 'scale'"
:aria-valuetext="`${ settings.zoomPercent }%`"
>
<label class="console-panel__option">
<input
v-model="settings.viewOnly"
type="checkbox"
>
{{ t('harvester.virtualMachine.detail.console.settings.viewOnly') }}
</label>
</div>
</template>
</v-dropdown>
</div>
<div class="toolbar-group">
<span class="console-status">{{ statusLabel }}</span>
<button
v-if="hasSoftRebootAction"
class="btn btn-sm bg-primary"
@click="softReboot"
>
{{ t("harvester.action.softreboot") }}
</button>
<button
class="btn btn-sm bg-primary"
@click="reconnect"
>
{{ t("harvester.action.reconnect") }}
</button>
<button
class="btn btn-sm bg-primary"
@click="toggleFullscreen"
>
{{ isFullscreen ? t('harvester.virtualMachine.detail.console.fullscreen.exit') : t('harvester.virtualMachine.detail.console.fullscreen.enter') }}
</button>
</div>
<NovncConsoleCustomKeys
v-if="renderKeysModal"
:current-user="currentUser"
@close="hideKeysModal"
/>
</div>
<NovncConsole
v-if="url && !isDown"
ref="novncConsole"
:url="url"
:settings="rfbSettings"
@status-changed="onStatusChanged"
@capabilities="onCapabilities"
/>
<p v-if="isDown">
{{ t("harvester.virtualMachine.detail.console.down") }}
@@ -491,56 +374,14 @@ export default {
<style lang="scss" scoped>
.vm-console {
height: 100vh;
height: 100dvh;
height: 100%;
display: grid;
grid-template-rows: 32px minmax(0, 1fr);
grid-template-rows: 30px auto;
}
.combination-keys {
display: flex;
align-items: center;
justify-content: space-between;
background: rgb(40, 40, 40);
}
.toolbar-group {
display: flex;
align-items: center;
gap: 4px;
}
.console-status {
color: #fff;
font-size: 12px;
margin-right: 8px;
}
.console-panel {
display: flex;
flex-direction: column;
width: 260px;
padding: 4px;
&__zoom-label {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-variant-numeric: tabular-nums;
}
&__zoom {
width: 100%;
margin: 4px 0 8px;
}
&__option {
display: flex;
align-items: center;
gap: 6px;
margin: 2px 0;
}
}
</style>
<style lang="scss">
@@ -1,31 +0,0 @@
import RFB from '@novnc/novnc/core/rfb';
export function normalizeZoom(value) {
const percent = Number(value);
if (!Number.isFinite(percent)) {
return 100;
}
return Math.min(200, Math.max(50, Math.round(percent / 10) * 10));
}
export default class ZoomableRFB extends RFB {
get zoomPercent() {
return this._zoomPercent ?? 100;
}
set zoomPercent(value) {
this._zoomPercent = normalizeZoom(value);
this._updateScale();
}
_updateScale() {
if (this.scaleViewport) {
super._updateScale();
} else {
this._display.scale = this.zoomPercent / 100;
this._fixScrollbars();
}
}
}
+4
View File
@@ -77,6 +77,10 @@ const FEATURE_FLAGS = {
'staticIPForVM',
'fsFreezeDeadline',
],
'v1.9.1': [
'lvmDmThinOnly',
],
'v1.10.0': [],
};
const generateFeatureFlags = () => {
+47 -9
View File
@@ -57,6 +57,8 @@ import {
} from './table-headers';
import { ADD_ONS } from './harvester-map';
import { registerAddonSideNav } from '../utils/dynamic-nav';
import { setVendor } from '@shell/config/private-label';
import { createCssVars } from '@shell/utils/color';
const TEMPLATE = HCI.VM_VERSION;
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
@@ -109,15 +111,51 @@ 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) => JSON.stringify([
!!getters['harvester-common/isHarvesterPrime'],
getters['prefs/theme'] === 'dark',
getters['harvester-common/privateLabel']
]),
(value) => {
const [isHarvesterPrime, isDark, privateLabel] = JSON.parse(value);
const primeLogo = isDark ? require('../assets/images/dark/suse-virtualization.svg') : require('../assets/images/suse-virtualization.svg');
setVendor(privateLabel || '');
let primaryColorStyle = document.getElementById('harvester-prime-primary-color');
// set harvester prime primary color style to #4dd192 (Cyan Green)
if (isHarvesterPrime) {
if (!primaryColorStyle) {
primaryColorStyle = document.createElement('style');
primaryColorStyle.id = 'harvester-prime-primary-color';
document.head.appendChild(primaryColorStyle);
primaryColorStyle.sheet.insertRule('body.theme-light, body.theme-dark {}', 0);
}
const colors = createCssVars('#4dd192', isDark ? 'dark' : 'light');
const rule = primaryColorStyle.sheet.cssRules[0];
Object.entries(colors).forEach(([property, color]) => rule.style.setProperty(property.trim(), color));
} else {
primaryColorStyle?.remove();
}
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({
+2
View File
@@ -3,6 +3,7 @@ export const HCI_SETTING = {
CONTAINERD_REGISTRY: 'containerd-registry',
LOG_LEVEL: 'log-level',
SERVER_VERSION: 'server-version',
SERVER_FLAVOR: 'server-flavor',
UI_INDEX: 'ui-index',
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
@@ -68,6 +69,7 @@ export const HCI_ALLOWED_SETTINGS = {
kind: 'json', from: 'import', featureFlag: 'csiOnlineExpandValidation'
},
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
[HCI_SETTING.SERVER_FLAVOR]: { readOnly: true },
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
+164
View File
@@ -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>
@@ -7,6 +7,7 @@ import NameNsDescription from '@shell/components/form/NameNsDescription';
import YamlEditor from '@shell/components/YamlEditor';
import CreateEditView from '@shell/mixins/create-edit-view';
import FormValidation from '@shell/mixins/form-validation';
import { HCI } from '@pkg/harvester/config/labels-annotations';
export default {
@@ -23,14 +24,19 @@ export default {
NameNsDescription,
},
mixins: [CreateEditView],
mixins: [CreateEditView, FormValidation],
inheritAttrs: false,
data() {
return {
config: this.value.data?.cloudInit || '',
type: this.value?.metadata?.labels?.[HCI.CLOUD_INIT] || 'user',
config: this.value.data?.cloudInit || '',
type: this.value?.metadata?.labels?.[HCI.CLOUD_INIT] || 'user',
fvFormRuleSets: [{
path: 'metadata.name',
rules: ['required'],
translationKey: 'nameNsDescription.name.label',
}],
};
},
@@ -78,6 +84,7 @@ export default {
:resource="value"
:errors="errors"
:apply-hooks="applyHooks"
:validation-passed="fvFormIsValid"
@finish="save"
@cancel="done"
@error="e=>errors=e"
@@ -86,6 +93,7 @@ export default {
:value="value"
:mode="mode"
:namespaced="true"
:rules="{ name: fvGetAndReportPathRules('metadata.name') }"
@update:value="$emit('update:value', $event)"
/>
@@ -7,8 +7,12 @@ import { allHash } from '@shell/utils/promise';
import { clone } from '@shell/utils/object';
import { HCI } from '../../../types';
import { NODE } from '@shell/config/types';
import { _CREATE, _CLONE } from '@shell/config/query-params';
import { LVM_TOPOLOGY_LABEL } from '../index.vue';
const STRIPED_TYPE = 'striped';
const DM_THIN_TYPE = 'dm-thin';
const DEFAULT_PARAMETERS = [
'type',
'vgName'
@@ -54,8 +58,17 @@ export default {
data() {
const node = (this.value.allowedTopologies?.[0]?.matchLabelExpressions || []).find((t) => t.key === LVM_TOPOLOGY_LABEL)?.values[0];
// striped is dropped once the LVM add-on reaches GA (v1.9.1); dm-thin is the only supported type.
const dmThinOnly = this.$store.getters['harvester-common/getFeatureEnabled']('lvmDmThinOnly');
const volumeGroupTypes = dmThinOnly ? [DM_THIN_TYPE] : [STRIPED_TYPE, DM_THIN_TYPE];
// Default new storage classes to dm-thin, and reset a cloned striped type that is no longer selectable.
if (dmThinOnly && [_CREATE, _CLONE].includes(this.realMode) && !volumeGroupTypes.includes(this.value.parameters.type)) {
this.value.parameters.type = DM_THIN_TYPE;
}
return {
volumeGroupTypes: ['striped', 'dm-thin'],
volumeGroupTypes,
node,
};
},
+22 -27
View File
@@ -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:
@@ -953,33 +955,15 @@ harvester:
console:
down: This virtual machine is down. Please start it to access its console.
shortcutKeys: Shortcut Keys
settings:
label: Settings
viewMode:
label: Display
scale: Scale the display to fit the window
resize: Resize the virtual machine display to the window
none: Manual zoom
zoom: Zoom
clipViewport: Clip to the window instead of showing scrollbars
viewOnly: View only
quality: JPEG quality
compression: Compression level
reset: Reset to defaults
fullscreen:
enter: Full Screen
exit: Exit Full Screen
takeOver:
label: Take Over
confirm: Take over this console? Any existing VNC session for this virtual machine will be disconnected.
status:
connecting: Connecting
connected: Connected
disconnected: Disconnected
failed: Connection failed
reconnecting: 'Reconnecting {retryTimes} of {maximumRetryTimes}'
credentialsRequired: This console requires credentials that the dashboard cannot provide.
unavailable: Unable to connect. Another VNC session may be active, or the console may be unavailable.
customShortcutKeys: Custom Shortcut Keys
management: Management Shortcut Keys
record:
start: Record
recording: Recording
stop: Stop Recording
tips: Press the record button to capture your keyboard inputs.
send: Send
preferredKeys: Preferred Custom Shortcut Keys
terminationGracePeriodSeconds:
label: Termination Grace Period
affinity:
@@ -1850,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.
@@ -2154,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:
@@ -2453,6 +2447,7 @@ advancedSettings:
'harv-containerd-registry': Containerd Registry Configuration to connect private registries.
'harv-log-level': Configure Harvester server log level. Defaults to Info.
'harv-server-version': Harvester server version.
'harv-server-flavor': "Indicates the Harvester edition: Prime or Community."
'harv-upgrade-checker-enabled': Specifies whether to enable Harvester upgrade check or not. Default is True.
'harv-upgrade-checker-url': Default Harvester upgrade check url. Only used when the <code>upgrade-checker-enabled</code> is equal to True.
'harv-ui-source': Configure how to load the UI source. The `External` option is used to load the UI from a ui-index URL, while the `Bundled` option loads the UI from the local Harvester server.
+1 -1
View File
@@ -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"
+2 -1
View File
@@ -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]: '' };
+18 -6
View File
@@ -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() {
@@ -0,0 +1,14 @@
import ManagementSetting from '@shell/models/management.cattle.io.setting';
import { HCI_SETTING } from '../../config/settings';
export default class HarvesterManagementSetting extends ManagementSetting {
get _availableActions() {
const actions = super._availableActions;
if (this.$rootGetters['isStandaloneHarvester'] && this.id === HCI_SETTING.UI_PL) {
return actions.filter((action) => !['goToClone', 'promptRemove'].includes(action.action));
}
return actions;
}
}
+14 -2
View File
@@ -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,18 +1,10 @@
<script>
import { defineAsyncComponent } from 'vue';
import Loading from '@shell/components/Loading';
import TabTitle from '@shell/components/TabTitle';
import { HCI } from '../../../../../types';
import NovncConsoleWrapper from '../../../../../components/novnc/NovncConsoleWrapper.vue';
export default {
components: {
NovncConsoleWrapper: defineAsyncComponent({
loader: () => import('../../../../../components/novnc/NovncConsoleWrapper.vue'),
loadingComponent: Loading,
}),
Loading,
TabTitle
},
components: { NovncConsoleWrapper, Loading },
async fetch() {
this.rows = await this.$store.dispatch('harvester/findAll', { type: HCI.VMI });
@@ -36,21 +28,17 @@ export default {
mounted() {
window.addEventListener('beforeunload', () => {
this.$refs.console?.close();
this.$refs.console.close();
});
},
head() {
return { title: this.vmi?.metadata?.name };
},
};
</script>
<template>
<TabTitle
v-if="vmi?.metadata?.name"
:breadcrumb="false"
:show-child="false"
>
{{ vmi.metadata.name }}
</TabTitle>
<Loading v-if="$fetchState.pending" />
<NovncConsoleWrapper
v-else
+19
View File
@@ -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;
},
+7 -2
View File
@@ -1,3 +1,5 @@
import { FAVORITE_TYPES } from '@shell/store/prefs';
/**
* Dynamically toggles SideNav entries based on the enabled status of a specific Addon.
*
@@ -26,10 +28,13 @@ export function registerAddonSideNav(store, productName, {
// Toggle the trigger a few times so an early kick (fired before the SideNav
// has mounted on first login) is retried once the component is listening.
// Use the prefs/load mutation (in-memory only)
[0, 600, 1500].forEach((delay) => {
setTimeout(() => {
store.dispatch('type-map/addFavorite', TRIGGER);
setTimeout(() => store.dispatch('type-map/removeFavorite', TRIGGER), 300);
const base = (store.getters['prefs/get'](FAVORITE_TYPES) || []).filter((t) => t !== TRIGGER);
store.commit('prefs/load', { key: FAVORITE_TYPES, value: [...base, TRIGGER] });
setTimeout(() => store.commit('prefs/load', { key: FAVORITE_TYPES, value: [...base] }), 300);
}, delay);
});
};
+1 -24
View File
@@ -1,24 +1 @@
const path = require('path');
const baseConfig = require('@rancher/shell/pkg/vue.config')(__dirname);
const baseConfigureWebpack = baseConfig.configureWebpack;
// noVNC >= 1.7 only publishes `./core/rfb.js` in its `exports` map, so resolve the
// package by path to keep the keysym/input helpers importable.
const NOVNC_DIR = path.resolve(__dirname, '..', '..', 'node_modules', '@novnc', 'novnc');
module.exports = {
...baseConfig,
configureWebpack(config) {
if (typeof baseConfigureWebpack === 'function') {
baseConfigureWebpack(config);
}
config.resolve.alias['@novnc/novnc'] = NOVNC_DIR;
// noVNC 1.7 uses top-level await for WebCodecs detection.
config.output = { ...config.output, environment: { ...config.output?.environment, asyncFunction: true } };
}
};
module.exports = require('@rancher/shell/pkg/vue.config')(__dirname);
+1 -21
View File
@@ -1,26 +1,6 @@
const path = require('path');
const config = require('@rancher/shell/vue.config');
const baseConfig = config(__dirname, {
module.exports = config(__dirname, {
excludes: [],
// excludes: ['harvester']
});
const baseConfigureWebpack = baseConfig.configureWebpack;
module.exports = {
...baseConfig,
configureWebpack(webpackConfig) {
if (typeof baseConfigureWebpack === 'function') {
baseConfigureWebpack(webpackConfig);
}
// noVNC >= 1.7 only publishes `./core/rfb.js` in its `exports` map, so resolve the
// package by path to keep the keysym/input helpers importable.
webpackConfig.resolve.alias['@novnc/novnc'] = path.resolve(__dirname, 'node_modules/@novnc/novnc');
// noVNC 1.7 uses top-level await for WebCodecs detection.
webpackConfig.output = { ...webpackConfig.output, environment: { ...webpackConfig.output?.environment, asyncFunction: true } };
}
};
+14 -74
View File
@@ -2097,10 +2097,10 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@novnc/novnc@1.2.0", "@novnc/novnc@1.7.0":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@novnc/novnc/-/novnc-1.7.0.tgz#7f832cf07c66475a81a25708b8e5299a5c4efec5"
integrity sha512-ucEJOx4T2avIRCleodk7YobZj5O2Ga2AeLfQ69A/yjG9HHba2+PDgwSkN3FttrmG+70ZGx21sElNFouK13RzyA==
"@novnc/novnc@1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@novnc/novnc/-/novnc-1.2.0.tgz#535ffd7d074022d35205deae2753100f1f2b29f3"
integrity sha512-FaUckOedGhSbwQBXk/KGyxKt9ngskg4wPw6ghbHWXOUEmQscAZr3467lTU5DSfppwHJt5k+lQiHoeYUuY90l2Q==
"@one-ini/wasm@0.1.1":
version "0.1.1"
@@ -2935,10 +2935,10 @@
dependencies:
"@types/node" "*"
"@types/node@*", "@types/node@25.3.3", "@types/node@25.9.5":
version "25.9.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.9.5.tgz#0fefc09e6e82e94cde291bacf43522e989eb01a4"
integrity sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==
"@types/node@*", "@types/node@25.3.3", "@types/node@25.9.8":
version "25.9.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.9.8.tgz#a460865b194c2c416a8f11841613207848b92583"
integrity sha512-VfMrScDmMhUJQmd5hArdQnFvK0OIeD36uN2Va1FcpYaLB/BgkgM8Ulc50XtYISPMz7APJ30+N0T5EM0jlcdfRw==
dependencies:
undici-types ">=7.24.0 <7.24.7"
@@ -3091,15 +3091,6 @@
"@typescript-eslint/types" "^8.62.0"
debug "^4.4.3"
"@typescript-eslint/project-service@8.68.0":
version "8.68.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.68.0.tgz#ea4b2869f59165c420cd7a4bbebc38039794e8cc"
integrity sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.68.0"
"@typescript-eslint/types" "^8.68.0"
debug "^4.4.3"
"@typescript-eslint/scope-manager@8.62.0":
version "8.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.62.0.tgz#a7a7b428d32444bc9a4fe16f24a78fc124283fd4"
@@ -3108,24 +3099,11 @@
"@typescript-eslint/types" "8.62.0"
"@typescript-eslint/visitor-keys" "8.62.0"
"@typescript-eslint/scope-manager@8.68.0":
version "8.68.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.68.0.tgz#e5a13a1159497faeab4e48279bf07576045b1499"
integrity sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==
dependencies:
"@typescript-eslint/types" "8.68.0"
"@typescript-eslint/visitor-keys" "8.68.0"
"@typescript-eslint/tsconfig-utils@8.62.0":
"@typescript-eslint/tsconfig-utils@8.62.0", "@typescript-eslint/tsconfig-utils@^8.62.0":
version "8.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz#9440a673581c6d9de308c4d5803dd52ed5d71729"
integrity sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==
"@typescript-eslint/tsconfig-utils@8.68.0", "@typescript-eslint/tsconfig-utils@^8.62.0", "@typescript-eslint/tsconfig-utils@^8.68.0":
version "8.68.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.68.0.tgz#594d7a3c5952055b3c431fc563ca7fd1defcce18"
integrity sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==
"@typescript-eslint/type-utils@8.62.0":
version "8.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.62.0.tgz#6f64d813ed9f340d796baed40cdab86b8e9a491a"
@@ -3137,16 +3115,11 @@
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@typescript-eslint/types@8.62.0":
"@typescript-eslint/types@8.62.0", "@typescript-eslint/types@^8.62.0":
version "8.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.62.0.tgz#601427c10203d9f0f34f0b3e474df735eb12b593"
integrity sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==
"@typescript-eslint/types@8.68.0", "@typescript-eslint/types@^8.62.0", "@typescript-eslint/types@^8.68.0":
version "8.68.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.68.0.tgz#3f9d4e62fbe5728f09403cdc7b4d58af842ac1af"
integrity sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==
"@typescript-eslint/typescript-estree@8.62.0":
version "8.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.0.tgz#b96b55d02e26aa09434421c3fa678e525ca09a4c"
@@ -3162,22 +3135,7 @@
tinyglobby "^0.2.15"
ts-api-utils "^2.5.0"
"@typescript-eslint/typescript-estree@8.68.0":
version "8.68.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.68.0.tgz#bf4165029825138ac27231a3ff02923ecd977f38"
integrity sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==
dependencies:
"@typescript-eslint/project-service" "8.68.0"
"@typescript-eslint/tsconfig-utils" "8.68.0"
"@typescript-eslint/types" "8.68.0"
"@typescript-eslint/visitor-keys" "8.68.0"
debug "^4.4.3"
minimatch "^10.2.2"
semver "^7.7.3"
tinyglobby "^0.2.15"
ts-api-utils "^2.5.0"
"@typescript-eslint/utils@8.62.0":
"@typescript-eslint/utils@8.62.0", "@typescript-eslint/utils@^8.0.0", "@typescript-eslint/utils@^8.13.0":
version "8.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.62.0.tgz#b5228524ca1ee51af40e156c82d425dec3e01cfe"
integrity sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==
@@ -3187,16 +3145,6 @@
"@typescript-eslint/types" "8.62.0"
"@typescript-eslint/typescript-estree" "8.62.0"
"@typescript-eslint/utils@^8.0.0", "@typescript-eslint/utils@^8.13.0":
version "8.68.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.68.0.tgz#00547f2c8de8aca2a3c21752a9711f73206fd36d"
integrity sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==
dependencies:
"@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.68.0"
"@typescript-eslint/types" "8.68.0"
"@typescript-eslint/typescript-estree" "8.68.0"
"@typescript-eslint/visitor-keys@8.62.0":
version "8.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.0.tgz#b6daab190bf8f18612f5b86323469a12288c6b31"
@@ -3205,14 +3153,6 @@
"@typescript-eslint/types" "8.62.0"
eslint-visitor-keys "^5.0.0"
"@typescript-eslint/visitor-keys@8.68.0":
version "8.68.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.68.0.tgz#78db3c9bb258a0309d9e2b1b617127c3a8fb1f54"
integrity sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==
dependencies:
"@typescript-eslint/types" "8.68.0"
eslint-visitor-keys "^5.0.0"
"@vee-validate/zod@4.15.0":
version "4.15.0"
resolved "https://registry.yarnpkg.com/@vee-validate/zod/-/zod-4.15.0.tgz#2de193da114eda9f873b5a677f13c23bcecb58aa"
@@ -12712,9 +12652,9 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2:
integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==
yaml@^2.5.1:
version "2.9.0"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.9.0.tgz#78274afd93598a1dfdd6130df6a566defcbf9aa4"
integrity sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==
version "2.9.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.9.1.tgz#c16233fb31944e705cfefaff38795587f57588ce"
integrity sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==
yargs-parser@^18.1.2:
version "18.1.3"