mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-06 22:26:05 +08:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f37a23845 | ||
|
|
8e93404849 | ||
|
|
00584f1d19 | ||
|
|
4fa6470343 | ||
|
|
d550046189 | ||
|
|
5f32a9ade5 | ||
|
|
3f39d561be | ||
|
|
7c84269ed6 | ||
|
|
f63be73b11 | ||
|
|
fabc1cec21 | ||
|
|
ed2b6f2551 | ||
|
|
72282ae50e | ||
|
|
8fba4e7050 | ||
|
|
fbd4a1108f | ||
|
|
823a88c85b | ||
|
|
3a2c6e3a3d | ||
|
|
be6e9b5f06 | ||
|
|
a88bed9eb4 | ||
|
|
f97f14c811 | ||
|
|
f8a29a1c54 |
@@ -12,4 +12,15 @@ runs:
|
|||||||
|
|
||||||
- name: Install packages
|
- name: Install packages
|
||||||
shell: bash
|
shell: bash
|
||||||
run: yarn install
|
run: |
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
if yarn install --frozen-lockfile; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$attempt" -lt 3 ]; then
|
||||||
|
sleep 10
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
+13
-5
@@ -7,7 +7,7 @@
|
|||||||
},
|
},
|
||||||
"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.3",
|
"@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",
|
||||||
@@ -15,6 +15,12 @@
|
|||||||
"cache-loader": "^4.1.0",
|
"cache-loader": "^4.1.0",
|
||||||
"color": "4.2.3",
|
"color": "4.2.3",
|
||||||
"ip": "2.0.1",
|
"ip": "2.0.1",
|
||||||
|
"@xterm/xterm": "6.0.0",
|
||||||
|
"@xterm/addon-fit": "0.11.0",
|
||||||
|
"@xterm/addon-search": "0.16.0",
|
||||||
|
"@xterm/addon-web-links": "0.12.0",
|
||||||
|
"@xterm/addon-webgl": "0.19.0",
|
||||||
|
"@xterm/addon-canvas": "0.7.0",
|
||||||
"node-polyfill-webpack-plugin": "^3.0.0",
|
"node-polyfill-webpack-plugin": "^3.0.0",
|
||||||
"elkjs": "^0.11.0",
|
"elkjs": "^0.11.0",
|
||||||
"vue-draggable-next": "^2.2.1",
|
"vue-draggable-next": "^2.2.1",
|
||||||
@@ -29,7 +35,7 @@
|
|||||||
"glob": "7.2.3",
|
"glob": "7.2.3",
|
||||||
"glob-parent": "6.0.2",
|
"glob-parent": "6.0.2",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"@types/lodash": "4.17.24",
|
"@types/lodash": "4.17.25",
|
||||||
"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",
|
||||||
@@ -42,15 +48,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",
|
||||||
|
|||||||
@@ -271,4 +271,9 @@ export default {
|
|||||||
.filter-label .v-popper__arrow-container {
|
.filter-label .v-popper__arrow-container {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fixed-header-actions .middle .filter + .btn-group {
|
||||||
|
display: inline-flex;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -116,6 +116,11 @@ export default {
|
|||||||
.vm-schedule-dropdown .v-popper__arrow-container {
|
.vm-schedule-dropdown .v-popper__arrow-container {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fixed-header-actions .middle .vm-schedule-filter + .btn-group {
|
||||||
|
display: inline-flex;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
import Socket, {
|
import Socket, {
|
||||||
EVENT_CONNECTED,
|
EVENT_CONNECTED,
|
||||||
EVENT_CONNECTING,
|
EVENT_CONNECTING,
|
||||||
@@ -22,74 +21,98 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
socket: null,
|
socket: null,
|
||||||
terminal: null,
|
terminal: null,
|
||||||
fitAddon: null,
|
textDecoder: null,
|
||||||
searchAddon: null,
|
fitAddon: null,
|
||||||
webglAddon: null,
|
searchAddon: null,
|
||||||
isOpen: false,
|
webglAddon: null,
|
||||||
isOpening: false,
|
onResize: null,
|
||||||
backlog: [],
|
isOpen: false,
|
||||||
firstTime: true,
|
isOpening: false,
|
||||||
queue: []
|
backlog: [],
|
||||||
|
firstTime: true,
|
||||||
|
queue: [],
|
||||||
|
isDraining: false,
|
||||||
|
drainScheduled: false,
|
||||||
|
drainTimer: null,
|
||||||
|
showSearch: false,
|
||||||
|
searchQuery: '',
|
||||||
|
searchOptions: {
|
||||||
|
caseSensitive: false,
|
||||||
|
regex: false,
|
||||||
|
wholeWord: false,
|
||||||
|
incremental: true,
|
||||||
|
decorations: {
|
||||||
|
matchBackground: '#B45309',
|
||||||
|
matchBorder: '#FDBA74',
|
||||||
|
matchOverviewRuler: '#FDBA74',
|
||||||
|
activeMatchBackground: '#EA580C',
|
||||||
|
activeMatchBorder: '#FED7AA',
|
||||||
|
activeMatchColorOverviewRuler: '#FED7AA',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
serialConsoleUrl() {
|
||||||
|
return this.value?.getSerialConsolePath || '';
|
||||||
|
},
|
||||||
|
|
||||||
xtermConfig() {
|
xtermConfig() {
|
||||||
return {
|
return {
|
||||||
allowProposedApi: true,
|
allowProposedApi: true,
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
|
cursorStyle: 'bar',
|
||||||
|
cursorWidth: 2,
|
||||||
|
scrollback: 5000,
|
||||||
useStyle: true,
|
useStyle: true,
|
||||||
fontSize: 12,
|
fontFamily: 'JetBrains Mono, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||||
|
fontSize: 16,
|
||||||
|
lineHeight: 1.3,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
|
||||||
queue: {
|
|
||||||
handler: debounce(async function(neu) {
|
|
||||||
if (neu.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const msg = await Promise.all(neu);
|
|
||||||
|
|
||||||
(msg || []).forEach((m) => {
|
|
||||||
this.terminal.write(m);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.queue = [];
|
|
||||||
}, 10),
|
|
||||||
deep: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.close();
|
this.close();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
serialConsoleUrl() {
|
||||||
|
if (this.terminal) {
|
||||||
|
this.connect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
this.textDecoder = new TextDecoder('utf-8');
|
||||||
await this.setupTerminal();
|
await this.setupTerminal();
|
||||||
await this.connect();
|
await this.connect();
|
||||||
|
this.onResize = debounce(() => this.fit(), 100);
|
||||||
|
window.addEventListener('resize', this.onResize);
|
||||||
|
this.scheduleFit();
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async setupTerminal() {
|
async setupTerminal() {
|
||||||
const docStyle = getComputedStyle(document.querySelector('body'));
|
const docStyle = getComputedStyle(document.querySelector('body'));
|
||||||
const xterm = await import(/* webpackChunkName: "xterm" */ 'xterm');
|
const xterm = await import(/* webpackChunkName: "xterm" */ '@xterm/xterm');
|
||||||
|
|
||||||
const addons = await allHash({
|
const addons = await allHash({
|
||||||
fit: import(/* webpackChunkName: "xterm" */ 'xterm-addon-fit'),
|
fit: import(/* webpackChunkName: "xterm" */ '@xterm/addon-fit'),
|
||||||
webgl: import(/* webpackChunkName: "xterm" */ 'xterm-addon-webgl'),
|
webgl: import(/* webpackChunkName: "xterm" */ '@xterm/addon-webgl'),
|
||||||
weblinks: import(/* webpackChunkName: "xterm" */ 'xterm-addon-web-links'),
|
weblinks: import(/* webpackChunkName: "xterm" */ '@xterm/addon-web-links'),
|
||||||
search: import(/* webpackChunkName: "xterm" */ 'xterm-addon-search'),
|
search: import(/* webpackChunkName: "xterm" */ '@xterm/addon-search'),
|
||||||
|
canvas: import(/* webpackChunkName: "xterm" */ '@xterm/addon-canvas'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const terminal = new xterm.Terminal({
|
const terminal = new xterm.Terminal({
|
||||||
theme: {
|
theme: {
|
||||||
background: docStyle.getPropertyValue('--terminal-bg').trim(),
|
background: docStyle.getPropertyValue('--terminal-bg').trim(),
|
||||||
cursor: docStyle.getPropertyValue('--terminal-cursor').trim(),
|
cursor: docStyle.getPropertyValue('--terminal-text').trim(),
|
||||||
foreground: docStyle.getPropertyValue('--terminal-text').trim()
|
foreground: docStyle.getPropertyValue('--terminal-text').trim()
|
||||||
},
|
},
|
||||||
...this.xtermConfig,
|
...this.xtermConfig,
|
||||||
@@ -97,25 +120,48 @@ export default {
|
|||||||
|
|
||||||
this.fitAddon = new addons.fit.FitAddon();
|
this.fitAddon = new addons.fit.FitAddon();
|
||||||
this.searchAddon = new addons.search.SearchAddon();
|
this.searchAddon = new addons.search.SearchAddon();
|
||||||
|
|
||||||
try {
|
|
||||||
this.webglAddon = new addons.webgl.WebGlAddon();
|
|
||||||
} catch (e) {
|
|
||||||
// Some browsers (Safari) don't support the webgl renderer, so don't use it.
|
|
||||||
this.webglAddon = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
terminal.loadAddon(this.fitAddon);
|
terminal.loadAddon(this.fitAddon);
|
||||||
terminal.loadAddon(this.searchAddon);
|
terminal.loadAddon(this.searchAddon);
|
||||||
terminal.loadAddon(new addons.weblinks.WebLinksAddon());
|
terminal.loadAddon(new addons.weblinks.WebLinksAddon());
|
||||||
terminal.open(this.$refs.xterm);
|
terminal.open(this.$refs.xterm);
|
||||||
|
|
||||||
if ( this.webglAddon ) {
|
// The webgl renderer can fail without throwing: the context may be lost
|
||||||
|
// mid-session, or it can attach but silently never paint, leaving the
|
||||||
|
// terminal's helper textarea at 0x0. Detect both cases and fall back to the
|
||||||
|
// canvas renderer so the terminal stays interactive.
|
||||||
|
try {
|
||||||
|
this.webglAddon = new addons.webgl.WebglAddon();
|
||||||
|
|
||||||
|
this.webglAddon.onContextLoss(() => {
|
||||||
|
if (!this.isUnmounted && this.terminal) {
|
||||||
|
this.fallbackToCanvas(terminal, addons);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
terminal.loadAddon(this.webglAddon);
|
terminal.loadAddon(this.webglAddon);
|
||||||
|
|
||||||
|
// Detect the silent "attaches but never paints" failure after a render frame.
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (this.isUnmounted || !this.terminal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = this.$refs.xterm?.querySelector('.xterm-helper-textarea');
|
||||||
|
const hasPainted = textarea && textarea.getBoundingClientRect().height > 0;
|
||||||
|
|
||||||
|
if (this.webglAddon && !hasPainted) {
|
||||||
|
this.fallbackToCanvas(terminal, addons);
|
||||||
|
this.fit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Some browsers (e.g. Safari) don't support the webgl renderer, so don't use it.
|
||||||
|
this.fallbackToCanvas(terminal, addons);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fit();
|
this.fit();
|
||||||
this.flush();
|
this.flush();
|
||||||
|
this.scheduleDrainQueue();
|
||||||
|
|
||||||
terminal.onData((input) => {
|
terminal.onData((input) => {
|
||||||
const msg = this.str2ab(input);
|
const msg = this.str2ab(input);
|
||||||
@@ -123,9 +169,132 @@ export default {
|
|||||||
this.write(msg);
|
this.write(msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
terminal.attachCustomKeyEventHandler((event) => {
|
||||||
|
const key = event.key?.toLowerCase();
|
||||||
|
|
||||||
|
if ((event.ctrlKey || event.metaKey) && key === 'f') {
|
||||||
|
event.preventDefault();
|
||||||
|
this.openSearch();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'escape' && this.showSearch) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.closeSearch();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
this.terminal = terminal;
|
this.terminal = terminal;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
openSearch() {
|
||||||
|
this.showSearch = true;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const input = this.$refs.searchInput;
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
input.focus();
|
||||||
|
input.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
closeSearch() {
|
||||||
|
this.showSearch = false;
|
||||||
|
this.searchQuery = '';
|
||||||
|
this.clearSearchHighlights();
|
||||||
|
this.terminal?.focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
clearSearchHighlights() {
|
||||||
|
this.searchAddon?.clearActiveDecoration();
|
||||||
|
this.searchAddon?.clearDecorations();
|
||||||
|
this.terminal?.clearSelection();
|
||||||
|
|
||||||
|
// Disposing the search decorations doesn't repaint the webgl/canvas
|
||||||
|
// layer, so force a refresh to remove the lingering highlight.
|
||||||
|
if (this.terminal) {
|
||||||
|
this.terminal.refresh(0, this.terminal.rows - 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
findNext() {
|
||||||
|
if (!this.searchAddon || !this.searchQuery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchAddon.findNext(this.searchQuery, this.searchOptions);
|
||||||
|
},
|
||||||
|
|
||||||
|
findPrevious() {
|
||||||
|
if (!this.searchAddon || !this.searchQuery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchAddon.findPrevious(this.searchQuery, this.searchOptions);
|
||||||
|
},
|
||||||
|
|
||||||
|
onSearchInput() {
|
||||||
|
if (!this.searchQuery) {
|
||||||
|
this.clearSearchHighlights();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.findNext();
|
||||||
|
},
|
||||||
|
|
||||||
|
onSearchKeydown(event) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (event.shiftKey) {
|
||||||
|
this.findPrevious();
|
||||||
|
} else {
|
||||||
|
this.findNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
this.closeSearch();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
scheduleFit() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.fit();
|
||||||
|
|
||||||
|
// Re-fit after layout settles to avoid undersized rows on first paint.
|
||||||
|
setTimeout(() => this.fit(), 120);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Dispose the webgl renderer (if any) and switch to the canvas renderer.
|
||||||
|
// Used when webgl fails to construct, loses its context, or silently never paints.
|
||||||
|
fallbackToCanvas(terminal, addons) {
|
||||||
|
if (this.isUnmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.webglAddon?.dispose();
|
||||||
|
this.webglAddon = null;
|
||||||
|
|
||||||
|
if (!this.canvasAddon) {
|
||||||
|
this.canvasAddon = new addons.canvas.CanvasAddon();
|
||||||
|
terminal.loadAddon(this.canvasAddon);
|
||||||
|
}
|
||||||
|
this.fit();
|
||||||
|
},
|
||||||
|
|
||||||
str2ab(str) {
|
str2ab(str) {
|
||||||
const enc = new TextEncoder();
|
const enc = new TextEncoder();
|
||||||
|
|
||||||
@@ -140,12 +309,131 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
enqueueMessage(messagePromise) {
|
||||||
|
this.queue.push(messagePromise);
|
||||||
|
this.scheduleDrainQueue();
|
||||||
|
},
|
||||||
|
|
||||||
|
scheduleDrainQueue() {
|
||||||
|
if (this.drainScheduled || this.isDraining || !this.terminal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drainScheduled = true;
|
||||||
|
|
||||||
|
const shouldDrainImmediately = this.queue.length <= 3;
|
||||||
|
|
||||||
|
const runDrain = () => {
|
||||||
|
this.drainScheduled = false;
|
||||||
|
this.drainTimer = null;
|
||||||
|
this.drainQueue();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shouldDrainImmediately) {
|
||||||
|
runDrain();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drainTimer = requestAnimationFrame(runDrain);
|
||||||
|
},
|
||||||
|
|
||||||
|
async decodeMessageData(data) {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.textDecoder) {
|
||||||
|
this.textDecoder = new TextDecoder('utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data instanceof Blob) {
|
||||||
|
const buffer = await data.arrayBuffer();
|
||||||
|
|
||||||
|
return this.textDecoder.decode(new Uint8Array(buffer), { stream: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data instanceof ArrayBuffer) {
|
||||||
|
return this.textDecoder.decode(new Uint8Array(data), { stream: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ArrayBuffer.isView(data)) {
|
||||||
|
const view = data;
|
||||||
|
const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
||||||
|
|
||||||
|
return this.textDecoder.decode(bytes, { stream: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(data || '');
|
||||||
|
},
|
||||||
|
|
||||||
|
async drainQueue() {
|
||||||
|
if (this.isDraining || !this.terminal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isDraining = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (this.queue.length > 0) {
|
||||||
|
const pendingMessages = this.queue.splice(0, this.queue.length);
|
||||||
|
const settledMessages = await Promise.allSettled(pendingMessages);
|
||||||
|
const messages = settledMessages
|
||||||
|
.filter((result) => result.status === 'fulfilled')
|
||||||
|
.map((result) => result.value);
|
||||||
|
const output = messages.filter(Boolean).join('');
|
||||||
|
|
||||||
|
if (output) {
|
||||||
|
await this.writeOutputInChunks(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.queue.length > 0) {
|
||||||
|
await this.nextFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.isDraining = false;
|
||||||
|
|
||||||
|
// If data arrived between the last while-check and releasing the lock,
|
||||||
|
// immediately schedule another drain so output never gets stuck.
|
||||||
|
if (this.queue.length > 0 && this.terminal) {
|
||||||
|
this.scheduleDrainQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async writeOutputInChunks(output) {
|
||||||
|
if (!output) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunkSize = output.length > 20000 ? 16000 : output.length > 8000 ? 8000 : output.length;
|
||||||
|
|
||||||
|
if (chunkSize === output.length) {
|
||||||
|
this.terminal.write(output);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < output.length; index += chunkSize) {
|
||||||
|
this.terminal.write(output.slice(index, index + chunkSize));
|
||||||
|
|
||||||
|
if (index + chunkSize < output.length) {
|
||||||
|
await this.nextFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
nextFrame() {
|
||||||
|
return new Promise((resolve) => requestAnimationFrame(resolve));
|
||||||
|
},
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.terminal.clear();
|
this.terminal.clear();
|
||||||
},
|
},
|
||||||
|
|
||||||
getSocketUrl() {
|
getSocketUrl() {
|
||||||
return `${ this.value?.getSerialConsolePath }`;
|
return this.serialConsoleUrl;
|
||||||
},
|
},
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
@@ -181,6 +469,7 @@ export default {
|
|||||||
this.fit();
|
this.fit();
|
||||||
this.flush();
|
this.flush();
|
||||||
}
|
}
|
||||||
|
this.scheduleFit();
|
||||||
|
|
||||||
if (this.firstTime) {
|
if (this.firstTime) {
|
||||||
this.socket.send(this.str2ab('\n'));
|
this.socket.send(this.str2ab('\n'));
|
||||||
@@ -195,7 +484,7 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.socket.addEventListener(EVENT_MESSAGE, (e) => {
|
this.socket.addEventListener(EVENT_MESSAGE, (e) => {
|
||||||
this.queue.push(e.detail.data.text());
|
this.enqueueMessage(this.decodeMessageData(e.detail.data));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.connect();
|
this.socket.connect();
|
||||||
@@ -218,22 +507,19 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.fitAddon.fit();
|
this.fitAddon.fit();
|
||||||
|
|
||||||
const { rows, cols } = this.fitAddon.proposeDimensions();
|
|
||||||
|
|
||||||
if ( !this.isOpen ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = JSON.stringify({
|
|
||||||
Width: cols,
|
|
||||||
Height: rows
|
|
||||||
});
|
|
||||||
|
|
||||||
this.socket.send(this.str2ab(message));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
|
if (this.onResize) {
|
||||||
|
window.removeEventListener('resize', this.onResize);
|
||||||
|
this.onResize = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.drainTimer) {
|
||||||
|
cancelAnimationFrame(this.drainTimer);
|
||||||
|
this.drainTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
if ( this.socket ) {
|
if ( this.socket ) {
|
||||||
this.socket.disconnect();
|
this.socket.disconnect();
|
||||||
}
|
}
|
||||||
@@ -248,6 +534,44 @@ export default {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="harvester-shell-container">
|
<div class="harvester-shell-container">
|
||||||
|
<div
|
||||||
|
v-if="showSearch"
|
||||||
|
class="shell-search"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref="searchInput"
|
||||||
|
v-model="searchQuery"
|
||||||
|
type="text"
|
||||||
|
class="shell-search-input"
|
||||||
|
placeholder="Search in terminal"
|
||||||
|
@input="onSearchInput"
|
||||||
|
@keydown="onSearchKeydown"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn role-secondary btn-sm shell-search-btn"
|
||||||
|
type="button"
|
||||||
|
title="Shift+Enter"
|
||||||
|
@click="findPrevious"
|
||||||
|
>
|
||||||
|
Prev
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn role-secondary btn-sm shell-search-btn"
|
||||||
|
type="button"
|
||||||
|
title="Enter"
|
||||||
|
@click="findNext"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn role-secondary btn-sm shell-search-btn"
|
||||||
|
type="button"
|
||||||
|
title="Esc"
|
||||||
|
@click="closeSearch"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
ref="xterm"
|
ref="xterm"
|
||||||
class="shell-body"
|
class="shell-body"
|
||||||
@@ -257,18 +581,73 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../../../node_modules/xterm/css/xterm.css';
|
@import '../../../../node_modules/@xterm/xterm/css/xterm.css';
|
||||||
|
|
||||||
body, #__nuxt, #__layout, MAIN {
|
body, #__nuxt, #__layout, MAIN {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.harvester-shell-container {
|
.harvester-shell-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.shell-body, .terminal.xterm {
|
.resize-observer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 5;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: color-mix(in srgb, var(--body-bg) 88%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-search-input {
|
||||||
|
min-width: 220px;
|
||||||
|
max-width: 420px;
|
||||||
|
flex: 0 1 320px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--body-bg);
|
||||||
|
color: var(--body-text);
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-search-btn {
|
||||||
|
min-width: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-body {
|
||||||
|
flex: 1 1 auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal.xterm,
|
||||||
|
.terminal.xterm .xterm-viewport,
|
||||||
|
.terminal.xterm .xterm-screen,
|
||||||
|
.terminal.xterm .xterm-scrollable-element {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal.xterm .xterm-scrollable-element {
|
||||||
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ export default {
|
|||||||
const host = window.location.host;
|
const host = window.location.host;
|
||||||
const prefix = window.location.pathname.replace(this.$route.path, '');
|
const prefix = window.location.pathname.replace(this.$route.path, '');
|
||||||
const params = this.$route?.params;
|
const params = this.$route?.params;
|
||||||
|
const popupWidth = Math.max(800, screen.width - 200);
|
||||||
|
const popupHeight = Math.max(600, screen.height - 200);
|
||||||
|
const popupLeft = Math.max(0, window.screenX + Math.floor((window.outerWidth - popupWidth) / 2));
|
||||||
|
const popupTop = Math.max(0, window.screenY + Math.floor((window.outerHeight - popupHeight) / 2));
|
||||||
|
|
||||||
const url = `https://${ host }${ prefix }/${ PRODUCT_NAME }/c/${ params.cluster }/console/${ uid }/${ type }`;
|
const url = `https://${ host }${ prefix }/${ PRODUCT_NAME }/c/${ params.cluster }/console/${ uid }/${ type }`;
|
||||||
|
|
||||||
@@ -63,7 +67,7 @@ export default {
|
|||||||
window.open(
|
window.open(
|
||||||
url,
|
url,
|
||||||
'_blank',
|
'_blank',
|
||||||
`toolbars=0,width=${ screen.width - 200 },height=${ screen.height - 200 },left=0,top=0,noreferrer`
|
`toolbars=0,width=${ popupWidth },height=${ popupHeight },left=${ popupLeft },top=${ popupTop },noreferrer`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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: [],
|
||||||
|
|||||||
@@ -273,19 +273,6 @@ const startMigrationAction = async() => {
|
|||||||
|
|
||||||
await plan.save();
|
await plan.save();
|
||||||
|
|
||||||
const planOwnerRef = {
|
|
||||||
apiVersion: FORKLIFT_API_VERSION,
|
|
||||||
kind: 'Plan',
|
|
||||||
name: plan.metadata.name,
|
|
||||||
uid: plan.metadata.uid,
|
|
||||||
blockOwnerDeletion: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
networkMap.metadata.ownerReferences = [planOwnerRef];
|
|
||||||
await networkMap.save();
|
|
||||||
storageMap.metadata.ownerReferences = [planOwnerRef];
|
|
||||||
await storageMap.save();
|
|
||||||
|
|
||||||
// Kick off the first migration through the model so the Migration payload
|
// Kick off the first migration through the model so the Migration payload
|
||||||
// lives in a single place (also reused by the dashboard start/restart action).
|
// lives in a single place (also reused by the dashboard start/restart action).
|
||||||
await plan.startMigration();
|
await plan.startMigration();
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Loading from '@shell/components/Loading';
|
|||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
import { NETWORK_PROTOCOL, NETWORK_TYPE } from '@pkg/harvester/config/types';
|
import { NETWORK_PROTOCOL, NETWORK_TYPE } from '@pkg/harvester/config/types';
|
||||||
import { set } from '@shell/utils/object';
|
import { set, remove } from '@shell/utils/object';
|
||||||
import ArrayList from '@shell/components/form/ArrayList';
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
@@ -142,10 +142,12 @@ export default {
|
|||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
const vlans = this.$store.getters[`${ inStore }/all`](HCI.VLAN) || [];
|
const vlans = this.$store.getters[`${ inStore }/all`](HCI.VLAN) || [];
|
||||||
|
|
||||||
return vlans.map((vlan) => ({
|
const options = vlans.map((vlan) => ({
|
||||||
label: vlan.id,
|
label: vlan.id,
|
||||||
value: vlan.id,
|
value: vlan.id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
return [{ label: this.t('generic.none'), value: '' }, ...options];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -160,6 +162,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onVlanChange(value) {
|
||||||
|
if (value === '') {
|
||||||
|
remove(this.value.spec, 'vlan');
|
||||||
|
} else {
|
||||||
|
set(this.value, 'spec.vlan', value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async saveSubnet(buttonCb) {
|
async saveSubnet(buttonCb) {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
const name = this.value?.metadata?.name;
|
const name = this.value?.metadata?.name;
|
||||||
@@ -283,12 +293,13 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="value.spec.vlan"
|
:value="value.spec.vlan ?? ''"
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
:options="vlanOptions"
|
:options="vlanOptions"
|
||||||
:placeholder="t('harvester.subnet.vlan.placeholder')"
|
:placeholder="t('harvester.subnet.vlan.placeholder')"
|
||||||
:label="t('harvester.subnet.vlan.label')"
|
:label="t('harvester.subnet.vlan.label')"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
@update:value="onVlanChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export default {
|
|||||||
RANCHER_TYPES,
|
RANCHER_TYPES,
|
||||||
fvFormRuleSets: [{ path: 'spec.displayName', rules: ['required'] }],
|
fvFormRuleSets: [{ path: 'spec.displayName', rules: ['required'] }],
|
||||||
canEditPSPBindings: true,
|
canEditPSPBindings: true,
|
||||||
|
isQuotasValid: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -190,15 +191,13 @@ export default {
|
|||||||
this['membershipUpdate'] = update;
|
this['membershipUpdate'] = update;
|
||||||
},
|
},
|
||||||
|
|
||||||
removeQuota(key) {
|
validateQuotas(isValid) {
|
||||||
['resourceQuota', 'namespaceDefaultResourceQuota'].forEach((specProp) => {
|
this.isQuotasValid = isValid;
|
||||||
if (this.value?.spec[specProp]?.limit && this.value?.spec[specProp]?.limit[key]) {
|
},
|
||||||
delete this.value?.spec[specProp]?.limit[key];
|
|
||||||
}
|
onQuotasInput({ projectLimit, nsLimit } = {}) {
|
||||||
if (this.value?.spec[specProp]?.usedLimit && this.value?.spec[specProp]?.usedLimit[key]) {
|
this.value.spec.resourceQuota = { ...this.value.spec.resourceQuota, limit: projectLimit };
|
||||||
delete this.value?.spec[specProp]?.usedLimit[key];
|
this.value.spec.namespaceDefaultResourceQuota = { ...this.value.spec.namespaceDefaultResourceQuota, limit: nsLimit };
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -212,7 +211,7 @@ export default {
|
|||||||
:resource="value"
|
:resource="value"
|
||||||
:subtypes="[]"
|
:subtypes="[]"
|
||||||
:can-yaml="false"
|
:can-yaml="false"
|
||||||
:validation-passed="fvFormIsValid"
|
:validation-passed="fvFormIsValid && isQuotasValid"
|
||||||
@error="e=>errors=e"
|
@error="e=>errors=e"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
@@ -267,10 +266,11 @@ export default {
|
|||||||
:weight="9"
|
:weight="9"
|
||||||
>
|
>
|
||||||
<ResourceQuota
|
<ResourceQuota
|
||||||
v-model:value="value"
|
:value="value"
|
||||||
:mode="canEditTabElements"
|
:mode="canEditTabElements"
|
||||||
:types="isStandaloneHarvester ? HARVESTER_TYPES : RANCHER_TYPES"
|
:types="isStandaloneHarvester ? HARVESTER_TYPES : RANCHER_TYPES"
|
||||||
@remove="removeQuota"
|
@input="onQuotasInput"
|
||||||
|
@validationChanged="validateQuotas"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
|
|||||||
@@ -835,13 +835,13 @@ harvester:
|
|||||||
title: "Network Data:"
|
title: "Network Data:"
|
||||||
tip: "The network-data configuration allows you to customize the instance's networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
|
tip: "The network-data configuration allows you to customize the instance's networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
|
||||||
sysprep:
|
sysprep:
|
||||||
title: Windows Sysprep Configuration
|
title: Windows Unattended & Sysprep Configuration
|
||||||
description: "Configure Windows automated installation using autounattend.xml. The configuration will be stored in a Kubernetes Secret and mounted as a CD-ROM during installation. <a href='https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/automate-windows-setup' target='_blank'>Learn more</a>"
|
description: "Configure Windows automated installation using autounattend.xml. The configuration will be stored in a Kubernetes Secret and mounted as a CD-ROM during installation. <a href='https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/automate-windows-setup' target='_blank'>Learn more</a>"
|
||||||
createNew: Create new...
|
createNew: Create new...
|
||||||
createNewTitle: Create Windows Sysprep Template
|
createNewTitle: Create Windows Unattended & Sysprep Template
|
||||||
xmlContent: autounattend.xml content
|
xmlContent: autounattend.xml content
|
||||||
secret:
|
secret:
|
||||||
label: Windows Sysprep Template
|
label: Windows Unattended & Sysprep Template
|
||||||
validation:
|
validation:
|
||||||
invalidXml: Invalid XML syntax
|
invalidXml: Invalid XML syntax
|
||||||
invalidNamespace: Invalid unattend.xml namespace. Expected 'urn:schemas-microsoft-com:unattend'
|
invalidNamespace: Invalid unattend.xml namespace. Expected 'urn:schemas-microsoft-com:unattend'
|
||||||
@@ -2417,7 +2417,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.
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ export const SSH_EXISTING_TYPE = {
|
|||||||
export default {
|
export default {
|
||||||
methods: {
|
methods: {
|
||||||
hasCloudConfigComment(userScript) {
|
hasCloudConfigComment(userScript) {
|
||||||
|
if (typeof userScript === 'string' && /^\s*#cloud-config(\s|$)/.test(userScript)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check that userData contains: #cloud-config
|
// Check that userData contains: #cloud-config
|
||||||
const userDataDoc = userScript ? YAML.parseDocument(userScript) : YAML.parseDocument({});
|
const userDataDoc = userScript ? YAML.parseDocument(userScript) : YAML.parseDocument({});
|
||||||
const items = userDataDoc?.contents?.items || [];
|
const items = userDataDoc?.contents?.items || [];
|
||||||
@@ -52,6 +56,14 @@ export default {
|
|||||||
exist = true;
|
exist = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userDataDoc?.contents?.commentBefore === 'cloud-config' || userDataDoc?.contents?.commentBefore?.includes('cloud-config\n')) {
|
||||||
|
exist = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userDataDoc?.contents?.comment === 'cloud-config' || userDataDoc?.contents?.comment?.includes('cloud-config\n')) {
|
||||||
|
exist = true;
|
||||||
|
}
|
||||||
|
|
||||||
items.map((item) => {
|
items.map((item) => {
|
||||||
const key = item.key;
|
const key = item.key;
|
||||||
|
|
||||||
|
|||||||
@@ -1211,9 +1211,10 @@ export default {
|
|||||||
|
|
||||||
userDataDoc = config.installAgent ? this.mergeQGA({ userDataDoc, ...config }) : this.deleteQGA({ userDataDoc, ...config });
|
userDataDoc = config.installAgent ? this.mergeQGA({ userDataDoc, ...config }) : this.deleteQGA({ userDataDoc, ...config });
|
||||||
const userDataYaml = userDataDoc.toString();
|
const userDataYaml = userDataDoc.toString();
|
||||||
|
const userDataValue = userDataDoc.toJSON();
|
||||||
|
|
||||||
if (userDataYaml === '{}\n') {
|
if (userDataValue === null || (typeof userDataValue === 'object' && !Array.isArray(userDataValue) && isEmpty(userDataValue))) {
|
||||||
// When the YAML parsed value is '{}\n', it means that the userData is empty, then undefined is returned.
|
// Empty userData should not create cloud-init content like `null`.
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ export default class HarvesterResource extends SteveModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disable Resources/ Extras cards in resource details pages
|
||||||
|
get resourcesCardRows() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
cleanForSave(data, _forNew) {
|
cleanForSave(data, _forNew) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,10 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import Loading from '@shell/components/Loading';
|
|
||||||
import { HCI } from '../../../../../types';
|
import { HCI } from '../../../../../types';
|
||||||
import SerialConsole from '../../../../../components/SerialConsole';
|
import SerialConsole from '../../../../../components/SerialConsole';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { SerialConsole, Loading },
|
components: { SerialConsole },
|
||||||
|
|
||||||
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 });
|
||||||
@@ -38,16 +37,44 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Loading v-if="$fetchState.pending" />
|
<div class="serial-console-page">
|
||||||
<SerialConsole
|
<SerialConsole
|
||||||
v-else
|
ref="serialConsole"
|
||||||
ref="serialConsole"
|
:value="vmi || {}"
|
||||||
v-model:value="vmi"
|
/>
|
||||||
/>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss">
|
||||||
body, #__nuxt, #__layout, main {
|
html,
|
||||||
|
body,
|
||||||
|
#__nuxt,
|
||||||
|
#__layout,
|
||||||
|
main {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.serial-console-page {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
min-height: 0;
|
||||||
|
background: var(--body-bg);
|
||||||
|
color: var(--terminal-text);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 6px;
|
||||||
|
pointer-events: none;
|
||||||
|
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
.harvester-shell-container {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--terminal-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -68,29 +68,35 @@ export default defineComponent({
|
|||||||
return (this.value || []).map((plan) => {
|
return (this.value || []).map((plan) => {
|
||||||
const planName = plan?.metadata?.name || '-';
|
const planName = plan?.metadata?.name || '-';
|
||||||
const namespace = plan?.metadata?.namespace || '';
|
const namespace = plan?.metadata?.namespace || '';
|
||||||
|
const networkMapRef = this.getPlanMapRef(plan, 'network');
|
||||||
|
const storageMapRef = this.getPlanMapRef(plan, 'storage');
|
||||||
|
|
||||||
const migrations = existing[HCI.FORKLIFT_MIGRATION]
|
const migrations = existing[HCI.FORKLIFT_MIGRATION]
|
||||||
.filter((resource) => this.ownedByPlan(resource, plan))
|
.filter((resource) => this.ownedByPlan(resource, plan))
|
||||||
.map((resource) => ({
|
.map((resource) => resource?.metadata?.name)
|
||||||
name: resource?.metadata?.name,
|
.filter(Boolean);
|
||||||
namespace: resource?.metadata?.namespace || namespace,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const matchedNetworkMap = existing[HCI.FORKLIFT_NETWORK_MAP].find((resource) => this.ownedByPlan(resource, plan));
|
const matchedNetworkMap = networkMapRef ? existing[HCI.FORKLIFT_NETWORK_MAP].find((resource) => resource?.metadata?.name === networkMapRef.name && resource?.metadata?.namespace === networkMapRef.namespace) : null;
|
||||||
const matchedStorageMap = existing[HCI.FORKLIFT_STORAGE_MAP].find((resource) => this.ownedByPlan(resource, plan));
|
const matchedStorageMap = storageMapRef ? existing[HCI.FORKLIFT_STORAGE_MAP].find((resource) => resource?.metadata?.name === storageMapRef.name && resource?.metadata?.namespace === storageMapRef.namespace) : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
planName,
|
planName,
|
||||||
namespace,
|
namespace,
|
||||||
migrations,
|
migrations,
|
||||||
networkMap: matchedNetworkMap ? {
|
networkMap: matchedNetworkMap ? {
|
||||||
namespace: matchedNetworkMap?.metadata?.namespace || namespace,
|
namespace: matchedNetworkMap?.metadata?.namespace || networkMapRef?.namespace || namespace,
|
||||||
name: matchedNetworkMap?.metadata?.name,
|
name: matchedNetworkMap?.metadata?.name,
|
||||||
} : null,
|
} : (networkMapRef ? {
|
||||||
|
namespace: networkMapRef.namespace,
|
||||||
|
name: networkMapRef.name,
|
||||||
|
} : null),
|
||||||
storageMap: matchedStorageMap ? {
|
storageMap: matchedStorageMap ? {
|
||||||
namespace: matchedStorageMap?.metadata?.namespace || namespace,
|
namespace: matchedStorageMap?.metadata?.namespace || storageMapRef?.namespace || namespace,
|
||||||
name: matchedStorageMap?.metadata?.name,
|
name: matchedStorageMap?.metadata?.name,
|
||||||
} : null,
|
} : (storageMapRef ? {
|
||||||
|
namespace: storageMapRef.namespace,
|
||||||
|
name: storageMapRef.name,
|
||||||
|
} : null),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -113,6 +119,20 @@ export default defineComponent({
|
|||||||
methods: {
|
methods: {
|
||||||
resourceNames,
|
resourceNames,
|
||||||
|
|
||||||
|
getPlanMapRef(plan, mapType) {
|
||||||
|
const ref = plan?.spec?.map?.[mapType];
|
||||||
|
const name = ref?.name;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
namespace: ref?.namespace || plan?.metadata?.namespace || '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether a related resource (migration / network map / storage map)
|
* Determine whether a related resource (migration / network map / storage map)
|
||||||
* is owned by the given plan by inspecting its `metadata.ownerReferences`.
|
* is owned by the given plan by inspecting its `metadata.ownerReferences`.
|
||||||
@@ -137,9 +157,8 @@ export default defineComponent({
|
|||||||
const existing = this.existingRelatedResources;
|
const existing = this.existingRelatedResources;
|
||||||
const targets = new Map();
|
const targets = new Map();
|
||||||
|
|
||||||
const addTarget = (type, resource) => {
|
const addTarget = (type, name, namespace) => {
|
||||||
const name = resource?.metadata?.name;
|
const ns = namespace || '';
|
||||||
const ns = resource?.metadata?.namespace;
|
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return;
|
return;
|
||||||
@@ -153,17 +172,20 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (const plan of this.value || []) {
|
for (const plan of this.value || []) {
|
||||||
existing[HCI.FORKLIFT_NETWORK_MAP]
|
const networkMapRef = this.getPlanMapRef(plan, 'network');
|
||||||
.filter((resource) => this.ownedByPlan(resource, plan))
|
const storageMapRef = this.getPlanMapRef(plan, 'storage');
|
||||||
.forEach((resource) => addTarget(HCI.FORKLIFT_NETWORK_MAP, resource));
|
|
||||||
|
|
||||||
existing[HCI.FORKLIFT_STORAGE_MAP]
|
if (networkMapRef) {
|
||||||
.filter((resource) => this.ownedByPlan(resource, plan))
|
addTarget(HCI.FORKLIFT_NETWORK_MAP, networkMapRef.name, networkMapRef.namespace);
|
||||||
.forEach((resource) => addTarget(HCI.FORKLIFT_STORAGE_MAP, resource));
|
}
|
||||||
|
|
||||||
|
if (storageMapRef) {
|
||||||
|
addTarget(HCI.FORKLIFT_STORAGE_MAP, storageMapRef.name, storageMapRef.namespace);
|
||||||
|
}
|
||||||
|
|
||||||
existing[HCI.FORKLIFT_MIGRATION]
|
existing[HCI.FORKLIFT_MIGRATION]
|
||||||
.filter((resource) => this.ownedByPlan(resource, plan))
|
.filter((resource) => this.ownedByPlan(resource, plan))
|
||||||
.forEach((resource) => addTarget(HCI.FORKLIFT_MIGRATION, resource));
|
.forEach((resource) => addTarget(HCI.FORKLIFT_MIGRATION, resource?.metadata?.name, resource?.metadata?.namespace));
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...targets.values()];
|
return [...targets.values()];
|
||||||
@@ -256,11 +278,11 @@ export default defineComponent({
|
|||||||
<div>• {{ t('harvester.addons.vmMigration.labels.migration') }}:</div>
|
<div>• {{ t('harvester.addons.vmMigration.labels.migration') }}:</div>
|
||||||
<template v-if="summary.migrations.length">
|
<template v-if="summary.migrations.length">
|
||||||
<div
|
<div
|
||||||
v-for="migration in summary.migrations"
|
v-for="migrationName in summary.migrations"
|
||||||
:key="`${ migration.namespace }/${ migration.name }`"
|
:key="migrationName"
|
||||||
>
|
>
|
||||||
<div class="ml-20">
|
<div class="ml-20">
|
||||||
- {{ migration.name }}
|
- {{ migrationName }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user