mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-04 05:06:05 +08:00
Compare commits
64
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96c374b7aa | ||
|
|
bbe0c4643f | ||
|
|
00584f1d19 | ||
|
|
4fa6470343 | ||
|
|
d550046189 | ||
|
|
5f32a9ade5 | ||
|
|
3f39d561be | ||
|
|
7c84269ed6 | ||
|
|
f63be73b11 | ||
|
|
fabc1cec21 | ||
|
|
ed2b6f2551 | ||
|
|
72282ae50e | ||
|
|
8fba4e7050 | ||
|
|
fbd4a1108f | ||
|
|
823a88c85b | ||
|
|
3a2c6e3a3d | ||
|
|
be6e9b5f06 | ||
|
|
a88bed9eb4 | ||
|
|
f97f14c811 | ||
|
|
f8a29a1c54 | ||
|
|
9a935434c2 | ||
|
|
afe632b723 | ||
|
|
d050f21f9a | ||
|
|
b87f367583 | ||
|
|
92aa8ec603 | ||
|
|
e013534373 | ||
|
|
d23c3d3b72 | ||
|
|
719c33fa12 | ||
|
|
a6bd7ec00e | ||
|
|
ac26fe2f0e | ||
|
|
790cade04d | ||
|
|
2903e86c02 | ||
|
|
2d747e435a | ||
|
|
eadda2a18b | ||
|
|
a178302f73 | ||
|
|
d376bfb3a5 | ||
|
|
7a84e08f99 | ||
|
|
3d39626b93 | ||
|
|
1c3b40b631 | ||
|
|
87baf75301 | ||
|
|
a10d651827 | ||
|
|
11ff7c8a7c | ||
|
|
f6dffe9816 | ||
|
|
b813bc578e | ||
|
|
7c3b2730ab | ||
|
|
429d9e18ad | ||
|
|
dc166d8fc9 | ||
|
|
3ff4534294 | ||
|
|
d4223ad2ae | ||
|
|
44297ada16 | ||
|
|
aea53fdd65 | ||
|
|
9640c87947 | ||
|
|
2cefd7cb78 | ||
|
|
fb6ffa3c0f | ||
|
|
e0436bff28 | ||
|
|
219f39ae52 | ||
|
|
41cf44d231 | ||
|
|
7d09cb7142 | ||
|
|
560643d299 | ||
|
|
c3bb0ea4be | ||
|
|
3c37f04ef6 | ||
|
|
d98db6ba49 | ||
|
|
531ed03796 | ||
|
|
59f260eab8 |
@@ -8,12 +8,16 @@
|
|||||||
- [ ] Yes, the backend owner is @
|
- [ ] Yes, the backend owner is @
|
||||||
- [ ] No, frontend-only.
|
- [ ] No, frontend-only.
|
||||||
|
|
||||||
### Related Issue #
|
### Related Issue
|
||||||
<!-- Link the issue as harvester/harvester#<issue_number> -->
|
<!-- Link the issue as harvester/harvester#<issue_number> -->
|
||||||
harvester/harvester#<issue_number>
|
harvester/harvester#<issue_number>
|
||||||
|
|
||||||
|
|
||||||
### Test screenshot or video (Required)
|
### Test Steps
|
||||||
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->
|
<!-- Provide the test steps for reviewer to verify the issue -->
|
||||||
|
1. Go to ... page
|
||||||
|
|
||||||
|
### Test screenshot or video
|
||||||
|
<!-- Attach screenshot or video of the changes and eventual comparison -->
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
name: Build Standalone on PR Merge
|
name: Build Standalone on PR Merge
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch: # manually trigger on Github Actions only support PR branch pushed to harvester/harvester-ui-extension
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -29,3 +30,35 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
with:
|
with:
|
||||||
CI_BRANCH: ${{github.ref_name}}
|
CI_BRANCH: ${{github.ref_name}}
|
||||||
|
comment-on-pr:
|
||||||
|
name: Comment build result on PR
|
||||||
|
# Only for manual runs so the existing push / pull_request flows are untouched.
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Add build artifact comment to the PR opened from this branch
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
BRANCH: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# Match scripts/version: sanitize the branch and map main -> latest.
|
||||||
|
if [ "$BRANCH" = "main" ]; then
|
||||||
|
DIR="latest"
|
||||||
|
else
|
||||||
|
DIR=$(printf '%s' "$BRANCH" | sed -E 's/[^a-zA-Z0-9.-]+/-/g')
|
||||||
|
fi
|
||||||
|
URL="https://releases.rancher.com/harvester-ui/dashboard/${DIR}/index.html"
|
||||||
|
|
||||||
|
PR=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --base main --head "$BRANCH" --json number --jq '.[0].number // empty')
|
||||||
|
if [ -z "$PR" ]; then
|
||||||
|
echo "No open PR found for branch $BRANCH, skipping comment."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
BODY=$(printf '🚀 **UI Artifact Build Status:** `SUCCESS` ✅\n> ui-index: %s\n> ui-source: external' "$URL")
|
||||||
|
gh pr comment "$PR" --repo "$GITHUB_REPOSITORY" --body "$BODY"
|
||||||
|
|||||||
+10
-6
@@ -1,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.9.0-rc5",
|
"version": "1.10.0-dev",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.0.0"
|
"node": ">=24.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-class-static-block": "7.29.7",
|
"@babel/plugin-transform-class-static-block": "7.29.7",
|
||||||
"@rancher/shell": "3.0.12-rc.7",
|
"@novnc/novnc": "1.7.0",
|
||||||
|
"@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",
|
||||||
"@vue-flow/core": "^1.33.5",
|
"@vue-flow/core": "^1.33.5",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"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",
|
||||||
@@ -39,7 +41,7 @@
|
|||||||
"merge": "2.1.1",
|
"merge": "2.1.1",
|
||||||
"node-forge": "1.4.0",
|
"node-forge": "1.4.0",
|
||||||
"nth-check": "2.1.1",
|
"nth-check": "2.1.1",
|
||||||
"qs": "6.15.3",
|
"qs": "6.16.0",
|
||||||
"roarr": "7.21.7",
|
"roarr": "7.21.7",
|
||||||
"semver": "7.8.5",
|
"semver": "7.8.5",
|
||||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||||
@@ -48,15 +50,17 @@
|
|||||||
"dev": "NODE_ENV=dev ./node_modules/.bin/vue-cli-service serve",
|
"dev": "NODE_ENV=dev ./node_modules/.bin/vue-cli-service serve",
|
||||||
"build": "./node_modules/.bin/vue-cli-service build",
|
"build": "./node_modules/.bin/vue-cli-service build",
|
||||||
"clean": "./node_modules/@rancher/shell/scripts/clean",
|
"clean": "./node_modules/@rancher/shell/scripts/clean",
|
||||||
"lint": "./node_modules/.bin/eslint --max-warnings 0 --ext .js,.ts,.vue .",
|
"lint": "./node_modules/@rancher/shell/scripts/lint --max-warnings 0 --ext .js,.ts,.vue .",
|
||||||
"lint:fix": "./node_modules/.bin/eslint --fix --max-warnings 0 --ext .js,.ts,.vue .",
|
"lint:fix": "./node_modules/@rancher/shell/scripts/lint --fix --max-warnings 0 --ext .js,.ts,.vue .",
|
||||||
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
|
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
|
||||||
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
|
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
|
||||||
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
|
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
|
||||||
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
|
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
|
||||||
"commitlint": "commitlint --edit",
|
"commitlint": "commitlint --edit",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"agents:generate": "./scripts/generate-agent-and-persona-mds.sh"
|
"agents:generate": "./scripts/generate-agent-and-persona-mds.sh",
|
||||||
|
"prelint": "npm --prefix node_modules/@rancher/shell/eslint-legacy install eslint-plugin-jest@^24 --no-save --no-audit --no-fund --legacy-peer-deps --loglevel=error",
|
||||||
|
"prelint:fix": "npm --prefix node_modules/@rancher/shell/eslint-legacy install eslint-plugin-jest@^24 --no-save --no-audit --no-fund --legacy-peer-deps --loglevel=error"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/load": "^19.8.1",
|
"@commitlint/load": "^19.8.1",
|
||||||
|
|||||||
@@ -231,14 +231,21 @@ export default {
|
|||||||
v-if="setting.customized"
|
v-if="setting.customized"
|
||||||
class="modified"
|
class="modified"
|
||||||
>
|
>
|
||||||
Modified
|
{{ t('advancedSettings.tags.modified.label') }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="setting.experimental"
|
v-if="setting.experimental"
|
||||||
v-clean-tooltip="t('advancedSettings.experimental')"
|
v-clean-tooltip="t('advancedSettings.tags.experimental.description')"
|
||||||
class="experimental"
|
class="experimental"
|
||||||
>
|
>
|
||||||
Experimental
|
{{ t('advancedSettings.tags.experimental.label') }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="setting.preview"
|
||||||
|
v-clean-tooltip="t('advancedSettings.tags.preview.description')"
|
||||||
|
class="preview"
|
||||||
|
>
|
||||||
|
{{ t('advancedSettings.tags.preview.label') }}
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
|
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
|
||||||
@@ -352,21 +359,19 @@ export default {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modified {
|
.modified,
|
||||||
|
.experimental,
|
||||||
|
.preview {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
border: 1px solid var(--primary);
|
border: 1px solid;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.experimental {
|
.modified { border-color: var(--primary); }
|
||||||
margin-left: 10px;
|
.experimental { border-color: var(--error); }
|
||||||
border: 1px solid var(--error);
|
.preview { border-color: var(--warning); }
|
||||||
border-radius: 5px;
|
|
||||||
padding: 2px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-search-match {
|
.no-search-match {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -1,64 +1,99 @@
|
|||||||
<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: () => ({})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
components: { BrandImage },
|
emits: ['status-changed', 'capabilities'],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
rfb: null,
|
rfb: null,
|
||||||
connected: false,
|
status: CONSOLE_STATUS.CONNECTING,
|
||||||
disconnected: false,
|
statusDetail: '',
|
||||||
reconnectDelay: 3000,
|
desktopName: '',
|
||||||
reconnecting: false,
|
|
||||||
maximumRetryTimes: 10,
|
|
||||||
retryTimes: 0,
|
retryTimes: 0,
|
||||||
setTimeout: null,
|
maximumRetryTimes: MAXIMUM_RETRY_TIMES,
|
||||||
|
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();
|
||||||
@@ -66,88 +101,307 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.clearTimeout();
|
this.isClosed = true;
|
||||||
|
this.clearReconnectTimer();
|
||||||
|
this.teardown();
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
connect() {
|
connect(takeOver = false) {
|
||||||
const rfb = new RFB(this.$refs.view, this.url);
|
if (!this.url || this.isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
rfb.addEventListener('connect', () => {
|
this.teardown();
|
||||||
this.clearTimeout();
|
|
||||||
|
|
||||||
this.connected = true;
|
this.status = this.retryTimes > 0 ? CONSOLE_STATUS.RECONNECTING : CONSOLE_STATUS.CONNECTING;
|
||||||
this.retryTimes = 0;
|
|
||||||
this.reconnecting = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
rfb.addEventListener('disconnect', (e) => {
|
let rfb;
|
||||||
this.clearTimeout();
|
|
||||||
|
|
||||||
this.disconnected = true;
|
try {
|
||||||
this.rfb = null;
|
const url = new URL(this.url);
|
||||||
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.reconnecting = false;
|
this.status = CONSOLE_STATUS.FAILED;
|
||||||
this.connected = true;
|
|
||||||
this.disconnected = true;
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.retryTimes += 1;
|
this.retryTimes += 1;
|
||||||
this.reconnecting = true;
|
this.status = CONSOLE_STATUS.RECONNECTING;
|
||||||
this.connected = false;
|
|
||||||
this.disconnected = false;
|
|
||||||
|
|
||||||
this.setTimeout = setTimeout(() => {
|
const delay = Math.min(BASE_RECONNECT_DELAY * (2 ** (this.retryTimes - 1)), MAXIMUM_RECONNECT_DELAY);
|
||||||
|
|
||||||
|
this.reconnectTimer = setTimeout(() => {
|
||||||
|
this.reconnectTimer = null;
|
||||||
this.connect();
|
this.connect();
|
||||||
}, this.reconnectDelay);
|
}, delay);
|
||||||
},
|
},
|
||||||
|
|
||||||
clearTimeout() {
|
clearReconnectTimer() {
|
||||||
if (this.setTimeout) {
|
if (this.reconnectTimer) {
|
||||||
clearTimeout(this.setTimeout);
|
clearTimeout(this.reconnectTimer);
|
||||||
|
this.reconnectTimer = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
disconnect() {
|
teardown(disconnect = true) {
|
||||||
this.rfb.disconnect();
|
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);
|
||||||
},
|
},
|
||||||
|
|
||||||
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>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<template>
|
||||||
.error {
|
<div class="novnc-console">
|
||||||
overflow: hidden;
|
<div
|
||||||
|
ref="view"
|
||||||
|
class="novnc-screen"
|
||||||
|
/>
|
||||||
|
|
||||||
.row {
|
<div
|
||||||
align-items: center;
|
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;
|
||||||
|
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);
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 5rem;
|
font-size: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.desert-landscape {
|
img {
|
||||||
img {
|
max-width: 100%;
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,264 +0,0 @@
|
|||||||
<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,15 +1,26 @@
|
|||||||
<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 NovncConsoleCustomKeys from './NovncConsoleCustomKeys';
|
import { normalizeZoom } from './ZoomableRFB';
|
||||||
|
|
||||||
const PREFERED_SHORTCUT_KEYS = 'prefered-shortcut-keys';
|
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 SHORT_KEYS = {
|
const SHORT_KEYS = {
|
||||||
ControlLeft: {
|
ControlLeft: {
|
||||||
@@ -105,9 +116,7 @@ const F_KEYS = {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'NovncConsoleWrapper',
|
name: 'NovncConsoleWrapper',
|
||||||
components: {
|
components: { NovncConsole, NovncConsoleItem },
|
||||||
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 }) };
|
||||||
@@ -131,39 +140,21 @@ export default {
|
|||||||
return {
|
return {
|
||||||
keysRecord: [],
|
keysRecord: [],
|
||||||
vmResource: {},
|
vmResource: {},
|
||||||
renderKeysModal: false,
|
settings: { ...DEFAULT_SETTINGS },
|
||||||
currentUser: null,
|
capabilities: {},
|
||||||
hideCustomKeysBar: false,
|
isFullscreen: false,
|
||||||
|
consoleStatus: {
|
||||||
|
status: 'connecting',
|
||||||
|
retryTimes: 0,
|
||||||
|
maximumRetryTimes: 0,
|
||||||
|
desktopName: '',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
savedShortcutKeys() {
|
canSendKeys() {
|
||||||
const preference = this.$store.getters['management/all'](STEVE.PREFERENCE);
|
return this.consoleStatus.status === 'connected' && !this.settings.viewOnly;
|
||||||
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() {
|
||||||
@@ -192,13 +183,16 @@ export default {
|
|||||||
...F_KEYS,
|
...F_KEYS,
|
||||||
};
|
};
|
||||||
|
|
||||||
out.AltLeft.keys = { PrintScreen: FUNCTION_KEYS.PrintScreen, ...F_KEYS };
|
out.AltLeft = { ...SHORT_KEYS.AltLeft, keys: { PrintScreen: FUNCTION_KEYS.PrintScreen, ...F_KEYS } };
|
||||||
out.ControlLeft.keys = {
|
out.ControlLeft = {
|
||||||
AltLeft: {
|
...SHORT_KEYS.ControlLeft,
|
||||||
...Object.assign(SHORT_KEYS.AltLeft, {}),
|
keys: {
|
||||||
keys: { Delete: FUNCTION_KEYS.Delete }
|
AltLeft: {
|
||||||
|
...SHORT_KEYS.AltLeft,
|
||||||
|
keys: { Delete: FUNCTION_KEYS.Delete }
|
||||||
|
},
|
||||||
|
...NORMAL_KEYS,
|
||||||
},
|
},
|
||||||
...NORMAL_KEYS,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
@@ -208,14 +202,53 @@ export default {
|
|||||||
return !!this.vmResource?.actions?.softreboot;
|
return !!this.vmResource?.actions?.softreboot;
|
||||||
},
|
},
|
||||||
|
|
||||||
preferredShortcutKeys() {
|
rfbSettings() {
|
||||||
return (this.savedShortcutKeys || []).map((item) => {
|
return {
|
||||||
return {
|
viewOnly: this.settings.viewOnly,
|
||||||
label: item.map((K) => K.key.charAt(0).toUpperCase() + K.key.slice(1)).join('+'),
|
scaleViewport: this.settings.viewMode === VIEW_MODE.SCALE,
|
||||||
value: item
|
zoomPercent: normalizeZoom(this.settings.zoomPercent),
|
||||||
};
|
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: {
|
||||||
@@ -233,137 +266,221 @@ 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.isOpen = false;
|
this.$refs.popover.hide();
|
||||||
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 class="vm-console">
|
<div
|
||||||
|
ref="consoleContainer"
|
||||||
|
class="vm-console"
|
||||||
|
>
|
||||||
<div class="combination-keys">
|
<div class="combination-keys">
|
||||||
<v-dropdown
|
<div class="toolbar-group">
|
||||||
ref="popover"
|
<v-dropdown
|
||||||
placement="top"
|
ref="popover"
|
||||||
trigger="click"
|
placement="top"
|
||||||
:container="false"
|
trigger="click"
|
||||||
@auto-hide="keysRecord = []"
|
:container="false"
|
||||||
>
|
@auto-hide="keysRecord = []"
|
||||||
<button class="btn btn-sm bg-primary">
|
>
|
||||||
{{ t("harvester.virtualMachine.detail.console.shortcutKeys") }}
|
<button
|
||||||
</button>
|
class="btn btn-sm bg-primary"
|
||||||
|
: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"
|
|
||||||
>
|
>
|
||||||
<button
|
{{ t("harvester.virtualMachine.detail.console.shortcutKeys") }}
|
||||||
class="btn btn-sm bg-primary"
|
</button>
|
||||||
@click="sendCustomKeys(keys.value)"
|
|
||||||
>
|
|
||||||
{{ keys.label }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</v-dropdown>
|
|
||||||
|
|
||||||
<NovncConsoleCustomKeys
|
<template #popper>
|
||||||
v-if="renderKeysModal"
|
<novnc-console-item
|
||||||
:current-user="currentUser"
|
:items="keymap"
|
||||||
@close="hideKeysModal"
|
: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>
|
||||||
</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") }}
|
||||||
@@ -374,14 +491,56 @@ export default {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.vm-console {
|
.vm-console {
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 30px auto;
|
grid-template-rows: 32px minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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">
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ import MappingColumn from './MappingColumn.vue';
|
|||||||
import StorageDefaultsModal from './StorageDefaultsModal.vue';
|
import StorageDefaultsModal from './StorageDefaultsModal.vue';
|
||||||
|
|
||||||
const DEFAULT_VOLUME_MODE = VOLUME_MODE.FILE_SYSTEM;
|
const DEFAULT_VOLUME_MODE = VOLUME_MODE.FILE_SYSTEM;
|
||||||
const DEFAULT_ACCESS_MODES = [ACCESS_MODE.READ_WRITE_MANY];
|
const DEFAULT_ACCESS_MODE = ACCESS_MODE?.READ_WRITE_MANY ?? 'ReadWriteMany';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
providerName: { type: String, default: '' },
|
providerName: { type: String, default: '' },
|
||||||
@@ -268,9 +268,9 @@ const buildStorageEntries = () => {
|
|||||||
capacity: 0,
|
capacity: 0,
|
||||||
target: '',
|
target: '',
|
||||||
volumeMode: DEFAULT_VOLUME_MODE,
|
volumeMode: DEFAULT_VOLUME_MODE,
|
||||||
accessModes: [...DEFAULT_ACCESS_MODES],
|
accessModes: [DEFAULT_ACCESS_MODE],
|
||||||
inheritedVolumeMode: DEFAULT_VOLUME_MODE,
|
inheritedVolumeMode: DEFAULT_VOLUME_MODE,
|
||||||
inheritedAccessModes: [...DEFAULT_ACCESS_MODES],
|
inheritedAccessModes: [DEFAULT_ACCESS_MODE],
|
||||||
inheritedFromProvider: false,
|
inheritedFromProvider: false,
|
||||||
overridden: false,
|
overridden: false,
|
||||||
usedBy: [],
|
usedBy: [],
|
||||||
@@ -310,9 +310,9 @@ const buildStorageEntriesFromProvider = (datastoresData) => {
|
|||||||
capacity: ds.capacity || 0,
|
capacity: ds.capacity || 0,
|
||||||
target: '',
|
target: '',
|
||||||
volumeMode: DEFAULT_VOLUME_MODE,
|
volumeMode: DEFAULT_VOLUME_MODE,
|
||||||
accessModes: [...DEFAULT_ACCESS_MODES],
|
accessModes: [DEFAULT_ACCESS_MODE],
|
||||||
inheritedVolumeMode: DEFAULT_VOLUME_MODE,
|
inheritedVolumeMode: DEFAULT_VOLUME_MODE,
|
||||||
inheritedAccessModes: [...DEFAULT_ACCESS_MODES],
|
inheritedAccessModes: [DEFAULT_ACCESS_MODE],
|
||||||
inheritedFromProvider: false,
|
inheritedFromProvider: false,
|
||||||
overridden: false,
|
overridden: false,
|
||||||
usedBy: [],
|
usedBy: [],
|
||||||
|
|||||||
@@ -8,16 +8,22 @@ import { useI18n } from '@shell/composables/useI18n';
|
|||||||
import { VOLUME_MODE, ACCESS_MODE } from '../../config/types';
|
import { VOLUME_MODE, ACCESS_MODE } from '../../config/types';
|
||||||
|
|
||||||
const VOLUME_MODE_OPTIONS = [VOLUME_MODE.FILE_SYSTEM, VOLUME_MODE.BLOCK];
|
const VOLUME_MODE_OPTIONS = [VOLUME_MODE.FILE_SYSTEM, VOLUME_MODE.BLOCK];
|
||||||
const ACCESS_MODE_OPTIONS = [ACCESS_MODE.READ_WRITE_ONCE, ACCESS_MODE.READ_WRITE_MANY, ACCESS_MODE.READ_ONLY_MANY];
|
const {
|
||||||
|
READ_WRITE_ONCE = 'ReadWriteOnce',
|
||||||
|
READ_ONLY_MANY = 'ReadOnlyMany',
|
||||||
|
READ_WRITE_MANY = 'ReadWriteMany',
|
||||||
|
READ_WRITE_ONCE_POD = 'ReadWriteOncePod'
|
||||||
|
} = ACCESS_MODE || {};
|
||||||
|
const ACCESS_MODE_OPTIONS = [READ_WRITE_ONCE, READ_WRITE_MANY, READ_ONLY_MANY, READ_WRITE_ONCE_POD];
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
storageClassName: { type: String, default: '' },
|
storageClassName: { type: String, default: '' },
|
||||||
providerName: { type: String, default: '' },
|
providerName: { type: String, default: '' },
|
||||||
showInherited: { type: Boolean, default: false },
|
showInherited: { type: Boolean, default: false },
|
||||||
volumeMode: { type: String, default: VOLUME_MODE.FILE_SYSTEM },
|
volumeMode: { type: String, default: VOLUME_MODE.FILE_SYSTEM },
|
||||||
accessModes: { type: Array, default: () => [ACCESS_MODE.READ_WRITE_MANY] },
|
accessModes: { type: Array, default: () => ['ReadWriteMany'] },
|
||||||
inheritedVolumeMode: { type: String, default: VOLUME_MODE.FILE_SYSTEM },
|
inheritedVolumeMode: { type: String, default: VOLUME_MODE.FILE_SYSTEM },
|
||||||
inheritedAccessModes: { type: Array, default: () => [ACCESS_MODE.READ_WRITE_MANY] },
|
inheritedAccessModes: { type: Array, default: () => ['ReadWriteMany'] },
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['apply', 'close']);
|
const emit = defineEmits(['apply', 'close']);
|
||||||
@@ -26,7 +32,7 @@ const store = useStore();
|
|||||||
const { t } = useI18n(store);
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
const localVolumeMode = ref(props.volumeMode || VOLUME_MODE.FILE_SYSTEM);
|
const localVolumeMode = ref(props.volumeMode || VOLUME_MODE.FILE_SYSTEM);
|
||||||
const localAccessMode = ref(props.accessModes?.[0] || ACCESS_MODE.READ_WRITE_MANY);
|
const localAccessMode = ref(props.accessModes?.[0] || READ_WRITE_MANY);
|
||||||
|
|
||||||
const volumeModeOptions = VOLUME_MODE_OPTIONS.map((value) => ({ label: value, value }));
|
const volumeModeOptions = VOLUME_MODE_OPTIONS.map((value) => ({ label: value, value }));
|
||||||
const accessModeOptions = ACCESS_MODE_OPTIONS.map((value) => ({ label: value, value }));
|
const accessModeOptions = ACCESS_MODE_OPTIONS.map((value) => ({ label: value, value }));
|
||||||
@@ -42,7 +48,7 @@ const apply = () => {
|
|||||||
// Repopulate the dropdowns with the provider default values; the user then applies.
|
// Repopulate the dropdowns with the provider default values; the user then applies.
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
localVolumeMode.value = props.inheritedVolumeMode || VOLUME_MODE.FILE_SYSTEM;
|
localVolumeMode.value = props.inheritedVolumeMode || VOLUME_MODE.FILE_SYSTEM;
|
||||||
localAccessMode.value = props.inheritedAccessModes?.[0] || ACCESS_MODE.READ_WRITE_MANY;
|
localAccessMode.value = props.inheritedAccessModes?.[0] || READ_WRITE_MANY;
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
|
|||||||
@@ -111,15 +111,17 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { kind: 'number', featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
|
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { kind: 'number', featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
|
||||||
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: {
|
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: {
|
||||||
kind: 'boolean',
|
kind: 'boolean',
|
||||||
experimental: true,
|
preview: true,
|
||||||
featureFlag: 'longhornV2LVMSupport'
|
featureFlag: 'longhornV2LVMSupport'
|
||||||
},
|
},
|
||||||
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_HUGEPAGE_ENABLED]: {
|
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_HUGEPAGE_ENABLED]: {
|
||||||
kind: 'boolean',
|
kind: 'boolean',
|
||||||
|
preview: true,
|
||||||
featureFlag: 'longhornV2HugepageSettings'
|
featureFlag: 'longhornV2HugepageSettings'
|
||||||
},
|
},
|
||||||
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_MEMORY_SIZE]: {
|
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_MEMORY_SIZE]: {
|
||||||
kind: 'number',
|
kind: 'number',
|
||||||
|
preview: true,
|
||||||
featureFlag: 'longhornV2HugepageSettings'
|
featureFlag: 'longhornV2HugepageSettings'
|
||||||
},
|
},
|
||||||
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ export const VOLUME_MODE = {
|
|||||||
FILE_SYSTEM: 'Filesystem'
|
FILE_SYSTEM: 'Filesystem'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ACCESS_MODE = {
|
||||||
|
READ_WRITE_MANY: 'ReadWriteMany',
|
||||||
|
READ_WRITE_ONCE: 'ReadWriteOnce',
|
||||||
|
READ_ONLY_MANY: 'ReadOnlyMany',
|
||||||
|
READ_WRITE_ONCE_POD: 'ReadWriteOncePod',
|
||||||
|
};
|
||||||
|
|
||||||
export const NETWORK_PROTOCOL = {
|
export const NETWORK_PROTOCOL = {
|
||||||
IPv4: 'IPv4',
|
IPv4: 'IPv4',
|
||||||
IPv6: 'IPv6',
|
IPv6: 'IPv6',
|
||||||
|
|||||||
@@ -390,14 +390,17 @@ export default {
|
|||||||
:vmi-resource="vmi"
|
:vmi-resource="vmi"
|
||||||
:vmim-resource="vmim"
|
:vmim-resource="vmim"
|
||||||
/>
|
/>
|
||||||
<DashboardMetrics
|
<div
|
||||||
v-if="showVmMetrics && liveMigrationProgressEnabled"
|
v-if="showVmMetrics && liveMigrationProgressEnabled"
|
||||||
:detail-url="VM_MIGRATION_DETAIL_URL"
|
class="migration-metrics-scroll mb-30"
|
||||||
graph-height="640px"
|
>
|
||||||
:has-summary-and-detail="false"
|
<DashboardMetrics
|
||||||
:vars="graphVars"
|
:detail-url="VM_MIGRATION_DETAIL_URL"
|
||||||
class="mb-30"
|
graph-height="960px"
|
||||||
/>
|
:has-summary-and-detail="false"
|
||||||
|
:vars="graphVars"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Events
|
<Events
|
||||||
v-if="liveMigrationProgressEnabled"
|
v-if="liveMigrationProgressEnabled"
|
||||||
:resource="vmi"
|
:resource="vmi"
|
||||||
@@ -436,3 +439,10 @@ export default {
|
|||||||
</Tabbed>
|
</Tabbed>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.migration-metrics-scroll {
|
||||||
|
max-height: 640px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, ACCESS_MODE } from '@pkg/harvester/config/types';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import ArrayList from '@shell/components/form/ArrayList';
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
@@ -10,6 +10,10 @@ import { HCI_SETTING } from '../../config/settings';
|
|||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
||||||
|
|
||||||
|
const {
|
||||||
|
READ_WRITE_ONCE, READ_ONLY_MANY, READ_WRITE_MANY, READ_WRITE_ONCE_POD
|
||||||
|
} = ACCESS_MODE;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CDISettings',
|
name: 'CDISettings',
|
||||||
|
|
||||||
@@ -90,10 +94,10 @@ export default {
|
|||||||
|
|
||||||
accessModeOptions() {
|
accessModeOptions() {
|
||||||
return [
|
return [
|
||||||
{ label: 'ReadWriteOnce', value: 'ReadWriteOnce' },
|
{ label: READ_WRITE_ONCE, value: READ_WRITE_ONCE },
|
||||||
{ label: 'ReadOnlyMany', value: 'ReadOnlyMany' },
|
{ label: READ_ONLY_MANY, value: READ_ONLY_MANY },
|
||||||
{ label: 'ReadWriteMany', value: 'ReadWriteMany' },
|
{ label: READ_WRITE_MANY, value: READ_WRITE_MANY },
|
||||||
{ label: 'ReadWriteOncePod', value: 'ReadWriteOncePod' },
|
{ label: READ_WRITE_ONCE_POD, value: READ_WRITE_ONCE_POD },
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,11 @@ import { HCI, VOLUME_SNAPSHOT } from '../types';
|
|||||||
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
||||||
import { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim';
|
import { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim';
|
||||||
import { GIBIBYTE } from '../utils/unit';
|
import { GIBIBYTE } from '../utils/unit';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, ACCESS_MODE } from '@pkg/harvester/config/types';
|
||||||
import { isInternalStorageClass } from '../utils/storage-class';
|
import { isInternalStorageClass } from '../utils/storage-class';
|
||||||
|
|
||||||
|
const { READ_WRITE_MANY, READ_WRITE_ONCE, READ_ONLY_MANY } = ACCESS_MODE;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterVolume',
|
name: 'HarvesterVolume',
|
||||||
|
|
||||||
@@ -83,7 +85,7 @@ export default {
|
|||||||
if (this.mode === _CREATE) {
|
if (this.mode === _CREATE) {
|
||||||
// default volumeMode to Block
|
// default volumeMode to Block
|
||||||
this.value.spec.volumeMode = VOLUME_MODE.BLOCK;
|
this.value.spec.volumeMode = VOLUME_MODE.BLOCK;
|
||||||
this.value.spec.accessModes = ['ReadWriteMany'];
|
this.value.spec.accessModes = [READ_WRITE_MANY];
|
||||||
}
|
}
|
||||||
|
|
||||||
const storage = this.value?.spec?.resources?.requests?.storage || null;
|
const storage = this.value?.spec?.resources?.requests?.storage || null;
|
||||||
@@ -160,7 +162,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
accessModeOptions() {
|
accessModeOptions() {
|
||||||
return ['ReadWriteOnce', 'ReadWriteMany', 'ReadOnlyMany'];
|
return [READ_WRITE_ONCE, READ_WRITE_MANY, READ_ONLY_MANY];
|
||||||
},
|
},
|
||||||
|
|
||||||
imageOption() {
|
imageOption() {
|
||||||
@@ -355,7 +357,7 @@ export default {
|
|||||||
|
|
||||||
getAccessMode() {
|
getAccessMode() {
|
||||||
if (!this.longhornV2LVMSupport) {
|
if (!this.longhornV2LVMSupport) {
|
||||||
return ['ReadWriteMany'];
|
return [READ_WRITE_MANY];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.value?.spec?.accessModes && this.value?.spec?.accessModes?.length > 0) {
|
if (this.value?.spec?.accessModes && this.value?.spec?.accessModes?.length > 0) {
|
||||||
@@ -371,7 +373,7 @@ export default {
|
|||||||
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.value.thirdPartyStorageFeatureEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2);
|
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.value.thirdPartyStorageFeatureEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2);
|
||||||
}
|
}
|
||||||
|
|
||||||
return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany'];
|
return readWriteOnce ? [READ_WRITE_ONCE] : [READ_WRITE_MANY];
|
||||||
},
|
},
|
||||||
buildDataVolumeObj() {
|
buildDataVolumeObj() {
|
||||||
const storage = {
|
const storage = {
|
||||||
|
|||||||
@@ -246,4 +246,27 @@ export default {
|
|||||||
.group-actions {
|
.group-actions {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Match the enable/disable passthrough bulk-action buttons height to the .btn (40px) on the right.
|
||||||
|
:deep(.bulk .rc-button.btn-medium.bulk-action:not(.btn-sm)) {
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the collapsed "Actions" dropdown button 40px too.
|
||||||
|
:deep(.bulk .rc-button.btn-medium.bulk-actions-dropdown:not(.btn-sm)) {
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lay the bulk row out with flex so the "N selected" label can wrap below the buttons.
|
||||||
|
:deep(.bulk) {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.bulk .action-availability) {
|
||||||
|
flex-basis: 100%;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|||||||
import Banner from '@components/Banner/Banner.vue';
|
import Banner from '@components/Banner/Banner.vue';
|
||||||
import remove from 'lodash/remove';
|
import remove from 'lodash/remove';
|
||||||
import { set } from '@shell/utils/object';
|
import { set } from '@shell/utils/object';
|
||||||
|
import { uniq } from '@shell/utils/array';
|
||||||
import { HCI } from '../../../types';
|
import { HCI } from '../../../types';
|
||||||
import DeviceList from './DeviceList';
|
import DeviceList from './DeviceList';
|
||||||
import CompatibilityMatrix from '../CompatibilityMatrix';
|
import CompatibilityMatrix from '../CompatibilityMatrix';
|
||||||
@@ -60,7 +61,15 @@ export default {
|
|||||||
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
|
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
vmDevices.forEach(({ name }) => {
|
// When off, read from spec; otherwise the spec name is a placeholder ('provisioned'),
|
||||||
|
// so read the real allocated device names from the deviceAllocationDetails annotation.
|
||||||
|
const hostDeviceNames = this.vm.isOff ? [
|
||||||
|
...vmDevices.map(({ name }) => name),
|
||||||
|
] : [
|
||||||
|
...Object.values(this.vm?.provisionedHostDevices || {}).reduce((acc, devices) => [...acc, ...devices], []),
|
||||||
|
];
|
||||||
|
|
||||||
|
uniq(hostDeviceNames).forEach((name) => {
|
||||||
if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
|
if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
|
||||||
selectedDevices.push(name);
|
selectedDevices.push(name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,12 @@ import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
|
|||||||
import { VOLUME_HOTPLUG_ACTION, SOURCE_TYPE } from '../../../config/harvester-map';
|
import { VOLUME_HOTPLUG_ACTION, SOURCE_TYPE } from '../../../config/harvester-map';
|
||||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
||||||
import { HCI } from '../../../types';
|
import { HCI } from '../../../types';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, ACCESS_MODE } from '@pkg/harvester/config/types';
|
||||||
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
|
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
|
||||||
import { EMPTY_IMAGE } from '../../../utils/vm';
|
import { EMPTY_IMAGE } from '../../../utils/vm';
|
||||||
|
|
||||||
|
const { READ_WRITE_MANY } = ACCESS_MODE;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['update:value'],
|
emits: ['update:value'],
|
||||||
|
|
||||||
@@ -69,7 +71,7 @@ export default {
|
|||||||
|
|
||||||
customAccessMode: {
|
customAccessMode: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'ReadWriteMany'
|
default: READ_WRITE_MANY
|
||||||
},
|
},
|
||||||
|
|
||||||
resourceType: {
|
resourceType: {
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storagec
|
|||||||
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
|
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
|
||||||
import { GIBIBYTE } from '../../../../utils/unit';
|
import { GIBIBYTE } from '../../../../utils/unit';
|
||||||
import { isInternalStorageClass } from '../../../../utils/storage-class';
|
import { isInternalStorageClass } from '../../../../utils/storage-class';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, ACCESS_MODE } from '@pkg/harvester/config/types';
|
||||||
|
|
||||||
|
const { READ_WRITE_MANY, READ_WRITE_ONCE } = ACCESS_MODE;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditVolume',
|
name: 'HarvesterEditVolume',
|
||||||
@@ -205,7 +207,7 @@ export default {
|
|||||||
|
|
||||||
getAccessMode(storageClassName) {
|
getAccessMode(storageClassName) {
|
||||||
if (!this.longhornV2LVMSupport) {
|
if (!this.longhornV2LVMSupport) {
|
||||||
return 'ReadWriteMany';
|
return READ_WRITE_MANY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
||||||
@@ -216,7 +218,7 @@ export default {
|
|||||||
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.thirdPartyStorageClassEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2);
|
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.thirdPartyStorageClassEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2);
|
||||||
}
|
}
|
||||||
|
|
||||||
return readWriteOnce ? 'ReadWriteOnce' : 'ReadWriteMany';
|
return readWriteOnce ? READ_WRITE_ONCE : READ_WRITE_MANY;
|
||||||
},
|
},
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
|
|||||||
@@ -953,15 +953,33 @@ 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
|
||||||
customShortcutKeys: Custom Shortcut Keys
|
settings:
|
||||||
management: Management Shortcut Keys
|
label: Settings
|
||||||
record:
|
viewMode:
|
||||||
start: Record
|
label: Display
|
||||||
recording: Recording
|
scale: Scale the display to fit the window
|
||||||
stop: Stop Recording
|
resize: Resize the virtual machine display to the window
|
||||||
tips: Press the record button to capture your keyboard inputs.
|
none: Manual zoom
|
||||||
send: Send
|
zoom: Zoom
|
||||||
preferredKeys: Preferred Custom Shortcut Keys
|
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.
|
||||||
terminationGracePeriodSeconds:
|
terminationGracePeriodSeconds:
|
||||||
label: Termination Grace Period
|
label: Termination Grace Period
|
||||||
affinity:
|
affinity:
|
||||||
@@ -2417,7 +2435,15 @@ typeDescription:
|
|||||||
harvester: "Branding allows administrators to globally re-brand the UI by customizing the Harvester product name, logos and color scheme."
|
harvester: "Branding allows administrators to globally re-brand the UI by customizing the Harvester product name, logos and color scheme."
|
||||||
|
|
||||||
advancedSettings:
|
advancedSettings:
|
||||||
experimental: 'Experimental features allow users to test and evaluate early-access functionality prior to official supported releases'
|
tags:
|
||||||
|
modified:
|
||||||
|
label: Modified
|
||||||
|
experimental:
|
||||||
|
label: Experimental
|
||||||
|
description: 'Experimental: The feature is incomplete but its essential functionality is operational. The feature is not recommended for use in production environments.'
|
||||||
|
preview:
|
||||||
|
label: Technical Preview
|
||||||
|
description: 'Technical Preview: The feature is almost complete and its functionality is not expected to change significantly. Explore the feature extensively before using in production environments.'
|
||||||
descriptions:
|
descriptions:
|
||||||
'harv-vlan': Default Network Interface name of the VLAN network.
|
'harv-vlan': Default Network Interface name of the VLAN network.
|
||||||
'harv-backup-target': Custom backup target to store virtual machine backups.
|
'harv-backup-target': Custom backup target to store virtual machine backups.
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ import { HCI } from '../../types';
|
|||||||
import { parseVolumeClaimTemplates, EMPTY_IMAGE } from '../../utils/vm';
|
import { parseVolumeClaimTemplates, EMPTY_IMAGE } from '../../utils/vm';
|
||||||
import impl, { QGA_JSON, USB_TABLET } from './impl';
|
import impl, { QGA_JSON, USB_TABLET } from './impl';
|
||||||
import { GIBIBYTE } from '../../utils/unit';
|
import { GIBIBYTE } from '../../utils/unit';
|
||||||
import { VOLUME_MODE, FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, ACCESS_MODE, FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
|
||||||
|
|
||||||
|
const { READ_WRITE_MANY } = ACCESS_MODE;
|
||||||
|
|
||||||
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
||||||
|
|
||||||
@@ -276,7 +278,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
customAccessMode() {
|
customAccessMode() {
|
||||||
return this.storageClassSetting.accessModes || 'ReadWriteMany';
|
return this.storageClassSetting.accessModes || READ_WRITE_MANY;
|
||||||
},
|
},
|
||||||
|
|
||||||
isWindows() {
|
isWindows() {
|
||||||
@@ -504,7 +506,7 @@ export default {
|
|||||||
id: randomStr(5),
|
id: randomStr(5),
|
||||||
source: SOURCE_TYPE.IMAGE,
|
source: SOURCE_TYPE.IMAGE,
|
||||||
name: 'disk-0',
|
name: 'disk-0',
|
||||||
accessMode: 'ReadWriteMany', // root disk only support LHv1 volume, should be RWX
|
accessMode: READ_WRITE_MANY, // root disk only support LHv1 volume, should be RWX
|
||||||
bus,
|
bus,
|
||||||
volumeName: '',
|
volumeName: '',
|
||||||
size,
|
size,
|
||||||
@@ -570,7 +572,7 @@ export default {
|
|||||||
const pvcResource = allPVCs.find( (O) => O.id === `${ namespace }/${ volume?.persistentVolumeClaim?.claimName }`);
|
const pvcResource = allPVCs.find( (O) => O.id === `${ namespace }/${ volume?.persistentVolumeClaim?.claimName }`);
|
||||||
|
|
||||||
source = SOURCE_TYPE.ATTACH_VOLUME;
|
source = SOURCE_TYPE.ATTACH_VOLUME;
|
||||||
accessMode = pvcResource?.spec?.accessModes?.[0] || 'ReadWriteMany';
|
accessMode = pvcResource?.spec?.accessModes?.[0] || READ_WRITE_MANY;
|
||||||
size = pvcResource?.spec?.resources?.requests?.storage || '10Gi';
|
size = pvcResource?.spec?.resources?.requests?.storage || '10Gi';
|
||||||
storageClassName = pvcResource?.spec?.storageClassName;
|
storageClassName = pvcResource?.spec?.storageClassName;
|
||||||
volumeMode = pvcResource?.spec?.volumeMode || VOLUME_MODE.BLOCK;
|
volumeMode = pvcResource?.spec?.volumeMode || VOLUME_MODE.BLOCK;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import SYSTEM_NAMESPACES from '@shell/config/system-namespaces';
|
|||||||
import { get } from '@shell/utils/object';
|
import { get } from '@shell/utils/object';
|
||||||
import { NAMESPACE } from '@shell/config/types';
|
import { NAMESPACE } from '@shell/config/types';
|
||||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
|
||||||
|
import { FORKLIFT_NAMESPACE } from '@pkg/harvester/config/harvester-map';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
|
|
||||||
const OBSCURE_NAMESPACE_PREFIX = [
|
const OBSCURE_NAMESPACE_PREFIX = [
|
||||||
@@ -93,6 +94,10 @@ export default class HciNamespace extends namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isSystem() {
|
get isSystem() {
|
||||||
|
if (this.metadata?.name === FORKLIFT_NAMESPACE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const systemNamespaces = ['fleet-default'];
|
const systemNamespaces = ['fleet-default'];
|
||||||
|
|
||||||
if (systemNamespaces.includes(this.metadata.name)) {
|
if (systemNamespaces.includes(this.metadata.name)) {
|
||||||
|
|||||||
@@ -1315,6 +1315,18 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get provisionedHostDevices() {
|
||||||
|
// Rancher-provisioned VMs store hostDevices.name as a placeholder ('provisioned'); the
|
||||||
|
// real allocated PCI device names live in this annotation, keyed by resourceName.
|
||||||
|
try {
|
||||||
|
const deviceAllocationDetails = JSON.parse(this.metadata?.annotations[HCI_ANNOTATIONS.VM_DEVICE_ALLOCATION_DETAILS] || '{}');
|
||||||
|
|
||||||
|
return deviceAllocationDetails?.hostdevices || {};
|
||||||
|
} catch (error) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get schedulingVMBackupFeatureEnabled() {
|
get schedulingVMBackupFeatureEnabled() {
|
||||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('schedulingVMBackup');
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('schedulingVMBackup');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester",
|
"name": "harvester",
|
||||||
"description": "Rancher UI Extension for Harvester",
|
"description": "Rancher UI Extension for Harvester",
|
||||||
"version": "1.9.0-rc5",
|
"version": "1.10.0-dev",
|
||||||
"private": false,
|
"private": false,
|
||||||
"rancher": {
|
"rancher": {
|
||||||
"annotations": {
|
"annotations": {
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
<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: { NovncConsoleWrapper, Loading },
|
components: {
|
||||||
|
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 });
|
||||||
@@ -28,17 +36,21 @@ 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 +1,24 @@
|
|||||||
module.exports = require('@rancher/shell/pkg/vue.config')(__dirname);
|
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 } };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
+21
-1
@@ -1,6 +1,26 @@
|
|||||||
|
const path = require('path');
|
||||||
const config = require('@rancher/shell/vue.config');
|
const config = require('@rancher/shell/vue.config');
|
||||||
|
|
||||||
module.exports = config(__dirname, {
|
const baseConfig = 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 } };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user