mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-04 13:16:05 +08:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
270977f884 | ||
|
|
50b4cf5248 | ||
|
|
cba37194b6 |
@@ -7,7 +7,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-class-static-block": "7.29.7",
|
"@babel/plugin-transform-class-static-block": "7.29.7",
|
||||||
"@novnc/novnc": "1.7.0",
|
|
||||||
"@rancher/shell": "3.0.13",
|
"@rancher/shell": "3.0.13",
|
||||||
"@vue-flow/background": "^1.3.0",
|
"@vue-flow/background": "^1.3.0",
|
||||||
"@vue-flow/controls": "^1.1.1",
|
"@vue-flow/controls": "^1.1.1",
|
||||||
@@ -28,7 +27,6 @@
|
|||||||
"yaml": "^2.5.1"
|
"yaml": "^2.5.1"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@novnc/novnc": "1.7.0",
|
|
||||||
"@types/node": "25.9.5",
|
"@types/node": "25.9.5",
|
||||||
"cronstrue": "2.59.0",
|
"cronstrue": "2.59.0",
|
||||||
"d3-color": "3.1.0",
|
"d3-color": "3.1.0",
|
||||||
|
|||||||
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: {
|
watch: {
|
||||||
settings: {
|
settings: {
|
||||||
@@ -269,7 +269,8 @@ export default {
|
|||||||
v-if="!setting.hide"
|
v-if="!setting.hide"
|
||||||
class="settings-value"
|
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 === '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 === 'enum'">{{ t(setting.enum) }}</pre>
|
||||||
<pre v-else-if="setting.kind === 'custom' && setting.custom">{{ setting.custom }}</pre>
|
<pre v-else-if="setting.kind === 'custom' && setting.custom">{{ setting.custom }}</pre>
|
||||||
|
|||||||
@@ -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>
|
<script>
|
||||||
|
import RFB from '@novnc/novnc/core/rfb';
|
||||||
import BrandImage from '@shell/components/BrandImage';
|
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 {
|
export default {
|
||||||
name: 'NovncConsole',
|
|
||||||
|
|
||||||
components: { BrandImage },
|
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
url: {
|
url: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
}
|
||||||
|
|
||||||
settings: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
emits: ['status-changed', 'capabilities'],
|
components: { BrandImage },
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
rfb: null,
|
rfb: null,
|
||||||
status: CONSOLE_STATUS.CONNECTING,
|
connected: false,
|
||||||
statusDetail: '',
|
disconnected: false,
|
||||||
desktopName: '',
|
reconnectDelay: 3000,
|
||||||
|
reconnecting: false,
|
||||||
|
maximumRetryTimes: 10,
|
||||||
retryTimes: 0,
|
retryTimes: 0,
|
||||||
maximumRetryTimes: MAXIMUM_RETRY_TIMES,
|
setTimeout: null,
|
||||||
reconnectTimer: null,
|
|
||||||
isClosed: false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
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() {
|
mounted() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.connect();
|
this.connect();
|
||||||
@@ -101,307 +66,88 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.isClosed = true;
|
this.clearTimeout();
|
||||||
this.clearReconnectTimer();
|
|
||||||
this.teardown();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
connect(takeOver = false) {
|
connect() {
|
||||||
if (!this.url || this.isClosed) {
|
const rfb = new RFB(this.$refs.view, this.url);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
this.disconnected = true;
|
||||||
const url = new URL(this.url);
|
this.rfb = null;
|
||||||
|
this.reconnect();
|
||||||
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.rfb = rfb;
|
this.rfb = rfb;
|
||||||
this.applySettings();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
reconnect() {
|
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) {
|
if (this.retryTimes >= this.maximumRetryTimes) {
|
||||||
this.status = CONSOLE_STATUS.FAILED;
|
this.reconnecting = false;
|
||||||
|
this.connected = true;
|
||||||
|
this.disconnected = true;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.retryTimes += 1;
|
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.setTimeout = setTimeout(() => {
|
||||||
|
|
||||||
this.reconnectTimer = setTimeout(() => {
|
|
||||||
this.reconnectTimer = null;
|
|
||||||
this.connect();
|
this.connect();
|
||||||
}, delay);
|
}, this.reconnectDelay);
|
||||||
},
|
},
|
||||||
|
|
||||||
clearReconnectTimer() {
|
clearTimeout() {
|
||||||
if (this.reconnectTimer) {
|
if (this.setTimeout) {
|
||||||
clearTimeout(this.reconnectTimer);
|
clearTimeout(this.setTimeout);
|
||||||
this.reconnectTimer = null;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
teardown(disconnect = true) {
|
disconnect() {
|
||||||
const rfb = this.rfb;
|
this.rfb.disconnect();
|
||||||
|
|
||||||
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);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
ctrlAltDelete() {
|
ctrlAltDelete() {
|
||||||
this.rfb?.sendCtrlAltDel();
|
this.rfb.sendCtrlAltDel();
|
||||||
},
|
},
|
||||||
|
|
||||||
sendKey(keysym, code, down) {
|
sendKey(keysym, code, down) {
|
||||||
this.rfb?.sendKey(keysym, code, down);
|
this.rfb.sendKey(keysym, code, down);
|
||||||
},
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</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>
|
<style lang="scss" scoped>
|
||||||
.novnc-console {
|
.error {
|
||||||
position: relative;
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: 100%;
|
|
||||||
padding: 8px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
|
||||||
|
|
||||||
.novnc-screen {
|
.row {
|
||||||
height: 100%;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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);
|
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 5rem;
|
font-size: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
.desert-landscape {
|
||||||
max-width: 100%;
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
<script>
|
||||||
|
import { STEVE } from '@shell/config/types';
|
||||||
import { escapeHtml } from '@shell/utils/string';
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import KeyTable from '@novnc/novnc/core/input/keysym';
|
import KeyTable from '@novnc/novnc/core/input/keysym';
|
||||||
|
import * as KeyboardUtil from '@novnc/novnc/core/input/util';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
import NovncConsole from './NovncConsole';
|
import NovncConsole from './NovncConsole';
|
||||||
import NovncConsoleItem from './NovncConsoleItem';
|
import NovncConsoleItem from './NovncConsoleItem';
|
||||||
import { normalizeZoom } from './ZoomableRFB';
|
import NovncConsoleCustomKeys from './NovncConsoleCustomKeys';
|
||||||
|
|
||||||
const CONSOLE_SETTINGS_KEY = 'harvester-vnc-console-settings';
|
const PREFERED_SHORTCUT_KEYS = 'prefered-shortcut-keys';
|
||||||
|
|
||||||
const VIEW_MODE = {
|
|
||||||
SCALE: 'scale',
|
|
||||||
NONE: 'none',
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_SETTINGS = {
|
|
||||||
viewMode: VIEW_MODE.NONE,
|
|
||||||
zoomPercent: 100,
|
|
||||||
viewOnly: false,
|
|
||||||
qualityLevel: 6,
|
|
||||||
compressionLevel: 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
const SHORT_KEYS = {
|
const SHORT_KEYS = {
|
||||||
ControlLeft: {
|
ControlLeft: {
|
||||||
@@ -116,7 +105,9 @@ const F_KEYS = {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'NovncConsoleWrapper',
|
name: 'NovncConsoleWrapper',
|
||||||
components: { NovncConsole, NovncConsoleItem },
|
components: {
|
||||||
|
NovncConsole, NovncConsoleItem, NovncConsoleCustomKeys
|
||||||
|
},
|
||||||
|
|
||||||
async fetch() {
|
async fetch() {
|
||||||
const _hash = { vmResource: this.$store.dispatch('harvester/find', { type: HCI.VM, id: this.value.id }) };
|
const _hash = { vmResource: this.$store.dispatch('harvester/find', { type: HCI.VM, id: this.value.id }) };
|
||||||
@@ -140,21 +131,39 @@ export default {
|
|||||||
return {
|
return {
|
||||||
keysRecord: [],
|
keysRecord: [],
|
||||||
vmResource: {},
|
vmResource: {},
|
||||||
settings: { ...DEFAULT_SETTINGS },
|
renderKeysModal: false,
|
||||||
capabilities: {},
|
currentUser: null,
|
||||||
isFullscreen: false,
|
hideCustomKeysBar: false,
|
||||||
consoleStatus: {
|
|
||||||
status: 'connecting',
|
|
||||||
retryTimes: 0,
|
|
||||||
maximumRetryTimes: 0,
|
|
||||||
desktopName: '',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
canSendKeys() {
|
savedShortcutKeys() {
|
||||||
return this.consoleStatus.status === 'connected' && !this.settings.viewOnly;
|
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() {
|
isDown() {
|
||||||
@@ -183,16 +192,13 @@ export default {
|
|||||||
...F_KEYS,
|
...F_KEYS,
|
||||||
};
|
};
|
||||||
|
|
||||||
out.AltLeft = { ...SHORT_KEYS.AltLeft, keys: { PrintScreen: FUNCTION_KEYS.PrintScreen, ...F_KEYS } };
|
out.AltLeft.keys = { PrintScreen: FUNCTION_KEYS.PrintScreen, ...F_KEYS };
|
||||||
out.ControlLeft = {
|
out.ControlLeft.keys = {
|
||||||
...SHORT_KEYS.ControlLeft,
|
AltLeft: {
|
||||||
keys: {
|
...Object.assign(SHORT_KEYS.AltLeft, {}),
|
||||||
AltLeft: {
|
keys: { Delete: FUNCTION_KEYS.Delete }
|
||||||
...SHORT_KEYS.AltLeft,
|
|
||||||
keys: { Delete: FUNCTION_KEYS.Delete }
|
|
||||||
},
|
|
||||||
...NORMAL_KEYS,
|
|
||||||
},
|
},
|
||||||
|
...NORMAL_KEYS,
|
||||||
};
|
};
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
@@ -202,53 +208,14 @@ export default {
|
|||||||
return !!this.vmResource?.actions?.softreboot;
|
return !!this.vmResource?.actions?.softreboot;
|
||||||
},
|
},
|
||||||
|
|
||||||
rfbSettings() {
|
preferredShortcutKeys() {
|
||||||
return {
|
return (this.savedShortcutKeys || []).map((item) => {
|
||||||
viewOnly: this.settings.viewOnly,
|
return {
|
||||||
scaleViewport: this.settings.viewMode === VIEW_MODE.SCALE,
|
label: item.map((K) => K.key.charAt(0).toUpperCase() + K.key.slice(1)).join('+'),
|
||||||
zoomPercent: normalizeZoom(this.settings.zoomPercent),
|
value: item
|
||||||
resizeSession: false,
|
};
|
||||||
clipViewport: false,
|
});
|
||||||
dragViewport: false,
|
|
||||||
qualityLevel: this.settings.qualityLevel,
|
|
||||||
compressionLevel: this.settings.compressionLevel,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
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: {
|
methods: {
|
||||||
@@ -266,221 +233,137 @@ export default {
|
|||||||
|
|
||||||
// Send function key, e.g. ALT + F
|
// Send function key, e.g. ALT + F
|
||||||
sendKeys() {
|
sendKeys() {
|
||||||
if (!this.canSendKeys || !this.$refs.novncConsole) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.keysRecord.forEach((key) => {
|
this.keysRecord.forEach((key) => {
|
||||||
this.$refs.novncConsole.sendKey(this.allKeys[key].value, key, true);
|
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.novncConsole.sendKey(this.allKeys[key].value, key, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$refs.popover.hide();
|
this.$refs.popover.isOpen = false;
|
||||||
this.keysRecord = [];
|
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() {
|
reconnect() {
|
||||||
this.$refs.novncConsole.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() {
|
softReboot() {
|
||||||
this.vmResource.doSoftReboot();
|
this.vmResource.doSoftReboot();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showKeysModal() {
|
||||||
|
this.renderKeysModal = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
hideKeysModal() {
|
||||||
|
this.renderKeysModal = false;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div
|
<div class="vm-console">
|
||||||
ref="consoleContainer"
|
|
||||||
class="vm-console"
|
|
||||||
>
|
|
||||||
<div class="combination-keys">
|
<div class="combination-keys">
|
||||||
<div class="toolbar-group">
|
<v-dropdown
|
||||||
<v-dropdown
|
ref="popover"
|
||||||
ref="popover"
|
placement="top"
|
||||||
placement="top"
|
trigger="click"
|
||||||
trigger="click"
|
:container="false"
|
||||||
:container="false"
|
@auto-hide="keysRecord = []"
|
||||||
@auto-hide="keysRecord = []"
|
>
|
||||||
>
|
<button class="btn btn-sm bg-primary">
|
||||||
<button
|
{{ t("harvester.virtualMachine.detail.console.shortcutKeys") }}
|
||||||
class="btn btn-sm bg-primary"
|
</button>
|
||||||
:disabled="!canSendKeys"
|
|
||||||
|
<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>
|
<NovncConsoleCustomKeys
|
||||||
<novnc-console-item
|
v-if="renderKeysModal"
|
||||||
:items="keymap"
|
:current-user="currentUser"
|
||||||
:path="keysRecord"
|
@close="hideKeysModal"
|
||||||
: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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NovncConsole
|
<NovncConsole
|
||||||
v-if="url && !isDown"
|
v-if="url && !isDown"
|
||||||
ref="novncConsole"
|
ref="novncConsole"
|
||||||
:url="url"
|
:url="url"
|
||||||
:settings="rfbSettings"
|
|
||||||
@status-changed="onStatusChanged"
|
|
||||||
@capabilities="onCapabilities"
|
|
||||||
/>
|
/>
|
||||||
<p v-if="isDown">
|
<p v-if="isDown">
|
||||||
{{ t("harvester.virtualMachine.detail.console.down") }}
|
{{ t("harvester.virtualMachine.detail.console.down") }}
|
||||||
@@ -491,56 +374,14 @@ export default {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.vm-console {
|
.vm-console {
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
height: 100dvh;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 32px minmax(0, 1fr);
|
grid-template-rows: 30px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.combination-keys {
|
.combination-keys {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
background: rgb(40, 40, 40);
|
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>
|
||||||
|
|
||||||
<style lang="scss">
|
<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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -57,6 +57,7 @@ import {
|
|||||||
} from './table-headers';
|
} from './table-headers';
|
||||||
import { ADD_ONS } from './harvester-map';
|
import { ADD_ONS } from './harvester-map';
|
||||||
import { registerAddonSideNav } from '../utils/dynamic-nav';
|
import { registerAddonSideNav } from '../utils/dynamic-nav';
|
||||||
|
import { setVendor } from '@shell/config/private-label';
|
||||||
|
|
||||||
const TEMPLATE = HCI.VM_VERSION;
|
const TEMPLATE = HCI.VM_VERSION;
|
||||||
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
|
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
|
||||||
@@ -109,15 +110,31 @@ export function init($plugin, store) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
store.dispatch('setIsSingleProduct', {
|
store.watch(
|
||||||
productName: PRODUCT_NAME,
|
(_state, getters) => ({
|
||||||
logo: require(`@shell/assets/images/providers/harvester.svg`),
|
isHarvesterPrime: !!getters['harvester-common/isHarvesterPrime'],
|
||||||
productNameKey: 'harvester.productLabel',
|
isDark: getters['prefs/theme'] === 'dark',
|
||||||
getVersionInfo: (store) => store.getters[`${ PRODUCT_NAME }/byId`]?.(HCI.SETTING, 'server-version')?.value || 'unknown',
|
privateLabel: getters['harvester-common/privateLabel']
|
||||||
afterLoginRoute: home,
|
}),
|
||||||
logoRoute: home,
|
({ isHarvesterPrime, isDark, privateLabel }) => {
|
||||||
supportCustomLogo: true
|
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({
|
product({
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -84,6 +84,8 @@ dialog:
|
|||||||
|
|
||||||
harvester:
|
harvester:
|
||||||
branding:
|
branding:
|
||||||
|
primeLabel: '{vendor}'
|
||||||
|
primeEmptyLabel: ''
|
||||||
logos:
|
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.'
|
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:
|
favicon:
|
||||||
@@ -953,33 +955,15 @@ harvester:
|
|||||||
console:
|
console:
|
||||||
down: This virtual machine is down. Please start it to access its console.
|
down: This virtual machine is down. Please start it to access its console.
|
||||||
shortcutKeys: Shortcut Keys
|
shortcutKeys: Shortcut Keys
|
||||||
settings:
|
customShortcutKeys: Custom Shortcut Keys
|
||||||
label: Settings
|
management: Management Shortcut Keys
|
||||||
viewMode:
|
record:
|
||||||
label: Display
|
start: Record
|
||||||
scale: Scale the display to fit the window
|
recording: Recording
|
||||||
resize: Resize the virtual machine display to the window
|
stop: Stop Recording
|
||||||
none: Manual zoom
|
tips: Press the record button to capture your keyboard inputs.
|
||||||
zoom: Zoom
|
send: Send
|
||||||
clipViewport: Clip to the window instead of showing scrollbars
|
preferredKeys: Preferred Custom Shortcut Keys
|
||||||
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.
|
|
||||||
terminationGracePeriodSeconds:
|
terminationGracePeriodSeconds:
|
||||||
label: Termination Grace Period
|
label: Termination Grace Period
|
||||||
affinity:
|
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-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-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.
|
'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-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-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.
|
'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
|
hostname: Hostname
|
||||||
rancherVersion: Rancher Version
|
rancherVersion: Rancher Version
|
||||||
password: Bootstrap Password
|
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!"
|
switchFailed: "{action} {name} failed!"
|
||||||
nvidiaDriverToolkit:
|
nvidiaDriverToolkit:
|
||||||
titles:
|
titles:
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<a
|
<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')"
|
v-tooltip="t('harvester.addons.rancherVcluster.accessRancher')"
|
||||||
class="ml-5"
|
class="ml-5"
|
||||||
rel="nofollow noopener noreferrer"
|
rel="nofollow noopener noreferrer"
|
||||||
|
|||||||
@@ -1317,8 +1317,9 @@ export default {
|
|||||||
case SOURCE_TYPE.IMAGE: {
|
case SOURCE_TYPE.IMAGE: {
|
||||||
const image = this.images.find( (I) => R.image === I.id);
|
const image = this.images.find( (I) => R.image === I.id);
|
||||||
|
|
||||||
|
out.spec.storageClassName = R.realName ? R.storageClassName : (R.storageClassName || image?.storageClassName);
|
||||||
|
|
||||||
if (image) {
|
if (image) {
|
||||||
out.spec.storageClassName = image.storageClassName;
|
|
||||||
out.metadata.annotations = { [HCI_ANNOTATIONS.IMAGE_ID]: image.id };
|
out.metadata.annotations = { [HCI_ANNOTATIONS.IMAGE_ID]: image.id };
|
||||||
} else {
|
} else {
|
||||||
out.metadata.annotations = { [HCI_ANNOTATIONS.IMAGE_ID]: '' };
|
out.metadata.annotations = { [HCI_ANNOTATIONS.IMAGE_ID]: '' };
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import HarvesterResource from './harvester';
|
|||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
|
|
||||||
const HARVESTER_NVIDIA_DRIVER_TOOLKIT = 'harvester-system/nvidia-driver-toolkit';
|
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 {
|
export default class HciAddonConfig extends HarvesterResource {
|
||||||
get availableActions() {
|
get availableActions() {
|
||||||
const out = super._availableActions;
|
const out = super._availableActions;
|
||||||
|
|
||||||
if (this.id === 'harvester-system/rancher-vcluster') {
|
if (this.id === RANCHER_VCLUSTER || this.id === RANCHER_K3K_ADDON) {
|
||||||
const rancherDashboard = {
|
const rancherDashboard = {
|
||||||
action: 'goToRancher',
|
action: 'goToRancher',
|
||||||
enabled: this.spec.enabled,
|
enabled: this.spec.enabled,
|
||||||
@@ -57,6 +59,15 @@ export default class HciAddonConfig extends HarvesterResource {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.spec.enabled && this.id === RANCHER_K3K_ADDON) {
|
||||||
|
this.$dispatch('promptModal', {
|
||||||
|
resources: [this],
|
||||||
|
component: 'HarvesterEnableK3k',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.spec.enabled = !this.spec.enabled;
|
this.spec.enabled = !this.spec.enabled;
|
||||||
await this.save();
|
await this.save();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -69,18 +80,19 @@ export default class HciAddonConfig extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
goToRancher() {
|
goToRancher() {
|
||||||
const valuesContent = jsyaml.load(this.spec.valuesContent);
|
|
||||||
|
|
||||||
window.open(
|
window.open(
|
||||||
`https://${ valuesContent.hostname }`,
|
this.rancherHostname,
|
||||||
'_blank',
|
'_blank',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get rancherHostname() {
|
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() {
|
get stateColor() {
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
vendor: getVendor(),
|
vendor: getVendor(),
|
||||||
uiPLSetting: {},
|
uiPLSetting: {},
|
||||||
|
privateLabelDraft: null,
|
||||||
uiLogoDarkSetting: {},
|
uiLogoDarkSetting: {},
|
||||||
uiLogoDark: '',
|
uiLogoDark: '',
|
||||||
uiLogoLightSetting: {},
|
uiLogoLightSetting: {},
|
||||||
@@ -81,6 +82,17 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
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() {
|
mode() {
|
||||||
const schema = this.$store.getters[`management/schemaFor`](MANAGEMENT.SETTING);
|
const schema = this.$store.getters[`management/schemaFor`](MANAGEMENT.SETTING);
|
||||||
|
|
||||||
@@ -112,7 +124,7 @@ export default {
|
|||||||
this.errors.push(e);
|
this.errors.push(e);
|
||||||
},
|
},
|
||||||
async save(btnCB) {
|
async save(btnCB) {
|
||||||
this.uiPLSetting.value = this.uiPLSetting.value.replaceAll(/[\<>&=#()"]/gm, '');
|
this.uiPLSetting.value = (this.privateLabel || '').replaceAll(/[\<>&=#()"]/gm, '');
|
||||||
if (this.customizeLogo) {
|
if (this.customizeLogo) {
|
||||||
this.uiLogoLightSetting.value = this.uiLogoLight;
|
this.uiLogoLightSetting.value = this.uiLogoLight;
|
||||||
this.uiLogoDarkSetting.value = this.uiLogoDark;
|
this.uiLogoDarkSetting.value = this.uiLogoDark;
|
||||||
@@ -169,7 +181,7 @@ export default {
|
|||||||
<div class="row mb-20">
|
<div class="row mb-20">
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-model:value="uiPLSetting.value"
|
v-model:value="privateLabel"
|
||||||
:label="t('branding.uiPL.label')"
|
:label="t('branding.uiPL.label')"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:maxlength="100"
|
:maxlength="100"
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import { defineAsyncComponent } from 'vue';
|
|
||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
import TabTitle from '@shell/components/TabTitle';
|
|
||||||
import { HCI } from '../../../../../types';
|
import { HCI } from '../../../../../types';
|
||||||
|
import NovncConsoleWrapper from '../../../../../components/novnc/NovncConsoleWrapper.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: { NovncConsoleWrapper, Loading },
|
||||||
NovncConsoleWrapper: defineAsyncComponent({
|
|
||||||
loader: () => import('../../../../../components/novnc/NovncConsoleWrapper.vue'),
|
|
||||||
loadingComponent: Loading,
|
|
||||||
}),
|
|
||||||
Loading,
|
|
||||||
TabTitle
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
async fetch() {
|
||||||
this.rows = await this.$store.dispatch('harvester/findAll', { type: HCI.VMI });
|
this.rows = await this.$store.dispatch('harvester/findAll', { type: HCI.VMI });
|
||||||
@@ -36,21 +28,17 @@ export default {
|
|||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener('beforeunload', () => {
|
||||||
this.$refs.console?.close();
|
this.$refs.console.close();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
head() {
|
||||||
|
return { title: this.vmi?.metadata?.name };
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TabTitle
|
|
||||||
v-if="vmi?.metadata?.name"
|
|
||||||
:breadcrumb="false"
|
|
||||||
:show-child="false"
|
|
||||||
>
|
|
||||||
{{ vmi.metadata.name }}
|
|
||||||
</TabTitle>
|
|
||||||
<Loading v-if="$fetchState.pending" />
|
<Loading v-if="$fetchState.pending" />
|
||||||
<NovncConsoleWrapper
|
<NovncConsoleWrapper
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import Parse from 'url-parse';
|
import Parse from 'url-parse';
|
||||||
|
import { MANAGEMENT } from '@shell/config/types';
|
||||||
|
import { SETTING } from '@shell/config/settings';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import { PRODUCT_NAME } from '../config/harvester';
|
import { PRODUCT_NAME } from '../config/harvester';
|
||||||
import { featureEnabled, getVersion } from '../utils/feature-flags';
|
import { featureEnabled, getVersion } from '../utils/feature-flags';
|
||||||
@@ -64,6 +66,23 @@ const mutations = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getters = {
|
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) {
|
getBundleId(state) {
|
||||||
return state.latestBundleId;
|
return state.latestBundleId;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,24 +1 @@
|
|||||||
const path = require('path');
|
module.exports = require('@rancher/shell/pkg/vue.config')(__dirname);
|
||||||
|
|
||||||
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 } };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
+1
-21
@@ -1,26 +1,6 @@
|
|||||||
const path = require('path');
|
|
||||||
const config = require('@rancher/shell/vue.config');
|
const config = require('@rancher/shell/vue.config');
|
||||||
|
|
||||||
const baseConfig = config(__dirname, {
|
module.exports = config(__dirname, {
|
||||||
excludes: [],
|
excludes: [],
|
||||||
// excludes: ['harvester']
|
// 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 } };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -2097,10 +2097,10 @@
|
|||||||
"@nodelib/fs.scandir" "2.1.5"
|
"@nodelib/fs.scandir" "2.1.5"
|
||||||
fastq "^1.6.0"
|
fastq "^1.6.0"
|
||||||
|
|
||||||
"@novnc/novnc@1.2.0", "@novnc/novnc@1.7.0":
|
"@novnc/novnc@1.2.0":
|
||||||
version "1.7.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@novnc/novnc/-/novnc-1.7.0.tgz#7f832cf07c66475a81a25708b8e5299a5c4efec5"
|
resolved "https://registry.yarnpkg.com/@novnc/novnc/-/novnc-1.2.0.tgz#535ffd7d074022d35205deae2753100f1f2b29f3"
|
||||||
integrity sha512-ucEJOx4T2avIRCleodk7YobZj5O2Ga2AeLfQ69A/yjG9HHba2+PDgwSkN3FttrmG+70ZGx21sElNFouK13RzyA==
|
integrity sha512-FaUckOedGhSbwQBXk/KGyxKt9ngskg4wPw6ghbHWXOUEmQscAZr3467lTU5DSfppwHJt5k+lQiHoeYUuY90l2Q==
|
||||||
|
|
||||||
"@one-ini/wasm@0.1.1":
|
"@one-ini/wasm@0.1.1":
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user