Compare commits

..
64 Commits
Author SHA1 Message Date
Andy Lee 96c374b7aa feat: improve vnc console
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-09-09 19:17:22 +08:00
renovate[bot] bbe0c4643f deps: update dependency qs to v6.16.0 [security] (#1134)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-07 13:56:15 +08:00
marcelofukumoto 00584f1d19 chore(deps): bump @rancher/shell to 3.0.13 (#1130)
Clears 26 of the 54 open Dependabot alerts (axios, dompurify, serve-static, and
drops vulnerable send / @tootallnate/once copies).

3.0.13 ships the flat-config ESLint 10 toolchain, so lint now routes through the
@rancher/shell launcher (keeping the legacy .eslintrc on ESLint 7); a prelint step
installs an ESLint-7-compatible eslint-plugin-jest into the isolated toolchain.

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
2026-09-04 10:36:52 +02:00
Tim SerongandVolker Theile 4fa6470343 feat: LHv2: graduate from "Experimental" to "Preview" (#1129)
In Harvester v1.9.0, we're moving LHv2 from "Experimental" to "Preview",
so I've added a new preview label to the settings interface.  I figure
given Experimental had the "error" color, I should make "Preview" use
the warning color, to make it a bit less alarming :-)

Related-to: https://github.com/harvester/harvester/issues/9960

Signed-off-by: Tim Serong <tserong@suse.com>
Co-authored-by: Volker Theile <vtheile@suse.com>
2026-09-01 11:30:38 +10:00
Andy Lee d550046189 fix: vGPU doesn't display when creating harvester downstream cluster (#1123)
* fix: vGPU doesn't display in edit VM page

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

* fix: align PCI passthrough action buttons height

Match the enable/disable passthrough and Actions dropdown buttons to 40px and move the selected-count label below them.

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-25 16:24:37 +08:00
Po Han Huang 5f32a9ade5 feat: improve readability for vm migration UI (#1112)
Signed-off-by: pohanhuang <pohan.huang@suse.com>
2026-08-18 12:32:02 +08:00
marcelofukumoto 3f39d561be security: fix 15 transitive Dependabot alerts via yarn.lock re-resolution (#1115)
Re-resolve 15 vulnerable transitive dependencies to their latest in-range
patched versions in yarn.lock only (no package.json changes): @babel/core,
@babel/plugin-transform-modules-systemjs, brace-expansion, fast-uri, form-data,
http-proxy-middleware, immutable, joi, js-cookie, launch-editor, nanoid,
shell-quote, svgo, websocket-driver, ws.

The remaining alerts are pinned by parent packages (chiefly @rancher/shell) or
need a cross-major bump, so they can't be resolved by the lockfile alone; they
are tracked separately.

Signed-off-by: Marcelo Fukumoto <marceloyfukumoto@gmail.com>
2026-08-17 10:30:05 +02:00
Andy Lee 7c84269ed6 fix: exclude forklift namespace from system classification (#1114)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-17 15:23:23 +10:00
Andy Lee f63be73b11 fix: ACCESS_MODE undefined error in forklift mapping page (#1111) 2026-08-14 10:01:27 +08:00
Andy Lee fabc1cec21 fix: prevent duplicated cloud-config for empty user data (#1109)
* fix: prevent duplicated cloud-config for empty user data

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

* refactor: AI review

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-12 14:20:25 +08:00
Andy Lee ed2b6f2551 fix: remove map ownerref writeback and delete maps via plan refs (#1105)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-10 18:14:48 +08:00
renovate[bot] 72282ae50e deps: update dependency @types/lodash to v4.17.25 (#1099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-10 10:10:31 +08:00
Volker Theile 8fba4e7050 fix: Rename to Windows Unattended & Sysprep (#1096)
Rename title and other mentions of Sysprep to prevent misunderstandings regarding functionality. For the average Windows administrator who simply wants to automate the installation of a Windows ISO, it isn't immediately intuitive when the term “Sysprep” is used on its own.

Related to: https://github.com/harvester/harvester/issues/1836
Related to: https://github.com/harvester/harvester-ui-extension/pull/954

Signed-off-by: Volker Theile <vtheile@suse.com>
2026-08-07 11:22:23 +08:00
Andy Lee fbd4a1108f ci: add test steps in PR template (#1093)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-05 15:53:40 +08:00
Andy Lee 823a88c85b ci: not use actions/github-script@v7 (#1092)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-05 14:37:24 +08:00
Andy Lee 3a2c6e3a3d ci: support manual standalone build trigger and PR comment (#1091)
* ci: add manually trigger build

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

* refactor: PR comment

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-05 14:21:41 +08:00
Andy Lee be6e9b5f06 fix(subnet): support None option for VLAN dropdown (#1085)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-05 12:23:57 +08:00
Andy Lee a88bed9eb4 fix: clear keyword should clear highlight (#1088)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-05 00:24:34 +08:00
Andy Lee f97f14c811 deps: bump @rancher/shell to 3.0.12-rc.7 (#1084)
* deps: update rancher/shell to 3.0.12-rc.7

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

* fix: move filter labels/schedule buttons position

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

* feat: improve serial console

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

* feat(serial-console): add terminal search toolbar and highlight tuning

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

* refactor: clear searach result after close

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

* refactor: fine tune serial console

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-04 19:53:24 +08:00
Andy Lee f8a29a1c54 fix: project resource quota not set in POST API payload (#1082)
* fix: project resource quota not set in POST API payload

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

* fix: guard onQuotasInput against undefined payload

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-04 13:27:30 +08:00
freeze 9a935434c2 feat: support attaching volumes as shareable disks (#1026)
Show a Shareable checkbox when attaching an existing volume whose
    PVC is RWX Block and not provisioned by Longhorn. Checking it sets
    disk.shareable and shows a data-corruption warning banner.

    Related issue: harvester/harvester#9650

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2026-08-03 16:24:33 +08:00
Andy Lee afe632b723 feat: add access/volume mode support and improvements for VM migration (#1074)
* fix: VM migration tab not appear when first login

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

* feat: add access/ volume mode model

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

* feat: add selected all | clear all actions for selected VM

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

* fix: bugs in VM selection table

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

* refactor: copilot review

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-08-03 12:58:37 +08:00
renovate[bot] d050f21f9a deps: update patch dependencies (#1075)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-03 12:58:04 +08:00
Andy Lee b87f367583 fix: prevent VM detail page hang when static IP assigned (#1072)
Make syncStaticIpAnnotations idempotent so it no longer mutates VM
annotations when already in sync, breaking a reactive update loop that
froze the browser on the VM detail page. Also stack multiple IP
addresses vertically in the IP Address column.

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-31 16:55:25 +08:00
Andy Lee 92aa8ec603 ci: disable footer-max-line-length rule (#1069)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-29 14:46:07 +08:00
Alejandro BonillaandVolker Theile e013534373 feat: allow Cloud Config for Windows guests (Cloudbase-Init) (#984)
* feat(vm): allow Cloud Config for Windows guests

Currently the Cloud Config editor is completely hidden when a VM's
OS Type is set to Windows, and no `cloudinitdisk` volume or secret
is written to the VM spec even if the user has entered content.

This blocks a common Windows-on-Harvester workflow: using
Cloudbase-Init to run first-boot configuration (install VMDP,
enable OpenSSH server, add authorized_keys, join a domain, run
`runcmd` steps, etc.). Users on Windows today have to hand-edit the
VM YAML after creation to attach a cloudinitdisk, which is fragile
and undiscoverable.

Changes:
- Remove the `v-if="!isWindows"` on the CloudConfig component so
  the editor is available for every OS type. The Sysprep editor
  continues to render only for Windows, so Windows now gets both
  panels; Linux is unchanged.
- Drop the `if (!this.isWindows)` guard around the cloudinitdisk
  volume serialization so the disk is attached whenever the user
  provided user-data or network-data, regardless of OS.
- Drop the `|| this.isWindows` early-return in `saveSecret` so the
  cloud-init secret is persisted for Windows VMs too.
- `getInitUserData` now returns a bare `#cloud-config\n` header for
  Windows instead of the Linux qemu-guest-agent runcmd template
  (which would fail on Cloudbase-Init). VMDP installs the QGA on
  Windows as a native service, so the runcmd path is not needed.

The "Install guest agent" checkbox stays disabled for Windows
because it specifically drives the Linux QGA-via-runcmd recipe.

Fixes point 2 of harvester/harvester#11124.

Signed-off-by: Alejandro Bonilla <abonilla@suse.com>

* fix(vm): do not clear cloud-config on isWindows watcher fire

The isWindows watcher runs on every change of the isWindows computed
property, including on mount when editing an existing Windows VM. Setting
this['userScript'] and this['networkScript'] to undefined at that point
wipes the cloud-config data that was just loaded from the existing VM's
cloudinit secret, so the Advanced -> Cloud Config editor shows empty
even though the VM has data on the wire.

Remove the two clears - Cloud Config is now supported for Windows guests
by this PR, so there is no reason to unset the user/network scripts when
the OS type is Windows. sshKey and installAgent remain gated (SSH keys
are injected via cloud-init on Linux only, and the qemu-guest-agent
package install checkbox does not apply to Windows).

Signed-off-by: Alejandro Bonilla <abonilla@suse.com>
Co-authored-by: Volker Theile <vtheile@suse.com>

* refactor(vm): Get CloudInit working for VM templates

- Remove unnecessary osType watcher in cloud config. The watcher (see `pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineCloudConfig/DataTemplate.vue`) that cleared the cloud config template for Windows VMs is no longer necessary.
- Automatically strip the CloudInit `User Data` properties `package_update` and `packages` for OS type `Windows`.

Signed-off-by: Volker Theile <vtheile@suse.com>

---------

Signed-off-by: Alejandro Bonilla <abonilla@suse.com>
Signed-off-by: Volker Theile <vtheile@suse.com>
Co-authored-by: Volker Theile <vtheile@suse.com>
2026-07-28 11:32:30 +02:00
Andy Lee d23c3d3b72 revert: PR build artifact (#1066)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-28 15:39:20 +08:00
Andy Lee 719c33fa12 ci: fix CI_BRANCH ternary so PR builds skip upload gate (#1062)
* ci: fix CI_BRANCH ternary so PR builds skip upload gate

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

* ci: simplify CI_BRANCH expression comment

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-28 14:34:21 +08:00
Andy Lee a6bd7ec00e feat: add VMware VDDK image input field to VM migration provider (#1057)
* feat: add VMware VDDK image option to VM migration provider

Add an optional VMware VDDK Image field to the Configure Provider step of the VM migration wizard, bound to spec.settings.vddkInitImage when set and left unset when empty. Supports create and edit flows and populates from the existing provider when editing.

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

* refactor: copilot review

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-28 14:10:44 +08:00
Andy Lee ac26fe2f0e ci: trigger standalone build when PR labeled with build-artifact (#1058)
* ci: trigger standalone build when PR labeled with build-artifact

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

* ci: skip upload gate for PR-triggered standalone builds

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-28 13:56:11 +08:00
Andy Lee 790cade04d feat: validate migration plan name against existing providers and plans (#1053)
Show an error banner and disable the Create and Start action when the migration plan name matches an existing provider or migration plan name.

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-24 16:43:57 +08:00
Andy Lee 2903e86c02 refactor: new design for review page (#1051)
* refactor: new design for review page

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

* refactor: customize empty table rows message

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

* refactor: fixed the table column

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

* style: update header tag and align fixed header actions in review migration step

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

* style: remove unneeded overflow css

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-24 10:24:22 +08:00
Andy Lee 2d747e435a fix: forklift mapping UX and migration progress fixes (#1049)
* refactor: modify relatedResources using ownerReferences uid

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

* refactor: wordings

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

* fix(vm-migration): keep completed VMs green and linkable in failed plans

When a plan fails overall, VMs that already reached 100% were marked with an error: their progress bar turned red while the label still read "Finished Successfully", and the migrated-VM detail link was hidden.

Skip the plan-level failure fallback for VMs at 100% and drop the !planFailed guard from the detail-link condition so successfully migrated VMs stay green and stay navigable.

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-23 14:38:49 +08:00
CopilotandAndy Lee eadda2a18b refactor: extract access mode strings to static ACCESS_MODE constant (#1039)
* refactor: extract access mode strings to static ACCESS_MODE constant

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

* refactor: destructure ACCESS_MODE variables in CDISettings.vue accessModeOptions

* refactor: destructure ACCESS_MODE at top level in all consumer files

* fix(lint): reorder imports to top of module in CDISettings, harvesterhci.io.volume, and VirtualMachineVolume

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-21 11:07:48 +08:00
Volker Theile a178302f73 fix: Creating a VM with no cloud init user data but network data only fails (#1044)
Related to: https://github.com/harvester/harvester/issues/11213

Signed-off-by: Volker Theile <vtheile@suse.com>
2026-07-21 11:06:24 +08:00
Andy Lee d376bfb3a5 docs: require scope-less conventional commit format in guides (#1042)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-20 20:05:36 +08:00
Andy Lee 7a84e08f99 fix(vm-migration): show elapsed duration for in-progress plans (#1037)
formatDuration returned an empty value when a migration was in progress (started set but completed missing), causing the duration column to show an em-dash for all running plans. It now computes elapsed time up to Date.now() when completed is absent, only returning the placeholder when started is unset or timestamps are invalid.

Related issue: harvester/harvester#10417

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-20 17:05:27 +08:00
Andy Lee 3d39626b93 docs(agents): clarify agent guide and share it across AI assistants (#1035)
* ci: disable header-max-length commit rule

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

* docs(agents): clarify agent guide and share it across AI assistants

- Symlink CLAUDE.md and GEMINI.md to AGENTS.md so Claude Code, Copilot
  and Gemini read the same guidance
- Use the full conventional-commit format with scope in the guide
- Clarify Vue SFC block order and the feature-flag development steps
- Bump the documented Node.js requirement to v24

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-20 15:51:12 +08:00
Andy Lee 1c3b40b631 refactor(host): update resource usage gauges and table column headers to fit small screen (#1031)
* refactor(host): update resource usage gauges and table column headers

- Replace ConsumptionGauge with PercentageBar in CPU/Memory/Storage
  formatters to show the percentage next to the progress bar
- Rename the Disk State column header to Disk (via i18n)
- Display the CPU Manager column header on two lines

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

* refactor: add noopener noreferrer rel

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-20 15:29:12 +08:00
Andy Lee 87baf75301 fix(ci): support scoped commit prefixes in PR label extraction (#1032)
Strip the scope in parentheses so titles like 'refactor(host): ...'
resolve to the 'refactor' type instead of falling back to 'other'.

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-20 15:23:26 +08:00
Andy Lee a10d651827 feat: improve VM migration dashboard mapping labels and VM detail link (#1029)
* feat: improve VM migration dashboard mapping labels and VM detail link

- Display Management Network / Ignored labels via i18n in mappings column and review card
- Add clickable external-link icon to migrated VM detail page after successful migration
- Add tooltip to the VM detail external-link icon

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

* refactor: modify duration column width

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

* refactor: copilot review comment

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-20 14:30:26 +08:00
renovate[bot] 11ff7c8a7c deps: update dependency @types/node to v25.9.5 (#1021)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-20 12:20:34 +08:00
Jack Yu f6dffe9816 feat: support skip MIG mode configuration in srivogpu (#1009)
Signed-off-by: Jack Yu <jack.yu@suse.com>
2026-07-20 11:22:40 +08:00
Andy Lee b813bc578e docs: add XSS safety comment for VM display name annotation (#1019)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-17 17:30:59 +08:00
Andy Lee 7c3b2730ab fix: remove bootable volume validation in VM disk validator (#1016)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-17 16:59:02 +08:00
Andy LeeandCopilot App 429d9e18ad fix: stale cloud-init secret name reused after VM name change (#1012)
* fix: regenerate stale cloud-init secret name after VM name change

When creating a VM, the cloud-init/sysprep secret name was generated
once and cached for the component's lifetime (needNewSecret is always
false during create). If a first create attempt was rejected by the
API (e.g. duplicate name) and the user corrected the name and retried,
the VM was created successfully but reused the secret name derived
from the rejected attempt, and any subsequent secret-creation failure
surfaced as a stale error after the VM already existed.

Now the secret name (and Windows sysprep secret name) is regenerated
whenever the VM name prefix used to generate it no longer matches the
current name, and is proactively reset on a failed create attempt so
retries always start fresh.

Fixes harvester/harvester#11174

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: copilot review

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

* refactor: remove console

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-17 14:29:01 +08:00
Andy Lee dc166d8fc9 ci: modify PR template (#1013)
* ci: modify PR template

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

* refactor: review comment

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-17 14:22:32 +08:00
Andy Lee 3ff4534294 feat: allow user specify namespace for migrated VM (#1007)
* feat: allow user specify namespace for migrated VM

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

* refactor: not pre-select target namespace

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

* refactor: ignore option

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

* refactor: copilot review

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-16 15:29:39 +08:00
Andy Lee d4223ad2ae feat: keep upgrade button appear (#1003)
* feat: keep upgrade button appear

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

* refactor: copilot review

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-16 11:24:51 +08:00
Andy Lee 44297ada16 feat: add migration plan duration time column (#1005)
* feat: add migration plan duration time column

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

* feat: allow duration column to be sorted

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-15 18:42:01 +08:00
Andy Lee aea53fdd65 chore: update main branch to 1.10.0-dev (#1004)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-15 18:23:32 +08:00
Andy Lee 9640c87947 fix: allow input vlan 1 in host network (#999)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-15 15:24:49 +08:00
Volker Theile 2cefd7cb78 fix: A disk entry must be either disk or cdrom, never both (#998)
Related to: https://github.com/harvester/harvester/issues/11154

Signed-off-by: Volker Theile <vtheile@suse.com>
2026-07-15 09:11:58 +02:00
Andy Lee fb6ffa3c0f refactor: add customized migration plan delete model (#996)
* refactor: add customized migration plan delete model

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

* refactor: copilot review

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

* refactor: copilot review

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-14 17:36:25 +08:00
Andy Lee e0436bff28 feat: add migration steps tooltip when mouse hover on progress bar (#994)
* feat: add progress bar tooltip

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

* refactor: copilot review

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-14 14:37:13 +08:00
Andy Lee 219f39ae52 refactor: rename pod network to management network (#992) 2026-07-14 13:56:59 +08:00
Andy Lee 41cf44d231 refactor: forklift UI bug and enhancement (#989)
* fix: hide Virtul machine migration tabs if forklift addon is not enabled

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

* fix: rename advanced to configurations

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

* fix: fixed column width

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

* refactor: review comment

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

* revert: power on background color

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-14 12:04:00 +08:00
Jack Yu 7d09cb7142 feat: prevent users from selecting mounted devices (#987)
Signed-off-by: Jack Yu <jack.yu@suse.com>
2026-07-14 11:29:41 +08:00
marcelofukumoto 560643d299 feat: forklift UI (#891)
* feat: forklift poc first commit

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat: add migration and plan detail pages with progress tracking

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): UI based on the FIGMA

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): Fixed config based on new figma changes

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): New changes added based on new figma

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): Add the succeded state

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* Merge the new APIs

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): Applied fix to select the proper data on network. Added condition to show critical

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): Added icons, critical state, fix grids, action button

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): Change to use the proper provider instead of a hardcoded

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): Added pagination UI

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): FIxed the routing for the extension

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): route and router doesnt work properly on extensions

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): fixed to fix the icon on extension

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): Added overflow hidden to mapping target list

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): Added better handling on the saving and testing

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): Remove test flags

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): Changed based on review

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): fixed title and button

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): Design changes

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): Changed to Wizard

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): added the provider wizard

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): Fixed some UI changes

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): Removed wrong configs used for testing

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): update size

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): small fixes

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): small fixes after Copilot

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): small fixes after Copilot

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): reverted settings.json changes

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): small fixes after Copilot

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): Fix the way it is registered

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): Fixed the type for VMware

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat(forklift): small changes before rebase

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat: Added new API for the vms

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat: Fix some labels

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat: Added some final fixes to structure

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat: added loading to provider steps

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* feat: added comments from copilot review

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

---------

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
2026-07-13 16:58:35 +08:00
Renuka Devi Rajendran c3bb0ea4be feat: Display mgmt interface details under mgmt cluster network (#974)
Signed-off-by: Renuka Devi Rajendran <renuka.rajendran@suse.com>
2026-07-10 11:40:57 +08:00
Andy Lee 3c37f04ef6 chore: limit the rancher to 2.15.0+ for harvester v1.9.0 (#982)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-10 10:52:33 +08:00
Volker Theile d98db6ba49 feat: Allow user to customize snapshot/backup deadline (#866)
* feat: Allow user to customize snapshot/backup deadline

Related to: https://github.com/harvester/harvester/issues/4098
Related to: https://github.com/harvester/harvester/pull/10545

Signed-off-by: Volker Theile <vtheile@suse.com>

* feat: Allow user to customize snapshot/backup deadline

Related to: https://github.com/harvester/harvester/issues/4098
Related to: https://github.com/harvester/harvester/pull/10545

Signed-off-by: Volker Theile <vtheile@suse.com>

---------

Signed-off-by: Volker Theile <vtheile@suse.com>
2026-07-09 08:44:47 +02:00
Andy Lee 531ed03796 feat: allow user specify static IP when VM creation (#973)
* feat: allow suer give static IP for VM

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

* feat: add feature flag

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-09 13:17:19 +08:00
Andy Lee 59f260eab8 fix: vpc topology should consume NAD labels (#975)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-08 13:47:25 +08:00
28 changed files with 2556 additions and 1969 deletions
+7 -3
View File
@@ -8,12 +8,16 @@
- [ ] Yes, the backend owner is @
- [ ] No, frontend-only.
### Related Issue #
### Related Issue
<!-- Link the issue as harvester/harvester#<issue_number> -->
harvester/harvester#<issue_number>
### Test screenshot or video (Required)
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->
### Test Steps
<!-- 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
on:
workflow_dispatch: # manually trigger on Github Actions only support PR branch pushed to harvester/harvester-ui-extension
push:
branches:
- main
@@ -29,3 +30,35 @@ jobs:
id-token: write
with:
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
View File
@@ -1,13 +1,14 @@
{
"name": "harvester-ui-extension",
"version": "1.9.0-rc5",
"version": "1.10.0-dev",
"private": false,
"engines": {
"node": ">=24.0.0"
},
"dependencies": {
"@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/controls": "^1.1.1",
"@vue-flow/core": "^1.33.5",
@@ -27,6 +28,7 @@
"yaml": "^2.5.1"
},
"resolutions": {
"@novnc/novnc": "1.7.0",
"@types/node": "25.9.5",
"cronstrue": "2.59.0",
"d3-color": "3.1.0",
@@ -39,7 +41,7 @@
"merge": "2.1.1",
"node-forge": "1.4.0",
"nth-check": "2.1.1",
"qs": "6.15.3",
"qs": "6.16.0",
"roarr": "7.21.7",
"semver": "7.8.5",
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
@@ -48,15 +50,17 @@
"dev": "NODE_ENV=dev ./node_modules/.bin/vue-cli-service serve",
"build": "./node_modules/.bin/vue-cli-service build",
"clean": "./node_modules/@rancher/shell/scripts/clean",
"lint": "./node_modules/.bin/eslint --max-warnings 0 --ext .js,.ts,.vue .",
"lint:fix": "./node_modules/.bin/eslint --fix --max-warnings 0 --ext .js,.ts,.vue .",
"lint": "./node_modules/@rancher/shell/scripts/lint --max-warnings 0 --ext .js,.ts,.vue .",
"lint:fix": "./node_modules/@rancher/shell/scripts/lint --fix --max-warnings 0 --ext .js,.ts,.vue .",
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
"commitlint": "commitlint --edit",
"prepare": "husky",
"agents:generate": "./scripts/generate-agent-and-persona-mds.sh"
"agents:generate": "./scripts/generate-agent-and-persona-mds.sh",
"prelint": "npm --prefix node_modules/@rancher/shell/eslint-legacy install eslint-plugin-jest@^24 --no-save --no-audit --no-fund --legacy-peer-deps --loglevel=error",
"prelint:fix": "npm --prefix node_modules/@rancher/shell/eslint-legacy install eslint-plugin-jest@^24 --no-save --no-audit --no-fund --legacy-peer-deps --loglevel=error"
},
"devDependencies": {
"@commitlint/load": "^19.8.1",
+17 -12
View File
@@ -231,14 +231,21 @@ export default {
v-if="setting.customized"
class="modified"
>
Modified
{{ t('advancedSettings.tags.modified.label') }}
</span>
<span
v-if="setting.experimental"
v-clean-tooltip="t('advancedSettings.experimental')"
v-clean-tooltip="t('advancedSettings.tags.experimental.description')"
class="experimental"
>
Experimental
{{ t('advancedSettings.tags.experimental.label') }}
</span>
<span
v-if="setting.preview"
v-clean-tooltip="t('advancedSettings.tags.preview.description')"
class="preview"
>
{{ t('advancedSettings.tags.preview.label') }}
</span>
</h1>
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
@@ -352,21 +359,19 @@ export default {
flex: 1;
}
.modified {
.modified,
.experimental,
.preview {
margin-left: 10px;
border: 1px solid var(--primary);
border: 1px solid;
border-radius: 5px;
padding: 2px 10px;
font-size: 12px;
}
.experimental {
margin-left: 10px;
border: 1px solid var(--error);
border-radius: 5px;
padding: 2px 10px;
font-size: 12px;
}
.modified { border-color: var(--primary); }
.experimental { border-color: var(--error); }
.preview { border-color: var(--warning); }
.no-search-match {
text-align: center;
+338 -84
View File
@@ -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>
import RFB from '@novnc/novnc/core/rfb';
import BrandImage from '@shell/components/BrandImage';
import ZoomableRFB from './ZoomableRFB';
export const CONSOLE_STATUS = {
CONNECTING: 'connecting',
CONNECTED: 'connected',
RECONNECTING: 'reconnecting',
DISCONNECTED: 'disconnected',
FAILED: 'failed',
};
const MAXIMUM_RETRY_TIMES = 10;
const BASE_RECONNECT_DELAY = 1000;
const MAXIMUM_RECONNECT_DELAY = 30000;
const clampLevel = (value, fallback) => {
const level = Number.parseInt(value, 10);
if (Number.isNaN(level)) {
return fallback;
}
return Math.min(Math.max(level, 0), 9);
};
export default {
name: 'NovncConsole',
components: { BrandImage },
props: {
url: {
type: String,
default: ''
}
},
settings: {
type: Object,
default: () => ({})
},
},
components: { BrandImage },
emits: ['status-changed', 'capabilities'],
data() {
return {
rfb: null,
connected: false,
disconnected: false,
reconnectDelay: 3000,
reconnecting: false,
maximumRetryTimes: 10,
status: CONSOLE_STATUS.CONNECTING,
statusDetail: '',
desktopName: '',
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() {
this.$nextTick(() => {
this.connect();
@@ -66,88 +101,307 @@ export default {
},
beforeUnmount() {
this.clearTimeout();
this.isClosed = true;
this.clearReconnectTimer();
this.teardown();
},
methods: {
connect() {
const rfb = new RFB(this.$refs.view, this.url);
connect(takeOver = false) {
if (!this.url || this.isClosed) {
return;
}
rfb.addEventListener('connect', () => {
this.clearTimeout();
this.teardown();
this.connected = true;
this.retryTimes = 0;
this.reconnecting = false;
});
this.status = this.retryTimes > 0 ? CONSOLE_STATUS.RECONNECTING : CONSOLE_STATUS.CONNECTING;
rfb.addEventListener('disconnect', (e) => {
this.clearTimeout();
let rfb;
this.disconnected = true;
this.rfb = null;
this.reconnect();
});
try {
const url = new URL(this.url);
url.searchParams.set('preserveSession', takeOver === true ? 'false' : 'true');
rfb = new ZoomableRFB(this.$refs.view, url.href, { shared: true });
} catch (err) {
this.statusDetail = err?.message || `${ err }`;
this.status = CONSOLE_STATUS.FAILED;
return;
}
rfb.addEventListener('connect', this.onConnect);
rfb.addEventListener('disconnect', this.onDisconnect);
rfb.addEventListener('securityfailure', this.onSecurityFailure);
rfb.addEventListener('credentialsrequired', this.onCredentialsRequired);
rfb.addEventListener('desktopname', this.onDesktopName);
rfb.addEventListener('capabilities', this.onCapabilities);
this.rfb = rfb;
this.applySettings();
},
reconnect() {
this.clearReconnectTimer();
this.isClosed = false;
this.retryTimes = 0;
this.statusDetail = '';
this.connect();
},
takeOver() {
if (!window.confirm(this.t('harvester.virtualMachine.detail.console.takeOver.confirm'))) {
return;
}
this.clearReconnectTimer();
this.isClosed = false;
this.retryTimes = 0;
this.statusDetail = '';
this.connect(true);
},
disconnect() {
this.isClosed = true;
this.clearReconnectTimer();
this.teardown();
this.status = CONSOLE_STATUS.DISCONNECTED;
},
onConnect() {
this.clearReconnectTimer();
this.retryTimes = 0;
this.statusDetail = '';
this.status = CONSOLE_STATUS.CONNECTED;
this.applySettings();
},
onDisconnect() {
const wasConnected = this.status === CONSOLE_STATUS.CONNECTED;
this.teardown(false);
if (this.isClosed) {
if (this.status !== CONSOLE_STATUS.FAILED) {
this.status = CONSOLE_STATUS.DISCONNECTED;
}
return;
}
if (wasConnected) {
this.scheduleReconnect();
} else {
this.clearReconnectTimer();
this.statusDetail = this.t('harvester.virtualMachine.detail.console.status.unavailable');
this.status = CONSOLE_STATUS.FAILED;
}
},
// Authentication problems will not fix themselves, so stop retrying.
onSecurityFailure(e) {
this.statusDetail = e?.detail?.reason || '';
this.isClosed = true;
this.clearReconnectTimer();
this.status = CONSOLE_STATUS.FAILED;
},
onCredentialsRequired() {
this.statusDetail = this.t('harvester.virtualMachine.detail.console.status.credentialsRequired');
this.isClosed = true;
this.clearReconnectTimer();
this.status = CONSOLE_STATUS.FAILED;
},
onDesktopName(e) {
this.desktopName = e?.detail?.name || '';
},
onCapabilities(e) {
this.$emit('capabilities', e?.detail?.capabilities || {});
},
scheduleReconnect() {
this.clearReconnectTimer();
if (this.isClosed) {
return;
}
if (this.retryTimes >= this.maximumRetryTimes) {
this.reconnecting = false;
this.connected = true;
this.disconnected = true;
this.status = CONSOLE_STATUS.FAILED;
return;
}
this.retryTimes += 1;
this.reconnecting = true;
this.connected = false;
this.disconnected = false;
this.status = CONSOLE_STATUS.RECONNECTING;
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.reconnectDelay);
}, delay);
},
clearTimeout() {
if (this.setTimeout) {
clearTimeout(this.setTimeout);
clearReconnectTimer() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
},
disconnect() {
this.rfb.disconnect();
teardown(disconnect = true) {
const rfb = this.rfb;
if (!rfb) {
return;
}
this.rfb = null;
rfb.removeEventListener('connect', this.onConnect);
rfb.removeEventListener('disconnect', this.onDisconnect);
rfb.removeEventListener('securityfailure', this.onSecurityFailure);
rfb.removeEventListener('credentialsrequired', this.onCredentialsRequired);
rfb.removeEventListener('desktopname', this.onDesktopName);
rfb.removeEventListener('capabilities', this.onCapabilities);
if (!disconnect) {
return;
}
try {
rfb.disconnect();
} catch (err) {
// The socket may already be gone, nothing left to clean up.
}
},
applySettings() {
const rfb = this.rfb;
if (!rfb) {
return;
}
const settings = this.settings || {};
rfb.viewOnly = !!settings.viewOnly;
rfb.scaleViewport = !!settings.scaleViewport;
rfb.resizeSession = !!settings.resizeSession;
rfb.clipViewport = !!settings.clipViewport;
rfb.dragViewport = !!settings.dragViewport;
rfb.zoomPercent = settings.zoomPercent;
rfb.focusOnClick = settings.focusOnClick !== false;
rfb.qualityLevel = clampLevel(settings.qualityLevel, 6);
rfb.compressionLevel = clampLevel(settings.compressionLevel, 2);
},
ctrlAltDelete() {
this.rfb.sendCtrlAltDel();
this.rfb?.sendCtrlAltDel();
},
sendKey(keysym, code, down) {
this.rfb.sendKey(keysym, code, down);
}
this.rfb?.sendKey(keysym, code, down);
},
}
};
</script>
<style lang="scss" scoped>
.error {
overflow: hidden;
<template>
<div class="novnc-console">
<div
ref="view"
class="novnc-screen"
/>
.row {
align-items: center;
}
<div
v-if="showError || showReconnecting"
class="novnc-overlay"
>
<div
v-if="showError"
class="text-center"
>
<BrandImage
file-name="error-desert-landscape.svg"
width="900"
height="300"
/>
<h1>
{{ t('generic.notification.title.warning') }}
</h1>
<h2 class="text-secondary mt-20">
{{ t('vncConsole.error.message') }}
</h2>
<p
v-if="statusDetail"
class="text-muted mt-10"
>
{{ statusDetail }}
</p>
<button
class="btn role-primary mt-20"
@click="reconnect"
>
{{ t('harvester.action.reconnect') }}
</button>
<button
v-if="!isClosed"
class="btn role-secondary mt-20 ml-10"
@click="takeOver"
>
{{ t('harvester.virtualMachine.detail.console.takeOver.label') }}
</button>
</div>
<h2
v-else
class="text-secondary"
>
{{ t('vncConsole.reconnecting.message') }}:{{ retryTimes }} of {{ maximumRetryTimes }}
</h2>
</div>
</div>
</template>
<style lang="scss" scoped>
.novnc-console {
position: relative;
box-sizing: border-box;
height: 100%;
padding: 8px;
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 {
font-size: 5rem;
}
.desert-landscape {
img {
max-width: 100%;
}
img {
max-width: 100%;
}
}
</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>
import { STEVE } from '@shell/config/types';
import { escapeHtml } from '@shell/utils/string';
import { allHash } from '@shell/utils/promise';
import KeyTable from '@novnc/novnc/core/input/keysym';
import * as KeyboardUtil from '@novnc/novnc/core/input/util';
import { HCI } from '../../types';
import NovncConsole from './NovncConsole';
import NovncConsoleItem from './NovncConsoleItem';
import 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 = {
ControlLeft: {
@@ -105,9 +116,7 @@ const F_KEYS = {
export default {
name: 'NovncConsoleWrapper',
components: {
NovncConsole, NovncConsoleItem, NovncConsoleCustomKeys
},
components: { NovncConsole, NovncConsoleItem },
async fetch() {
const _hash = { vmResource: this.$store.dispatch('harvester/find', { type: HCI.VM, id: this.value.id }) };
@@ -131,39 +140,21 @@ export default {
return {
keysRecord: [],
vmResource: {},
renderKeysModal: false,
currentUser: null,
hideCustomKeysBar: false,
settings: { ...DEFAULT_SETTINGS },
capabilities: {},
isFullscreen: false,
consoleStatus: {
status: 'connecting',
retryTimes: 0,
maximumRetryTimes: 0,
desktopName: '',
},
};
},
computed: {
savedShortcutKeys() {
const preference = this.$store.getters['management/all'](STEVE.PREFERENCE);
const preferedShortcutKeys = preference?.[0]?.data?.[PREFERED_SHORTCUT_KEYS];
let out = [];
if (!preference?.[0]?.data) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.hideCustomKeysBar = true;
return out;
}
if (!preferedShortcutKeys) {
return out;
}
try {
out = JSON.parse(preferedShortcutKeys);
} catch (err) {
this.$store.dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(this.value.metadata.name) }),
err,
}, { root: true });
}
return out;
canSendKeys() {
return this.consoleStatus.status === 'connected' && !this.settings.viewOnly;
},
isDown() {
@@ -192,13 +183,16 @@ export default {
...F_KEYS,
};
out.AltLeft.keys = { PrintScreen: FUNCTION_KEYS.PrintScreen, ...F_KEYS };
out.ControlLeft.keys = {
AltLeft: {
...Object.assign(SHORT_KEYS.AltLeft, {}),
keys: { Delete: FUNCTION_KEYS.Delete }
out.AltLeft = { ...SHORT_KEYS.AltLeft, keys: { PrintScreen: FUNCTION_KEYS.PrintScreen, ...F_KEYS } };
out.ControlLeft = {
...SHORT_KEYS.ControlLeft,
keys: {
AltLeft: {
...SHORT_KEYS.AltLeft,
keys: { Delete: FUNCTION_KEYS.Delete }
},
...NORMAL_KEYS,
},
...NORMAL_KEYS,
};
return out;
@@ -208,14 +202,53 @@ export default {
return !!this.vmResource?.actions?.softreboot;
},
preferredShortcutKeys() {
return (this.savedShortcutKeys || []).map((item) => {
return {
label: item.map((K) => K.key.charAt(0).toUpperCase() + K.key.slice(1)).join('+'),
value: item
};
});
rfbSettings() {
return {
viewOnly: this.settings.viewOnly,
scaleViewport: this.settings.viewMode === VIEW_MODE.SCALE,
zoomPercent: normalizeZoom(this.settings.zoomPercent),
resizeSession: false,
clipViewport: false,
dragViewport: false,
qualityLevel: this.settings.qualityLevel,
compressionLevel: this.settings.compressionLevel,
};
},
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: {
@@ -233,137 +266,221 @@ export default {
// Send function key, e.g. ALT + F
sendKeys() {
if (!this.canSendKeys || !this.$refs.novncConsole) {
return;
}
this.keysRecord.forEach((key) => {
this.$refs.novncConsole.sendKey(this.allKeys[key].value, key, true);
});
this.keysRecord.reverse().forEach((key) => {
[...this.keysRecord].reverse().forEach((key) => {
this.$refs.novncConsole.sendKey(this.allKeys[key].value, key, false);
});
this.$refs.popover.isOpen = false;
this.$refs.popover.hide();
this.keysRecord = [];
},
sendCustomKeys(keys) {
const keyList = [].concat(keys);
keyList.forEach((K) => {
this.$refs.novncConsole.sendKey(KeyboardUtil.getKeysym(K), KeyboardUtil.getKeycode(K), true);
});
keyList.reverse().forEach((K) => {
this.$refs.novncConsole.sendKey(KeyboardUtil.getKeysym(K), KeyboardUtil.getKeycode(K), false);
});
},
reconnect() {
this.$refs.novncConsole.reconnect();
},
loadSettings() {
try {
const saved = JSON.parse(window.localStorage.getItem(CONSOLE_SETTINGS_KEY) || '{}');
this.settings = {
viewMode: Object.values(VIEW_MODE).includes(saved?.viewMode) ? saved.viewMode : DEFAULT_SETTINGS.viewMode,
zoomPercent: normalizeZoom(saved?.zoomPercent ?? DEFAULT_SETTINGS.zoomPercent),
viewOnly: saved?.viewOnly === true,
qualityLevel: saved?.qualityLevel ?? DEFAULT_SETTINGS.qualityLevel,
compressionLevel: saved?.compressionLevel ?? DEFAULT_SETTINGS.compressionLevel,
};
} catch (err) {
this.settings = { ...DEFAULT_SETTINGS };
}
},
onStatusChanged(info) {
this.consoleStatus = info;
},
onCapabilities(capabilities) {
this.capabilities = capabilities;
},
ctrlAltDelete() {
if (this.canSendKeys) {
this.$refs.novncConsole?.ctrlAltDelete();
}
},
async toggleFullscreen() {
try {
if (document.fullscreenElement) {
await document.exitFullscreen();
} else {
await this.$refs.consoleContainer.requestFullscreen();
// Keyboard lock lets shortcuts such as Ctrl+W and F11 reach the guest.
await navigator.keyboard?.lock?.(['KeyW', 'F11']);
}
} catch (err) {
this.$store.dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(this.value.metadata.name) }),
err,
}, { root: true });
}
},
onFullscreenChange() {
this.isFullscreen = !!document.fullscreenElement;
if (!this.isFullscreen) {
navigator.keyboard?.unlock?.();
}
},
softReboot() {
this.vmResource.doSoftReboot();
},
showKeysModal() {
this.renderKeysModal = true;
},
hideKeysModal() {
this.renderKeysModal = false;
},
}
};
</script>
<template>
<div id="app">
<div class="vm-console">
<div
ref="consoleContainer"
class="vm-console"
>
<div class="combination-keys">
<v-dropdown
ref="popover"
placement="top"
trigger="click"
:container="false"
@auto-hide="keysRecord = []"
>
<button class="btn btn-sm bg-primary">
{{ t("harvester.virtualMachine.detail.console.shortcutKeys") }}
</button>
<template #popper>
<novnc-console-item
:items="keymap"
:path="keysRecord"
:pos="0"
@update="update"
@send-keys="sendKeys"
/>
</template>
</v-dropdown>
<button
v-if="hasSoftRebootAction"
class="btn btn-sm bg-primary"
@click="softReboot"
>
{{ t("harvester.action.softreboot") }}
</button>
<button
class="btn btn-sm bg-primary"
@click="reconnect"
>
{{ t("harvester.action.reconnect") }}
</button>
<v-dropdown
v-if="!hideCustomKeysBar"
ref="customKeyPopover"
placement="top"
trigger="click"
:container="false"
>
<button class="btn btn-sm bg-primary">
{{ t("harvester.virtualMachine.detail.console.customShortcutKeys") }}
</button>
<template #popper>
<div>
<button
class="btn btn-sm bg-primary"
@click="showKeysModal"
>
{{ t("harvester.virtualMachine.detail.console.management") }}
</button>
</div>
<hr>
<div
v-for="(keys, index) in preferredShortcutKeys"
:key="index"
<div class="toolbar-group">
<v-dropdown
ref="popover"
placement="top"
trigger="click"
:container="false"
@auto-hide="keysRecord = []"
>
<button
class="btn btn-sm bg-primary"
:disabled="!canSendKeys"
>
<button
class="btn btn-sm bg-primary"
@click="sendCustomKeys(keys.value)"
>
{{ keys.label }}
</button>
</div>
</template>
</v-dropdown>
{{ t("harvester.virtualMachine.detail.console.shortcutKeys") }}
</button>
<NovncConsoleCustomKeys
v-if="renderKeysModal"
:current-user="currentUser"
@close="hideKeysModal"
/>
<template #popper>
<novnc-console-item
:items="keymap"
:path="keysRecord"
:pos="0"
@update="update"
@send-keys="sendKeys"
/>
</template>
</v-dropdown>
<button
class="btn btn-sm bg-primary"
:disabled="!canSendKeys"
@click="ctrlAltDelete"
>
Ctrl+Alt+Del
</button>
<v-dropdown
placement="top"
trigger="click"
:container="false"
>
<button class="btn btn-sm bg-primary">
{{ t("harvester.virtualMachine.detail.console.settings.label") }}
</button>
<template #popper>
<div class="console-panel">
<label>{{ t('harvester.virtualMachine.detail.console.settings.viewMode.label') }}</label>
<div
v-for="option in viewModeOptions"
:key="option.value"
>
<label class="console-panel__option">
<input
v-model="settings.viewMode"
type="radio"
:value="option.value"
>
{{ option.label }}
</label>
</div>
<label
for="vnc-zoom"
class="console-panel__zoom-label"
>
<span>{{ t('harvester.virtualMachine.detail.console.settings.zoom') }}</span>
<span>{{ settings.zoomPercent }}%</span>
</label>
<input
id="vnc-zoom"
v-model.number="settings.zoomPercent"
class="console-panel__zoom"
type="range"
min="50"
max="200"
step="10"
:disabled="settings.viewMode === 'scale'"
:aria-valuetext="`${ settings.zoomPercent }%`"
>
<label class="console-panel__option">
<input
v-model="settings.viewOnly"
type="checkbox"
>
{{ t('harvester.virtualMachine.detail.console.settings.viewOnly') }}
</label>
</div>
</template>
</v-dropdown>
</div>
<div class="toolbar-group">
<span class="console-status">{{ statusLabel }}</span>
<button
v-if="hasSoftRebootAction"
class="btn btn-sm bg-primary"
@click="softReboot"
>
{{ t("harvester.action.softreboot") }}
</button>
<button
class="btn btn-sm bg-primary"
@click="reconnect"
>
{{ t("harvester.action.reconnect") }}
</button>
<button
class="btn btn-sm bg-primary"
@click="toggleFullscreen"
>
{{ isFullscreen ? t('harvester.virtualMachine.detail.console.fullscreen.exit') : t('harvester.virtualMachine.detail.console.fullscreen.enter') }}
</button>
</div>
</div>
<NovncConsole
v-if="url && !isDown"
ref="novncConsole"
:url="url"
:settings="rfbSettings"
@status-changed="onStatusChanged"
@capabilities="onCapabilities"
/>
<p v-if="isDown">
{{ t("harvester.virtualMachine.detail.console.down") }}
@@ -374,14 +491,56 @@ export default {
<style lang="scss" scoped>
.vm-console {
height: 100%;
height: 100vh;
height: 100dvh;
display: grid;
grid-template-rows: 30px auto;
grid-template-rows: 32px minmax(0, 1fr);
}
.combination-keys {
display: flex;
align-items: center;
justify-content: space-between;
background: rgb(40, 40, 40);
}
.toolbar-group {
display: flex;
align-items: center;
gap: 4px;
}
.console-status {
color: #fff;
font-size: 12px;
margin-right: 8px;
}
.console-panel {
display: flex;
flex-direction: column;
width: 260px;
padding: 4px;
&__zoom-label {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-variant-numeric: tabular-nums;
}
&__zoom {
width: 100%;
margin: 4px 0 8px;
}
&__option {
display: flex;
align-items: center;
gap: 6px;
margin: 2px 0;
}
}
</style>
<style lang="scss">
@@ -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';
const DEFAULT_VOLUME_MODE = VOLUME_MODE.FILE_SYSTEM;
const DEFAULT_ACCESS_MODES = [ACCESS_MODE.READ_WRITE_MANY];
const DEFAULT_ACCESS_MODE = ACCESS_MODE?.READ_WRITE_MANY ?? 'ReadWriteMany';
const props = defineProps({
providerName: { type: String, default: '' },
@@ -268,9 +268,9 @@ const buildStorageEntries = () => {
capacity: 0,
target: '',
volumeMode: DEFAULT_VOLUME_MODE,
accessModes: [...DEFAULT_ACCESS_MODES],
accessModes: [DEFAULT_ACCESS_MODE],
inheritedVolumeMode: DEFAULT_VOLUME_MODE,
inheritedAccessModes: [...DEFAULT_ACCESS_MODES],
inheritedAccessModes: [DEFAULT_ACCESS_MODE],
inheritedFromProvider: false,
overridden: false,
usedBy: [],
@@ -310,9 +310,9 @@ const buildStorageEntriesFromProvider = (datastoresData) => {
capacity: ds.capacity || 0,
target: '',
volumeMode: DEFAULT_VOLUME_MODE,
accessModes: [...DEFAULT_ACCESS_MODES],
accessModes: [DEFAULT_ACCESS_MODE],
inheritedVolumeMode: DEFAULT_VOLUME_MODE,
inheritedAccessModes: [...DEFAULT_ACCESS_MODES],
inheritedAccessModes: [DEFAULT_ACCESS_MODE],
inheritedFromProvider: false,
overridden: false,
usedBy: [],
@@ -8,16 +8,22 @@ import { useI18n } from '@shell/composables/useI18n';
import { VOLUME_MODE, ACCESS_MODE } from '../../config/types';
const VOLUME_MODE_OPTIONS = [VOLUME_MODE.FILE_SYSTEM, VOLUME_MODE.BLOCK];
const ACCESS_MODE_OPTIONS = [ACCESS_MODE.READ_WRITE_ONCE, ACCESS_MODE.READ_WRITE_MANY, ACCESS_MODE.READ_ONLY_MANY];
const {
READ_WRITE_ONCE = 'ReadWriteOnce',
READ_ONLY_MANY = 'ReadOnlyMany',
READ_WRITE_MANY = 'ReadWriteMany',
READ_WRITE_ONCE_POD = 'ReadWriteOncePod'
} = ACCESS_MODE || {};
const ACCESS_MODE_OPTIONS = [READ_WRITE_ONCE, READ_WRITE_MANY, READ_ONLY_MANY, READ_WRITE_ONCE_POD];
const props = defineProps({
storageClassName: { type: String, default: '' },
providerName: { type: String, default: '' },
showInherited: { type: Boolean, default: false },
volumeMode: { type: String, default: VOLUME_MODE.FILE_SYSTEM },
accessModes: { type: Array, default: () => [ACCESS_MODE.READ_WRITE_MANY] },
accessModes: { type: Array, default: () => ['ReadWriteMany'] },
inheritedVolumeMode: { type: String, default: VOLUME_MODE.FILE_SYSTEM },
inheritedAccessModes: { type: Array, default: () => [ACCESS_MODE.READ_WRITE_MANY] },
inheritedAccessModes: { type: Array, default: () => ['ReadWriteMany'] },
});
const emit = defineEmits(['apply', 'close']);
@@ -26,7 +32,7 @@ const store = useStore();
const { t } = useI18n(store);
const localVolumeMode = ref(props.volumeMode || VOLUME_MODE.FILE_SYSTEM);
const localAccessMode = ref(props.accessModes?.[0] || ACCESS_MODE.READ_WRITE_MANY);
const localAccessMode = ref(props.accessModes?.[0] || READ_WRITE_MANY);
const volumeModeOptions = VOLUME_MODE_OPTIONS.map((value) => ({ label: value, value }));
const accessModeOptions = ACCESS_MODE_OPTIONS.map((value) => ({ label: value, value }));
@@ -42,7 +48,7 @@ const apply = () => {
// Repopulate the dropdowns with the provider default values; the user then applies.
const reset = () => {
localVolumeMode.value = props.inheritedVolumeMode || VOLUME_MODE.FILE_SYSTEM;
localAccessMode.value = props.inheritedAccessModes?.[0] || ACCESS_MODE.READ_WRITE_MANY;
localAccessMode.value = props.inheritedAccessModes?.[0] || READ_WRITE_MANY;
};
const cancel = () => {
+3 -1
View File
@@ -111,15 +111,17 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { kind: 'number', featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: {
kind: 'boolean',
experimental: true,
preview: true,
featureFlag: 'longhornV2LVMSupport'
},
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_HUGEPAGE_ENABLED]: {
kind: 'boolean',
preview: true,
featureFlag: 'longhornV2HugepageSettings'
},
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_MEMORY_SIZE]: {
kind: 'number',
preview: true,
featureFlag: 'longhornV2HugepageSettings'
},
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
+7
View File
@@ -15,6 +15,13 @@ export const VOLUME_MODE = {
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 = {
IPv4: 'IPv4',
IPv6: 'IPv6',
@@ -390,14 +390,17 @@ export default {
:vmi-resource="vmi"
:vmim-resource="vmim"
/>
<DashboardMetrics
<div
v-if="showVmMetrics && liveMigrationProgressEnabled"
:detail-url="VM_MIGRATION_DETAIL_URL"
graph-height="640px"
:has-summary-and-detail="false"
:vars="graphVars"
class="mb-30"
/>
class="migration-metrics-scroll mb-30"
>
<DashboardMetrics
:detail-url="VM_MIGRATION_DETAIL_URL"
graph-height="960px"
:has-summary-and-detail="false"
:vars="graphVars"
/>
</div>
<Events
v-if="liveMigrationProgressEnabled"
:resource="vmi"
@@ -436,3 +439,10 @@ export default {
</Tabbed>
</div>
</template>
<style lang="scss" scoped>
.migration-metrics-scroll {
max-height: 640px;
overflow-y: auto;
}
</style>
@@ -1,5 +1,5 @@
<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 ArrayList from '@shell/components/form/ArrayList';
import { Checkbox } from '@components/Form/Checkbox';
@@ -10,6 +10,10 @@ import { HCI_SETTING } from '../../config/settings';
import { allHash } from '@shell/utils/promise';
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 {
name: 'CDISettings',
@@ -90,10 +94,10 @@ export default {
accessModeOptions() {
return [
{ label: 'ReadWriteOnce', value: 'ReadWriteOnce' },
{ label: 'ReadOnlyMany', value: 'ReadOnlyMany' },
{ label: 'ReadWriteMany', value: 'ReadWriteMany' },
{ label: 'ReadWriteOncePod', value: 'ReadWriteOncePod' },
{ label: READ_WRITE_ONCE, value: READ_WRITE_ONCE },
{ label: READ_ONLY_MANY, value: READ_ONLY_MANY },
{ label: READ_WRITE_MANY, value: READ_WRITE_MANY },
{ 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 { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim';
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';
const { READ_WRITE_MANY, READ_WRITE_ONCE, READ_ONLY_MANY } = ACCESS_MODE;
export default {
name: 'HarvesterVolume',
@@ -83,7 +85,7 @@ export default {
if (this.mode === _CREATE) {
// default volumeMode to 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;
@@ -160,7 +162,7 @@ export default {
},
accessModeOptions() {
return ['ReadWriteOnce', 'ReadWriteMany', 'ReadOnlyMany'];
return [READ_WRITE_ONCE, READ_WRITE_MANY, READ_ONLY_MANY];
},
imageOption() {
@@ -355,7 +357,7 @@ export default {
getAccessMode() {
if (!this.longhornV2LVMSupport) {
return ['ReadWriteMany'];
return [READ_WRITE_MANY];
}
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);
}
return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany'];
return readWriteOnce ? [READ_WRITE_ONCE] : [READ_WRITE_MANY];
},
buildDataVolumeObj() {
const storage = {
@@ -246,4 +246,27 @@ export default {
.group-actions {
display: inline;
}
// Match the enable/disable passthrough bulk-action buttons height to the .btn (40px) on the right.
:deep(.bulk .rc-button.btn-medium.bulk-action:not(.btn-sm)) {
min-height: 40px;
}
// Make the collapsed "Actions" dropdown button 40px too.
:deep(.bulk .rc-button.btn-medium.bulk-actions-dropdown:not(.btn-sm)) {
min-height: 40px;
}
// Lay the bulk row out with flex so the "N selected" label can wrap below the buttons.
:deep(.bulk) {
display: flex;
flex-wrap: wrap;
align-items: center;
}
:deep(.bulk .action-availability) {
flex-basis: 100%;
margin-left: 0;
margin-top: 6px;
}
</style>
@@ -5,6 +5,7 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
import Banner from '@components/Banner/Banner.vue';
import remove from 'lodash/remove';
import { set } from '@shell/utils/object';
import { uniq } from '@shell/utils/array';
import { HCI } from '../../../types';
import DeviceList from './DeviceList';
import CompatibilityMatrix from '../CompatibilityMatrix';
@@ -60,7 +61,15 @@ export default {
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
});
vmDevices.forEach(({ name }) => {
// When off, read from spec; otherwise the spec name is a placeholder ('provisioned'),
// so read the real allocated device names from the deviceAllocationDetails annotation.
const hostDeviceNames = this.vm.isOff ? [
...vmDevices.map(({ name }) => name),
] : [
...Object.values(this.vm?.provisionedHostDevices || {}).reduce((acc, devices) => [...acc, ...devices], []),
];
uniq(hostDeviceNames).forEach((name) => {
if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
selectedDevices.push(name);
}
@@ -16,10 +16,12 @@ import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
import { VOLUME_HOTPLUG_ACTION, SOURCE_TYPE } from '../../../config/harvester-map';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
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 { EMPTY_IMAGE } from '../../../utils/vm';
const { READ_WRITE_MANY } = ACCESS_MODE;
export default {
emits: ['update:value'],
@@ -69,7 +71,7 @@ export default {
customAccessMode: {
type: String,
default: 'ReadWriteMany'
default: READ_WRITE_MANY
},
resourceType: {
@@ -14,7 +14,9 @@ import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storagec
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
import { GIBIBYTE } from '../../../../utils/unit';
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 {
name: 'HarvesterEditVolume',
@@ -205,7 +207,7 @@ export default {
getAccessMode(storageClassName) {
if (!this.longhornV2LVMSupport) {
return 'ReadWriteMany';
return READ_WRITE_MANY;
}
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);
}
return readWriteOnce ? 'ReadWriteOnce' : 'ReadWriteMany';
return readWriteOnce ? READ_WRITE_ONCE : READ_WRITE_MANY;
},
update() {
+36 -10
View File
@@ -953,15 +953,33 @@ harvester:
console:
down: This virtual machine is down. Please start it to access its console.
shortcutKeys: Shortcut Keys
customShortcutKeys: Custom Shortcut Keys
management: Management Shortcut Keys
record:
start: Record
recording: Recording
stop: Stop Recording
tips: Press the record button to capture your keyboard inputs.
send: Send
preferredKeys: Preferred Custom Shortcut Keys
settings:
label: Settings
viewMode:
label: Display
scale: Scale the display to fit the window
resize: Resize the virtual machine display to the window
none: Manual zoom
zoom: Zoom
clipViewport: Clip to the window instead of showing scrollbars
viewOnly: View only
quality: JPEG quality
compression: Compression level
reset: Reset to defaults
fullscreen:
enter: Full Screen
exit: Exit Full Screen
takeOver:
label: Take Over
confirm: Take over this console? Any existing VNC session for this virtual machine will be disconnected.
status:
connecting: Connecting
connected: Connected
disconnected: Disconnected
failed: Connection failed
reconnecting: 'Reconnecting {retryTimes} of {maximumRetryTimes}'
credentialsRequired: This console requires credentials that the dashboard cannot provide.
unavailable: Unable to connect. Another VNC session may be active, or the console may be unavailable.
terminationGracePeriodSeconds:
label: Termination Grace Period
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."
advancedSettings:
experimental: 'Experimental features allow users to test and evaluate early-access functionality prior to official supported releases'
tags:
modified:
label: Modified
experimental:
label: Experimental
description: 'Experimental: The feature is incomplete but its essential functionality is operational. The feature is not recommended for use in production environments.'
preview:
label: Technical Preview
description: 'Technical Preview: The feature is almost complete and its functionality is not expected to change significantly. Explore the feature extensively before using in production environments.'
descriptions:
'harv-vlan': Default Network Interface name of the VLAN network.
'harv-backup-target': Custom backup target to store virtual machine backups.
+6 -4
View File
@@ -25,7 +25,9 @@ import { HCI } from '../../types';
import { parseVolumeClaimTemplates, EMPTY_IMAGE } from '../../utils/vm';
import impl, { QGA_JSON, USB_TABLET } from './impl';
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';
@@ -276,7 +278,7 @@ export default {
},
customAccessMode() {
return this.storageClassSetting.accessModes || 'ReadWriteMany';
return this.storageClassSetting.accessModes || READ_WRITE_MANY;
},
isWindows() {
@@ -504,7 +506,7 @@ export default {
id: randomStr(5),
source: SOURCE_TYPE.IMAGE,
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,
volumeName: '',
size,
@@ -570,7 +572,7 @@ export default {
const pvcResource = allPVCs.find( (O) => O.id === `${ namespace }/${ volume?.persistentVolumeClaim?.claimName }`);
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';
storageClassName = pvcResource?.spec?.storageClassName;
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 { NAMESPACE } from '@shell/config/types';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
import { FORKLIFT_NAMESPACE } from '@pkg/harvester/config/harvester-map';
import { HCI } from '../../types';
const OBSCURE_NAMESPACE_PREFIX = [
@@ -93,6 +94,10 @@ export default class HciNamespace extends namespace {
}
get isSystem() {
if (this.metadata?.name === FORKLIFT_NAMESPACE) {
return false;
}
const systemNamespaces = ['fleet-default'];
if (systemNamespaces.includes(this.metadata.name)) {
@@ -1315,6 +1315,18 @@ export default class VirtVm extends HarvesterResource {
}
}
get provisionedHostDevices() {
// Rancher-provisioned VMs store hostDevices.name as a placeholder ('provisioned'); the
// real allocated PCI device names live in this annotation, keyed by resourceName.
try {
const deviceAllocationDetails = JSON.parse(this.metadata?.annotations[HCI_ANNOTATIONS.VM_DEVICE_ALLOCATION_DETAILS] || '{}');
return deviceAllocationDetails?.hostdevices || {};
} catch (error) {
return {};
}
}
get schedulingVMBackupFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('schedulingVMBackup');
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "harvester",
"description": "Rancher UI Extension for Harvester",
"version": "1.9.0-rc5",
"version": "1.10.0-dev",
"private": false,
"rancher": {
"annotations": {
@@ -1,10 +1,18 @@
<script>
import { defineAsyncComponent } from 'vue';
import Loading from '@shell/components/Loading';
import TabTitle from '@shell/components/TabTitle';
import { HCI } from '../../../../../types';
import NovncConsoleWrapper from '../../../../../components/novnc/NovncConsoleWrapper.vue';
export default {
components: { NovncConsoleWrapper, Loading },
components: {
NovncConsoleWrapper: defineAsyncComponent({
loader: () => import('../../../../../components/novnc/NovncConsoleWrapper.vue'),
loadingComponent: Loading,
}),
Loading,
TabTitle
},
async fetch() {
this.rows = await this.$store.dispatch('harvester/findAll', { type: HCI.VMI });
@@ -28,17 +36,21 @@ export default {
mounted() {
window.addEventListener('beforeunload', () => {
this.$refs.console.close();
this.$refs.console?.close();
});
},
head() {
return { title: this.vmi?.metadata?.name };
},
};
</script>
<template>
<TabTitle
v-if="vmi?.metadata?.name"
:breadcrumb="false"
:show-child="false"
>
{{ vmi.metadata.name }}
</TabTitle>
<Loading v-if="$fetchState.pending" />
<NovncConsoleWrapper
v-else
+24 -1
View File
@@ -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
View File
@@ -1,6 +1,26 @@
const path = require('path');
const config = require('@rancher/shell/vue.config');
module.exports = config(__dirname, {
const baseConfig = config(__dirname, {
excludes: [],
// excludes: ['harvester']
});
const baseConfigureWebpack = baseConfig.configureWebpack;
module.exports = {
...baseConfig,
configureWebpack(webpackConfig) {
if (typeof baseConfigureWebpack === 'function') {
baseConfigureWebpack(webpackConfig);
}
// noVNC >= 1.7 only publishes `./core/rfb.js` in its `exports` map, so resolve the
// package by path to keep the keysym/input helpers importable.
webpackConfig.resolve.alias['@novnc/novnc'] = path.resolve(__dirname, 'node_modules/@novnc/novnc');
// noVNC 1.7 uses top-level await for WebCodecs detection.
webpackConfig.output = { ...webpackConfig.output, environment: { ...webpackConfig.output?.environment, asyncFunction: true } };
}
};
+1587 -1393
View File
File diff suppressed because it is too large Load Diff