Compare commits

...
413 Commits
Author SHA1 Message Date
mergify[bot]andTim Liou bbc415a2b4 chore: bump version to 1.8.0-rc4 (#778) (#779)
(cherry picked from commit 9d698b1230)

Signed-off-by: Tim Liou <tim.liou@suse.com>
Co-authored-by: Tim Liou <tim.liou@suse.com>
2026-03-27 16:42:53 +08:00
c12d4b5fba feat: support advance option and creation from DataVolume (#776) (#777)
* feat: support advance option and creation from DataVolume

    - advance option would let user change the accessMode/volumeMode
    - creation from DataVolume could supports various scenario with
      3rd-party storage



* feat: add data migration action on volume page



* refactor: use show advanced options link instead of checkbox



* feat: add feature flag



* feat: add feature flag for dataMigration action



---------




(cherry picked from commit 566e79eda5)

Signed-off-by: Vicente Cheng <freeze.bilsted@gmail.com>
Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-26 17:33:01 +08:00
mergify[bot]andAndy Lee dc91be11f9 chore: bump version to 1.8.0-rc3 (#774) (#775)
(cherry picked from commit 42ddcfc1fe)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-26 12:35:12 +08:00
mergify[bot]andAndy Lee e5b72499b5 fix: use default value got undefined value in payload (#772) (#773)
(cherry picked from commit ad3decf71f)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-25 17:12:51 +08:00
mergify[bot]andAndy Lee e66037b007 ci: remove the single quota for use commit hash (#767) (#771)
(cherry picked from commit 8083a41df0)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-25 14:13:08 +08:00
mergify[bot]andfreeze c676cad057 chore: pin GH Actions to commit sha (#765) (#766)
(cherry picked from commit 62801b3b13)

Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com>
2026-03-25 14:08:49 +08:00
mergify[bot]andAndy Lee 254abd4648 ci: add CODEOWNERS file (#768) (#769)
(cherry picked from commit 46b860260a)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-25 13:42:42 +08:00
renovate[bot] 1df1d3534a deps: update dependency yaml to v2.8.3 (#757)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-22 13:06:18 +00:00
mergify[bot]andAndy Lee ff1e6e6e7f docs: update install instruction doc link (#753) (#754)
(cherry picked from commit 97e93dba0b)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-20 16:53:54 +08:00
mergify[bot]andAndy Lee 622242e4a5 refactor: change rwxNetwork setting to kind json (#751) (#752)
(cherry picked from commit 9a8a709e56)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-20 14:48:49 +08:00
mergify[bot]andAndy Lee d8ea9be174 feat: introduce rwxNetwork setting (#746) (#750)
* feat: add rwxNetwork setting



* fix: network payload



---------


(cherry picked from commit d1949641a7)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-19 16:19:44 +08:00
mergify[bot]andfreeze f8a479cfcf feat: add storage migration operation (#724) (#749)
- add storage migration
    - add cancel storage migration


(cherry picked from commit 9c9f59c939)

Signed-off-by: Vicente Cheng <freeze.bilsted@gmail.com>
Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com>
2026-03-19 11:53:24 +08:00
mergify[bot]andAndy Lee 6d5e584113 chore: bump to v1.8.0-rc2 (#747) (#748)
(cherry picked from commit ccc14c7fb9)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-19 11:31:07 +08:00
mergify[bot]andAndy Lee a9eb04195e feat: introduce instance-manager-resources setting (#744) (#745)
(cherry picked from commit 2ba471907e)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-18 17:21:28 +08:00
mergify[bot]andAndy Lee 0441c1f6e3 refactor: remove using resourceName to determine is vGPU device (#741) (#743)
(cherry picked from commit 5aea476f64)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-18 17:16:28 +08:00
mergify[bot]andAndy Lee dde58269d5 fix: typo (#740) (#742)
(cherry picked from commit 519c7d9f1f)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-17 16:33:37 +08:00
mergify[bot]andTim Liou b8111f0ad7 fix: reword the error message to focus on bootable volume (#736) (#738)
(cherry picked from commit a9c392c13f)

Signed-off-by: Tim Liou <tim.liou@suse.com>
Co-authored-by: Tim Liou <tim.liou@suse.com>
2026-03-12 17:46:08 +08:00
mergify[bot]andTim Liou 6d627f82e9 fix: container disks don't need volumeClaimTemplates but volumes (#735) (#737)
(cherry picked from commit 888ec7a50f)

Signed-off-by: Tim Liou <tim.liou@suse.com>
Co-authored-by: Tim Liou <tim.liou@suse.com>
2026-03-12 17:45:40 +08:00
mergify[bot]andAndy Lee cfc7a76fe7 feat: add vGPU filter button and hide the enable/disable passthrough in PCIDevice page (#729) (#732)
* feat: add another filter button



* feat: add feature falg to hide vGPU enable/disable actions in PCIDevices page



* refactor: update with conditionally rendering



---------


(cherry picked from commit 23344e0c07)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-11 15:39:41 +08:00
mergify[bot]andPo Han Huang 71d3067354 feat: ensure the state is pending when perform cloning the efi (#730) (#734)
* feat: ensure the state is pending when perform cloning the efi



* feat: define harvesterhci.io/clone-backend-storage-status in labels-annotations.js



---------


(cherry picked from commit a2486a7d38)

Signed-off-by: pohanhuang <pohan.huang@suse.com>
Co-authored-by: Po Han Huang <hhcs9527@gmail.com>
2026-03-11 15:39:01 +08:00
mergify[bot]andAndy Lee 9ecc372009 chore: bump to v1.8.0-rc1 (#731) (#733)
(cherry picked from commit df3d249923)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-11 15:34:55 +08:00
Andy Lee 62b80b3cec feat: add Insecure Skip TLS Verify checkbox in cluster-registration-url setting (#716)
* feat: add Insecure Skip TLS Verify checkbox

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

* refactor: set insecureSkipTLSVerify default to false

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

* fix: conflict

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

* refactor: remove unneeded change

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

* fix: get the feature flag in data()

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

* refactor: make data logic simpler

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

* refactor: put tip in info banner

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-09 10:51:20 +08:00
renovate[bot] 1cf94ee550 deps: update dependency @types/node to v20.19.37 (#726)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-08 14:15:49 +08:00
renovate[bot] dfaa9fbe33 deps: update dependency elkjs to ^0.11.0 (#727)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-08 14:15:01 +08:00
Andy Lee 94b1c24479 chore: bump @rancher/shell to v3.0.9-rc.6 (#712)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-08 14:13:46 +08:00
Jack Yu e0b2b9ec57 feat: add support-bundle-file-name setting (#725)
* feat: support customizing support bundle file name

Signed-off-by: Jack Yu <jack.yu@suse.com>

* feat: add supportBundleFileNameSetting feature flag

Signed-off-by: Jack Yu <jack.yu@suse.com>

---------

Signed-off-by: Jack Yu <jack.yu@suse.com>
2026-03-06 15:51:20 +08:00
Andy Lee 06b38a0f99 fix: import sources page header (#723)
* fix: the import sources page header

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

* refactor: update VMware term

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-05 16:36:15 +08:00
Yiya Chen 4be3634c82 feat: add VPC network topology in detail page (#721)
* feat: add topology

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* feat: add provider info

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: remove comments

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* fix: exclude default network

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* feat: add VPC peering

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: remove regex

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: adjust row height

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* feat: introduce auto layout

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2026-03-04 11:47:00 +08:00
Andy Lee de103ff91e fix: missing Download Logs button in Rancher integration mode (#715)
* fix: missing Download Logs button in Rancher integration mode

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

* refactor: add comment

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-02 16:47:20 +08:00
Andy Lee 37a91601c9 feat: add vmMigrationTimeout in vm-force-reset-policy setting (#718)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-02 11:10:52 +08:00
renovate[bot] 9794671ccd deps: update patch digest dependencies (#719)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 11:10:22 +08:00
Volker Theile 337f9186f1 fix(vmimport): Rename list pages according to their URL path (#717)
See https://github.com/harvester/harvester-ui-extension/pull/697#issuecomment-3957918146

Related to: https://github.com/harvester/harvester/issues/4663

Signed-off-by: Volker Theile <vtheile@suse.com>
2026-02-26 16:32:40 +08:00
Volker Theile 7c187e894d fix(vmimport): Add missing list page for ovasource (#714)
... and several small improvements.

Signed-off-by: Volker Theile <vtheile@suse.com>
2026-02-25 16:47:15 +08:00
Gaurav Mehta c2df13ad73 fix: changed logic to use power status from underlying inventory (#707)
Signed-off-by: Gaurav Mehta <gaurav.mehta@suse.com>
2026-02-23 20:08:54 +11:00
Tim Liou 93f027a57c fix: shall update hotpluggable when change to a non-iso image (#713)
Signed-off-by: Tim Liou <tim.liou@suse.com>
2026-02-23 17:08:33 +08:00
renovate[bot] da83f04b6c deps: update dependency qs to v6.15.0 (#710)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 11:50:05 +08:00
renovate[bot] a97cb08e3f deps: update patch digest dependencies (#705)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 14:22:19 +08:00
Tim Liou 3fdc9f03a3 feat: add cdrom hotplug volume (#703)
Signed-off-by: Tim Liou <tim.liou@suse.com>
2026-02-13 16:33:50 +08:00
Jack Yu c8a613874a feat: add cpu model selection (#702)
Signed-off-by: Jack Yu <jack.yu@suse.com>
2026-02-13 15:40:07 +08:00
Andy Lee 2db7ee7397 feat: show notification if there is VM pending restart (#700)
* feat: show notification if there is VM pending restart

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

* refactor: update based on comments

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

* refactor: calculate count from vm names

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-02-03 16:21:20 +08:00
Andy Lee 8b9b5b41b7 style: set max-height for namespace dropdown menu (#693)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-30 16:43:15 +08:00
Andy Lee 77599900b5 feat: add resume button for upgrade paused node (#698)
* feat: add nodeUpgradeOption setting

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

* feat: add resume button when node paused

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

* feat: add feature flag in v1.7.0

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-30 15:12:00 +08:00
Andy Lee 473c1ba355 fix: this.action typo (#696)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-28 15:49:07 +08:00
Andy Lee 708a95b67b chore: restrict v1.8.0 ui extension to run on rancher 2.14 or above (#695)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-27 22:34:47 +08:00
Andy Lee fecb3de0cf chore: update @rancher/shell to v3.0.9-rc.1 (#694)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-26 15:44:14 +08:00
Jack Yu 0781bde188 feat: add warning message when disabling a device that have not been detached in the backend (#675)
* feat: add wanring message when disabling a device that haven not been detached in the backend

Signed-off-by: Jack Yu <jack.yu@suse.com>

* fix: remove unused en-us key

Signed-off-by: Jack Yu <jack.yu@suse.com>

---------

Signed-off-by: Jack Yu <jack.yu@suse.com>
2026-01-26 14:07:28 +08:00
Kuan-Po Tseng 0647600e88 fix: use longhorn-static for upgrade vmimage (#690)
Signed-off-by: Cooper Tseng <cooper.tseng@suse.com>
2026-01-22 09:46:20 +08:00
Andy Lee 99dbba7958 fix: remove isCordoned condition (#689)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-21 17:26:53 +08:00
Dominik WombacherandVolker Theile 3dcc50980b feat: Introduce VM Import UI flow pages (#642)
* feat(vmimport): First working side nav attempt

Add vmimport entries when the related resource actually exists aka addon was enabled

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): improved version that uses 'store.watch' instead of polling

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): further tuning of dynamic side navi load/unload

Code formatting and commits. Also safeguard if something is wrong with the store

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* refactor(vmimport): separate vmimport side nav entries from dynamic logic

function registerAddonSideNav introduced in utils/dynamic-nav.js

Decouples vmimport side nav entries from the hide/unhide based on addon status logic

Makes it reusable in the UI with other AddOns in the future

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): add custom headers for HCI.VMIMPORT

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): add custom headers for HCI.VMIMPORT_SOURCE_V

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): add custom headers for HCI.VMIMPORT_SOURCE_O

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): array instead string passed to configureType

Caused routing issues for CRUD operations

Labels moved to labelTypes section to follow standards

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): registerAddonSideNav improved and refactored

Clear comments, code refactoring, additional checks and validations

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): show correct status for virtualmachineimport

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): custom list components with ns grouping

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): 404 on refresh and missing menu entry

Restores virtualType definitions to register routes synchronously,
preventing 404 errors during page reload.

Updates dynamic-nav to force-fetch addon data if missing, fixing
hidden menu issues on direct page access.

Restores explicit label keys for virtualTypes to ensure correct
naming in the side navigation.

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): add edit form for VirtualMachineImport resource

Adds a UI form for VirtualMachineImport to replace manual YAML editing.
The form fetches VmwareSource and OpenstackSource objects for the
source selection dropdown.

It validates the VM name against RFC-1123 rules and filters out
internal storage classes. Users can also configure network mappings
via a dynamic list.

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): add edit form for VmwareSource

Adds a UI to configure VmwareSource resources including the endpoint
and datacenter fields.

For authentication, users can either select an existing Secret or
enter a username and password directly. The form handles creating
the required Kubernetes Secret in the background when new credentials
are provided.

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): add edit form for OpenstackSource

Custom edit form for OpenstackSource resource. Creates new secret
or lets users select existing secrets. Support all fields the CRD has.

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* chore(vmimport): vmware source default endpoint and datacenter renamed

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): add edit form for OvaSource

OvaSource (new in harvester / vm-import-controller v1.7.0).
Imports VMs from an OVA file using via HTTP or HTTPS.

The form supports URL configuration and optional Basic Auth using a
username and password. Users can also provide an optional CA Certificate
for HTTPS verification and configure advanced HTTP timeout settings.

VirtualMachineImport edit page to updated to include OvaSource in
the source dropdown.

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* chore(vmimport): align tab names on openstacksource

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): import { TextArea } from '@components/Form/TextArea' not found

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): 'Destination Network' don't list all networks

Online listed the 'mgmt' Network. Adjust to read all Virtual Machine Networks.

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): rename side-nav entry to 'Virtual Machine Imports'

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): OvaSource Auth tab throws error selecting existing secret

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): Add missing caCert input field to vmware source

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* refactor(vmimport): use 'LabeledInput' instead of 'TextAreaAutoGrow' for cacert fields

Changing the type allows labels to show up in the UI

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* refactor(vmimport): Move vars into types files and reference them

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* refactor(vmimport): Use 'currentProduct' value instead of hardcoded 'harvester' string

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* refactor(vmimport): shorten 'selectedOption.raw' usage

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* refactor(vmimport): Checks to make splice() usage more robust

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* refactor(vmimport): re-use existing rfc1123 val function

Move rfc1123 validation error message to l10n/en-us.yaml

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): var name typo in vmi edit rfc1123 check

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): vmi use 'FormValidation' and l10n for labels

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): oss use 'FormValidation' and l10n for labels

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): ovas use 'FormValidation' and l10n for labels

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): vms use 'FormValidation' and l10n for labels

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* refactor(vmimport): Display error message at the top of the page

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

---------

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>
Signed-off-by: Volker Theile <vtheile@suse.com>
Co-authored-by: Volker Theile <vtheile@suse.com>
2026-01-21 15:30:55 +08:00
renovate[bot] ee1c3de188 deps: update patch digest dependencies (#686)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 11:22:38 +08:00
Kuan-Po Tseng 915559962a fix: use file as field name instead of chunk in cdi vmimage upload (#684)
Signed-off-by: Cooper Tseng <cooper.tseng@suse.com>
2026-01-20 10:57:48 +08:00
Andy Lee b1b1a31c04 fix: do not set cpu.maxSockets on UI (#674)
* fix: do not set cpu.maxSockets for ARM clusters

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

* fix: remove maxSocket to fix bug on ARM cluster

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-16 17:09:13 +08:00
Guilherme Macedo 7f52562d22 ci: add FOSSA scanning workflow (#683)
Signed-off-by: Guilherme Macedo <guilherme@gmacedo.com>
2026-01-16 15:42:33 +08:00
Andy Lee b140c05697 ci: only do package auto update for release-harvester-v1.7 ~ v1.9 (#682)
* ci: disable package auto update for release-harvester-v1.0/v1.5

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

* refactor: only match release v1.7 ~ v1.9 branches

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

* refactor: change basebranches

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-14 15:51:15 +08:00
Andy Lee ad9fef63c0 chore: update copyright year to 2026 (#681)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-13 15:15:50 +08:00
renovate[bot] 786e271ac6 deps: update patch digest dependencies (#676)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 22:28:33 +08:00
freeze c169853e5a chore: bump version to v1.8.0-dev (#672)
Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2026-01-04 23:19:49 +08:00
Tim Liou 1352246e1e fix: drop mac-address annotation from vm template to prevent MAC address reusing (#663)
related-to: harvester/harvester#9788
related-to: harvester/harvester#9798

Signed-off-by: Tim Liou <tim.liou@suse.com>
2026-01-02 17:24:57 +08:00
renovate[bot] 49374bb18a deps: update dependency qs to v6.14.1 [security] (#664)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-02 16:15:54 +08:00
Andy Lee fe3a12e28c docs: add README.md in pkg/harvester (#661)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-12-23 16:23:01 +08:00
Andy Lee a86302c9d5 chore: bump version to v1.7.0 (#658) 2025-12-22 13:23:54 +08:00
freeze 5fe7e13fcd chore: bump version to v1.7.0-rc7 (#656)
Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2025-12-16 20:19:22 +08:00
renovate[bot] c079984047 deps: update dependency @types/node to v20.19.27 (#651)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 14:22:49 +08:00
Andy Lee 5769588633 chore: bump version to v1.7.0-rc6 (#649)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-12-11 16:47:48 +08:00
Andy Lee b29950f99c fix: failed to create multiple VMs (#647)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-12-11 16:12:39 +08:00
Andy Lee 6c27a46274 fix: do not inherit template secret when creating new VM (#643)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-12-10 17:12:56 +08:00
Andy Lee b03fffbc30 feat: read addon displayname from label and add descheduler description (#644)
* refactor: display addon name from addon.harvesterhci.io/displayName label

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

* refactor: add descheduler description

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-12-10 15:13:21 +08:00
Andy Lee 5b668a176c feat: integrate cron editor in vm schedule edit page (#635)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-12-10 08:02:58 +08:00
renovate[bot] b4019a2c86 deps: update patch digest dependencies (#630)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 15:08:40 +08:00
freeze 416098ffd8 chore: bump to v1.7.0-rc5 (#636)
Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2025-12-04 22:40:17 +08:00
Andy Lee 3d7b96d86d chore: bump to v1.7.0-rc4 (#621)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-27 17:45:36 +08:00
Caio Torres 0b37467f76 fix: create new secret on vm creation (#614)
* fix: create new secret on vm creation

Signed-off-by: Caio Torres <caio.torres@suse.com>

* fix: ensure parseVM result is immutable

Signed-off-by: Caio Torres <caio.torres@suse.com>

---------

Signed-off-by: Caio Torres <caio.torres@suse.com>
2025-11-27 17:36:55 +08:00
renovate[bot] fab7fbec5e deps: update dependency node-forge to v1.3.2 [security] (#623)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-27 14:32:20 +08:00
Andy Lee d94003f8c2 feat: allow user to attach volume to muliple VMs (#620)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-27 14:22:06 +08:00
renovate[bot] dbb5b01cc3 deps: update patch digest dependencies (#615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 14:56:50 +08:00
Andy Lee 467933bda0 chore: bump version to v1.7.0-rc3 (#612) 2025-11-20 10:19:12 +08:00
Yiya Chen 1b183febdc fix: condition render namespaceOptions (#607)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-11-19 17:35:33 +08:00
Andy Lee 70d3b656f7 fix: change migConfiguration model to inherit from harvester resource (#608)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-19 17:30:58 +08:00
Andy Lee 10d19cd329 feat: create related image storageclass before OS upgrade (#595)
* feat: create related image SC before upgrade

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

* refactor: update spec.targetStorageClassName

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

* refactor: based on comment

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-17 17:25:16 +08:00
renovate[bot] 87e44cb658 deps: update dependency @types/node to v20.19.25 (#600)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 17:08:36 +08:00
Yiya Chen 1715ae754c feat: modified placeholder (#599) 2025-11-17 17:05:40 +08:00
30de2b1a18 feat: add support for configuring transparent hugepages (#414)
* feat: add support for configuring transparent hugepages

Related-to: https://github.com/harvester/harvester/issues/5006
Co-authored-by: Moritz Röhrich <moritz.rohrich@suse.com>
Signed-off-by: Tim Serong <tserong@suse.com>

* fix: return empty object if hugepages can't be found for node

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

Co-authored-by: Andy Lee <andy.lee@suse.com>
Signed-off-by: Tim Serong <tserong@suse.com>

---------

Signed-off-by: Tim Serong <tserong@suse.com>
Co-authored-by: Moritz Röhrich <moritz.rohrich@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-12 17:09:56 +11:00
Andy Lee 6fedcc353c chore: bump version to v1.7.0-rc2 (#596)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-12 09:14:24 +08:00
Yiya Chen f9bff21e84 feat: support for HotPlugNICs from Kubevirt (#582)
* refactor: rename hotplug volume
* feat: add hotplug NIC
* feat: add hot unplug
* refactor: rename NIC
* feat: get latest status
* feat: disable not ready options
* feat: filter out system networks
* refactor: update wordings

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-11-11 11:43:46 +08:00
Andy Lee 6735826e15 chore: update yarn.lock for @rancher/shell v3.0.8-rc.8 (#591)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-10 15:59:11 +08:00
Andy Lee 9e17e239cf chore: bump shell version to v3.0.8-rc.8 (#588)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-10 14:53:49 +08:00
Tim Serong a1cf41bda9 feat: enable snapshot and clone for LHv2 (#379)
Now that Longhorn supports volume clone with the V2 data engine, we
can enable volume snapshot and clone.

Related issue: https://github.com/harvester/harvester/issues/6710

Signed-off-by: Tim Serong <tserong@suse.com>
2025-11-06 16:18:37 +11:00
Andy Lee db58024351 ci: lint last commit if is empty string or all zero (#584)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-05 15:07:43 +08:00
Andy Lee 81bf19419c chore: bump version to v1.7.0-rc1 (#583)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-04 16:07:02 +08:00
Andy Lee 6f90cae482 feat: add vGPU MIG Configuration page (#576)
* feat: add vGPU MIGConfiguration page

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

* feat: add detail page

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

* feat: add banner

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

* refactor: allow editConfig when status is empty

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

* refactor: remove unneeded code

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

* refactor: only show disable action if MIGConfig is enabled

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

* refactor: some UI flow changes

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

* feat: show configured profile in table

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

* refactor: show configured profiles with requested count

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

* refactor: based on review

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-04 13:32:30 +08:00
renovate[bot] b4980a51e7 deps: update dependency @types/node to v20.19.24 (#578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 15:32:05 +08:00
Andy Lee 756ed383ac fix: namespace can't be selected the same in IP pool page (#560)
* fix: the namespacess option can't be selected twice in standalone UI

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

* refactor: disabled slected ns

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-31 16:52:05 +08:00
Yiya Chen 7e0a9dcd80 feat: add kubevirt migration setting (#577)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-31 16:49:31 +08:00
devsymo 5fae6c3087 fix(image): Correctly handle query parameters in image URL validation (#569)
* fix(image): Correctly handle query parameters in image URL validation

The imageUrl validator currently fails to correctly extract the file extension
when the image URL contains query parameters or fragments (e.g., 'image.qcow2?token=abc').

This change introduces a dedicated function, `getFilenameFromUrl`,
which uses the native URL object for robust parsing.
This ensures the file suffix validation is always performed on the actual filename,
ignoring any trailing parameters.

Signed-off-by: devsymo <devsymo@hotmail.com>

* fix(lint): Resolve formatting and spacing warnings

Signed-off-by: devsymo <devsymo@hotmail.com>

* fix: adjusted filename extraction logic and remove duplicate code

Signed-off-by: DevSymo <DevSymo@hotmail.com>

---------

Signed-off-by: devsymo <devsymo@hotmail.com>
Signed-off-by: DevSymo <DevSymo@hotmail.com>
2025-10-30 16:02:35 +08:00
renovate[bot] a994d9861e deps: update patch digest dependencies (#572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 11:17:36 +08:00
Andy Lee 5d996c42dc ci: allow any string as commit scope (#571)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-23 14:18:02 +08:00
Andy Lee 88cd302ce4 fix: trunk input field is missing when creating L2VlanTrunkNetwork (#570)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-23 09:47:47 +08:00
Yiya Chen e6dd8d6771 feat: online Volume Resizing from the VM Page (#568)
* feat: add conditions tab
* feat: ignore restart for resizing
* refactor: remove unused code
---------
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-22 17:33:28 +08:00
Andy Lee 1a3822881e fix: view and edit mode for l2vlan trunk (#563)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-20 14:52:32 +08:00
renovate[bot] ee8bb21a10 deps: update dependency @types/node to v20.19.22 (#564)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 10:44:54 +08:00
Yiya Chen e3d30a0eec feat: make size editable (#562)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-17 15:36:36 +08:00
Andy Lee 532b6c4d50 feat: add access / trunk mode in create VM network page (#510)
* feat: add l2VlanTrunkMode feature

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

* refactor: remove unneeded code

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

* refactor: fix edit l2vlan trunk mode edit page

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

* fix: hide Route tab when trunk mode

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-17 15:28:21 +08:00
Yiya Chen f22644e058 fix: reset filesystem (#559)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-17 12:15:58 +08:00
Andy Lee cd128d0444 chore: limit v1.7.0 to install on Rancher 2.13.0+ (#558)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-15 17:38:09 +08:00
Yiya Chen 0278f51260 fix: WebVNC console shortcut keys not working correctly (#557)
* style: disable overflow

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* style: fix class name

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-13 16:03:16 +08:00
Andy Lee 98efd63110 fix: abort upload when switch to URL option (#552)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-13 14:48:37 +08:00
renovate[bot] 78e78ac8dd deps: update patch digest dependencies (#553)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-12 22:27:42 +08:00
Yiya Chen 2aaa0a55a2 feat: add default value (#548)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-09 13:37:06 +08:00
Andy Lee 5c2a23924d fix: allow to edit as yaml if has empty CPU or memory (#547)
* fix: allow edit as yaml in create VM page if empty CPU or memory

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

* refactor: remove getCPUMemoryValidation

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-08 16:11:27 +08:00
Yiya Chen 7af8a82838 feat: remove machine type dropdown (#546)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-08 13:41:35 +08:00
Caio Torres 3b343bcaca fix: create new secret on vm creation (#537)
Signed-off-by: Caio Torres <caio.torres@suse.com>
2025-10-07 17:23:18 +08:00
renovate[bot] 6ddd35d661 deps: update dependency @types/node to v20.19.19 (#542)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 12:01:21 +08:00
Andy Lee bd28ba6f71 feat: add network policy page (#536)
* feat: add Network Policiies page

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

* ci: add build

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

* ci: ensure FROM and TO exist

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

* ci: skip commitlint if FROM and TO emtpy

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

* revert: unnecessary change

* feat: add banner

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-03 09:38:31 +08:00
Andy Lee 4bb67153ce fix: set maxSockets : 1 if not enabled cpuMemoryhotplug (#539) 2025-10-02 17:50:16 +08:00
Andy Lee 7dbd442519 fix: force fetch VM data (#535) 2025-09-25 18:55:47 +08:00
Andy Lee 5a0d7f283d fix: unable to create VM when choosing L2VlanNetwork (#534)
* fix: unable to create VM via l2vlan network

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

* refactor: based on discussion

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

* refactor: add comment

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-23 16:32:39 +08:00
Andy Lee 9fdbe9c58f feat: add ACL tab in create subnet page (#527)
* feat: add ACL tab in create subnet page

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

* fix: typo

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-22 16:13:13 +08:00
renovate[bot] f652ed9d4b deps: update dependency @types/node to v20.19.17 (#530)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 10:11:21 +08:00
Andy Lee 21a1cd4e89 feat: skip ksmtuned updated if there no related change (#526)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-22 10:03:34 +08:00
renovate[bot] facc74ca51 deps: update dependency @types/node to v20.19.14 (#522)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 10:14:17 +08:00
Andy Lee 74c12a0d1b feat: add info banner static route tab (#520) 2025-09-13 14:00:18 +08:00
Andy Lee 3277ab4a2b refactor: remove ui-plugin-index setting (#518)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-09 14:55:23 +08:00
Andy Lee 4aabf0b7a3 fix: hide VM take backup action if backup target is not available (#512)
* fix: hide VM take backup action if backup target is not available

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

* refactor: use extracted func

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-09 14:36:23 +08:00
Andy Lee 4e8cb31e9d fix: unable to attach Ti volume in create VM page (#513)
* fix: unable to attach Ti volume in create VM page

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

* refactor: only consider Ti / Gi for attached volume

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-09 14:27:40 +08:00
renovate[bot] 1b214b2b6f deps: update dependency @types/node to v20.19.13 (#514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 14:39:13 +08:00
Andy Lee 748c88866a feat: display N/A in vlanID for overlay network (#511)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-06 11:21:16 +08:00
Andy Lee c3e5c2161e feat: add DHCP ui config in subnet page (#504)
* feat: add dhcp ui setting in subnet page

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

* feat: add dhcp option banner link

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-03 12:04:20 +08:00
Andy Lee f932afee68 refactor: disable the setting detail drawer (#507)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-02 17:47:53 +08:00
renovate[bot] 0bbdf3bf17 deps: update minor dependencies (#493)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 14:44:52 +08:00
renovate[bot] bdf3ac2803 deps: update patch digest dependencies (#492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 12:14:18 +08:00
Andy Lee 0d183a8174 ci: update renovate.json to remove semanticCommitScope (#506)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-02 12:09:14 +08:00
Andy Lee 8f72c33f4b ci: update renovate.json to let PR title start with deps: (#505) 2025-09-02 11:07:49 +08:00
Caio Torres 22c99211f1 fix: preserve YAML device parameters on config editing (#487)
Signed-off-by: Caio Torres <caio.torres@suse.com>
2025-09-01 14:20:12 +08:00
Andy Lee 775330d829 feat: add reconnect button in VNC console (#489)
* feat: add reconnect button in VNC console

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

* refactor: reconnnect logic

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

* refactor: call reconnnect

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-01 13:51:56 +08:00
freeze 18be022a9f chore: modify the check-version script (#490)
- to pass the dev build

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2025-08-31 15:11:35 +08:00
Andy Lee 56b4b46b5a chore: add v1.6.1 and v1.7.0 feature flags (#483)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-27 15:50:17 +08:00
Andy Lee db398ecad3 chore: bump version to v1.7.0-dev (#482)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-26 14:54:30 +08:00
Andy Lee d175e3b11a chore: bump version to v1.6.0 (#480)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-26 11:17:58 +08:00
Andy Lee 905db4b12c fix: reboot button not work in VNC console (#475)
* fix: reboot button in VNC console

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

* style: hide the arrow in VNC console

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-25 18:07:26 +08:00
Andy Lee fc31b4bb9d ci: setup env using actions/setup (#478)
* ci: setup env using actions/setup

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

* ci: rerun lint if PR force push

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-25 18:04:32 +08:00
Andy Lee db29a7c31b ci: correct set PR label script name (#476)
* fix: set label script name

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

* ci: rerun PR label after PR edited

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-25 17:40:03 +08:00
Andy Lee 2b3541164d ci: add .github/release.yml (#468)
* ci: add release.yml

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

* refactor: update release.yml

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-22 15:50:11 +08:00
Andy Lee ea5e9aa1f4 ci: auto add PR label workflow and script (#466)
* ci: add auto PR label workflow and script

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

* fix: bug fix prefix

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

* refactor: update label mapping

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

* refactor: read commitlint prefix from commitlint.config.js

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

* refactor: rename to scripts/extract-release-label.mjs

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-22 15:45:54 +08:00
Andy Lee 18a5608e72 ci: only run PR lint in PR stage (#471)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-22 14:42:50 +08:00
Andy Lee 18599fc94c deps: upgrade @rancher/shell to 3.0.5-rc.8 (#469)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-22 10:40:08 +08:00
Andy Lee e155d46483 ci: add PR title lint (#458)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-22 10:38:39 +08:00
Andy Lee 795e5d178f fix: resource page detail title link (#459)
* fix: resource page detail title link

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

* refactor: extract harvester type in a map

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

* refactor: put in config/harvester-map.js

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-15 16:48:38 +08:00
Andy Lee 2e03ac3cf7 fix: spec.domain.memory.guest is lost issue (#462)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-15 11:58:11 +08:00
Andy Lee 76605fdc07 refactor: remove unneeded divider in storage class action menu (#460)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-15 10:51:13 +08:00
Andy Lee c289b001db chore: bump version to v1.6.0-rc6 (#456)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-13 15:05:16 +08:00
Yiya Chen e8d63da1eb fix: fallback default value (#454)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-08-12 13:53:09 +08:00
Andy Lee 4a456ac07d chore: update reviewer (#452)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-05 12:08:06 +08:00
Andy Lee 57dfddd593 chore: bump to 1.6.0-rc5 (#450)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-04 21:31:31 +08:00
Andy Lee 66eac96575 feat: add restore failed message in VM state description (#448)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-04 14:09:26 +08:00
Andy Lee 182d92d80b fix: featureFlags is undefined when installing lower version of ui-ext (#443)
* fix: featureFlags is undefined if install lower version of ui-ext

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-01 15:31:20 +08:00
Yiya Chen 1b362f570b fix: update cdiSettings (#444)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-08-01 12:04:14 +08:00
Andy Lee cf798048f8 chore: bump version to v1.6.0-rc4 (#439)
* chore: bump version to v1.6.0-rc4

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

* fix: missing vm network migration setting

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-30 16:05:29 +08:00
Andy Lee 93c8399935 fix: cpu/ memory columns in host detail VM tab (#436)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-29 16:58:22 +08:00
Andy Lee b72f523ddf fix: isStopping state condition (#431)
* fix: isStopping state condition

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

* refactor: remove unused variable

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-29 12:58:38 +08:00
Andy Lee e5ffb08df3 chore: upgrade rancher/shell to 3.0.5-rc.7 (#429)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-28 18:33:37 +08:00
Andy Lee 98a6322c11 refactor: filter anchor link in setting description when searching (#428)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-28 18:19:24 +08:00
Andy Lee be7e4bd80b feat: add runStragety field when cloning a VM (#424)
* feat: add runStragety field when cloning a VM

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

* refactor: remove unneeded change

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-28 14:18:03 +08:00
Yiya ChenandAndy Lee ed2bc3100b feat: expose CDI settings (#418)
* feat: add cdi settings tab

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* feat: basic layout

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* feat: add fields mutation

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: rename keys

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: add edit mode

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: remove isCreate

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* feat: filter volume mode options

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-25 13:55:54 +08:00
Yiya Chen b6ffb3e9f1 fix: exception error (#422)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-24 12:37:56 +08:00
Andy Lee 3d8a6bba7e chore: bump version to 1.6.0-rc3 (#420)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-23 15:00:28 +08:00
Andy Lee be9311dc0c feat: CPU / Memory hotplug support (#413)
* feat: add maxCPU and maxMemory

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

* feat: add hotplug dialog

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

* feat: add restart message

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

* feat: let VM template support cpuMemoryHotplug

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

* feat: add feature flag

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

* feat: add max-hotplug-ratio setting

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-23 14:08:59 +08:00
Andy Lee e294f4c00f refactor: remove unneeded translation keys (#416)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-21 17:41:40 +08:00
Nick ChungandAndy Lee a9fa928912 feat: Remove guest cluster in Rancher (#391)
* feat: Remove guest cluster in Rancher

Signed-off-by: Nick Chung <nick.chung@suse.com>

* feat: add feature flag 1.6.0

Signed-off-by: Nick Chung <nick.chung@suse.com>

* feat: update for review

Signed-off-by: Nick Chung <nick.chung@suse.com>

* chore: fix for review

Signed-off-by: Nick Chung <nick.chung@suse.com>

* chore: fix for review

Signed-off-by: Nick Chung <nick.chung@suse.com>

* refactor: reduce redundant code

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

* chore: change text area to yaml editor

Signed-off-by: Nick Chung <nick.chung@suse.com>

* refactor: change radio and yaml editor position

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

---------

Signed-off-by: Nick Chung <nick.chung@suse.com>
Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-21 17:25:51 +08:00
Yiya Chen ef2b4d1589 feat: disable vm volume resize (#409)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-17 18:29:02 +08:00
Andy Lee a73e9f0ac1 ci: disable subject-max-length commitlint rule (#408)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-17 13:51:06 +08:00
Andy Lee 11b3bf4c1f fix: lint in config/types.js (#407)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-16 19:09:25 +08:00
Nick Chung b775ce5f50 chore: disable vmstate-persistence and longhorn-static StorageClasses (#377)
* chore: disable vmstate-persistence and longhorn-static StorageClasses

Signed-off-by: Nick Chung <nick.chung@suse.com>

* chore: allow internal storage class deletions in image and volumn

Signed-off-by: Nick Chung <nick.chung@suse.com>

* chore: remove deletion tooltips in image and volume pages

Signed-off-by: Nick Chung <nick.chung@suse.com>

* chore: rollback style changes of image and volume lists

Signed-off-by: Nick Chung <nick.chung@suse.com>

---------

Signed-off-by: Nick Chung <nick.chung@suse.com>
2025-07-16 18:21:06 +08:00
Andy Lee 193d4536e3 chore: bump version to v1.6.0-rc2 (#403)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-16 18:09:34 +08:00
Andy Lee 9ca9fdb521 feat: support for subnets and VPCs from UI (#374)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-16 17:51:37 +08:00
Yiya Chen bcabefe9f3 fix: add default value to prevent exception (#397)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-16 15:16:40 +08:00
Andy Lee c541f81dc3 ci: add package version check (#396)
* ci: add package version check

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

* ci: extract check version in a script

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

* ci: update env variable

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-16 13:17:05 +08:00
Yiya Chen 4486f71c8f feat: add vm-migration-network setting (#395)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-16 10:13:07 +08:00
Yiya Chen 060105ead3 chore: upgrade rancher/shell to 3.0.5-rc6 (#393)
* chore: bump shell rc6
* chore: add rancher restriction

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-14 09:56:14 +08:00
Andy Lee 3ea73978ee fix: find the correct vmim based on creationTimeStamp (#389)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-10 11:16:45 +08:00
Yiya Chen f4e363396d feat: add csi-online-expand-validation setting (#378)
* feat: add csi-online-expand-validation setting
* feat: invalid json error message
* feat: handle API errors
* refactor: remove inStore()

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-09 15:26:56 +08:00
Andy Lee fa16e24983 chore: bump version to v1.6.0-rc1 (#385)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-09 14:34:36 +08:00
Andy Lee ce63ee6a19 fix: read volume accessMode even is RWO (#381)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-09 11:58:21 +08:00
Yiya Chen ee28697161 ci: ignore draft PR add add backport labels (#382)
* ci: skip draft PR

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* ci: fix auto assign

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* ci: add backport label

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* fix: remove token

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-09 11:34:30 +08:00
Yiya Chen 85a06feb91 feat: disable vm images (#376)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-08 15:23:32 +08:00
Yiya Chen 17dd46cee9 ci: add commands_restrictions (#375)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-03 17:46:11 +08:00
Nick Chung 0ef4ff65cc fix: can't migrate network config in Rancher integration (#371)
Signed-off-by: Nick Chung <nick.chung@suse.com>
2025-07-03 16:52:07 +08:00
Yiya Chen cebb302730 ci: use pull_request_target (#370)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-03 14:00:31 +08:00
Andy Lee f12717a8f4 fix: add custom promptRemove for backup and snasphot deletion (#367) 2025-07-03 11:51:53 +08:00
Yiya Chen 03f54643fd ci: remove action-add-labels (#368)
* ci: remove action-add-labels
* ci: use gh cli
* ci: use pull_request_target
* ci: add ref

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-02 14:46:03 +08:00
Yiya Chen 7fb6d44208 ci: add backport label on PR creation (#365)
* ci: improve backport workflow
* chore: ignore tmp files
* ci: add exact match
* ci: target main branch
* ci: rewrite workflows

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-02 11:39:02 +08:00
Nick Chung dc683a50a4 feat: [ENHANCEMENT] Add setting upgrade-config to UI(#8369) (#347)
Signed-off-by: Nick Chung <nick.chung@suse.com>
2025-07-01 11:14:56 +08:00
Yiya Chen be421054d8 feat: SB Enhancements (NS selection and timeout) (#345)
* feat: add namespace field
* feat: add optional inputs
* feat: refine code
* feat: add feature flag
* refactor: fix lint error
* feat: filter default namespaces
* refactor: hide tips with feature flag
* refactor: use UnitInput
* feat: load default value from settings
* refactor: fix API url
* refactor: no available namespaces
* chore: update subject-case rule
---------
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-01 09:38:22 +08:00
Andy Lee fcef0391bb ci: replace tag with github.event.release.tag_name (#356)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-27 17:29:37 +08:00
Andy Lee 1d89aafeab fix: cloned secret should keep originl type (#354)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-27 15:55:47 +08:00
Nick Chung 7386a2deb6 chore: remove run-lint from build-standalone-on-release (#355)
Signed-off-by: Nick Chung <nick.chung@suse.com>
2025-06-27 12:41:22 +08:00
Andy Lee 26f5ebc8e2 ci: auto open backport pr to v1.6 release branch (#353)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-26 17:01:00 +08:00
Andy Lee 8cd482870b chore: downgrade shell to 3.0.5-rc3 (#348)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-26 11:39:16 +08:00
Andy Lee 57cbd799dd fix adding existing volume in edit VM page (#351) 2025-06-26 10:07:15 +08:00
Yiya Chen 3694b316ab fix: dynamic get VM's machine type API endpoint (#349)
* fix: update API endpoint
* fix: remove redudant slash
---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-06-26 09:19:09 +08:00
Nick Chung 717258defd feat: introduce commit-lint (#350)
Signed-off-by: Nick Chung <nick.chung@suse.com>
2025-06-26 08:39:44 +08:00
Andy Lee 7aae6264f7 Add auto assign reviewer workflow (#346)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-25 11:13:18 +08:00
Andy Lee c88a083e04 Upgrade rancher/shell to 3.0.5-rc5 (#344)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-23 14:56:55 +08:00
Yiya Chen 52c4556e64 fix: failed to clone an image file created by file upload (#339)
* fix: filter clone action
* fix: hide clone action for other types
* feat: show source type in details
---------
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-06-18 17:17:43 +08:00
Andy Lee 90cb147938 Align settings page actionMenu style with other pages (#342)
* Align settings page actionMeun with other pages

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

* update based on PR feedback

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-18 15:03:57 +08:00
Andy Lee d9c97de0fd Add searchbox in settings page (#338)
* Add searchbox in setting page

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

* update based on comment

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

* PR feedback

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-17 14:50:52 +08:00
Andy Lee fb59b396d1 remove disabled for upgrade action (#340)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-17 13:51:34 +08:00
Andy Lee bdcea54eeb Fix uploading image stuck at edit page (#336)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-11 16:49:26 +08:00
Andy Lee 912ca7883f Add description column in namespace and image pages (#335)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-11 10:16:34 +08:00
Andy Lee a89678cc81 upgrade shell to 3.0.5-rc3 (#334)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-11 10:16:24 +08:00
Andy Lee d4d3774c3b filter golden image volume count in dashboard (#332)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-10 12:30:21 +08:00
Andy Lee 32ebdc3acd fix http-proxy setting not saved (#329)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-06 17:01:31 +08:00
Andy Lee 703abd7ab9 add auto backport v1.0 mergify rule (#328)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-06 16:59:46 +08:00
Andy Lee e8f7f0e06b fix multiple VM modals (#325) 2025-06-06 15:51:04 +08:00
Yiya Chen feddcd247a feat: add confirmation modal for vm soft reboot and restart (#327)
* feat: add confirmation for vm softreboot and restart

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: update wordings

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* feat: handle bulk action

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-06-06 12:27:15 +08:00
Yiya Chen 29b1ab2fb9 feat: support manually configuring VM IP (#315)
* feat: add annotations

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* feat: map annotation

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* feat: add annotaion to template

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-06-06 10:03:37 +08:00
Yiya Chen 01528f7429 fix: inherit labels (#322)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-06-04 17:42:57 +08:00
Andy Lee 9e015ec3b1 fix API doesn't bring checksum (#323) 2025-06-03 20:58:03 +08:00
Yiya Chen 7ecc9c320d fix: VM's machine type options is not support (#307)
* feat: get options from API

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* feat: add feature flag

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: remove default value

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: fallback to none

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-06-02 12:38:04 +08:00
Andy Lee d6da4898b4 Fix error banner can't be dismissed (#312)
* fix error banner can't be dismissed

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

* update all CruResource

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-29 15:43:14 +08:00
Andy Lee d023104371 simply PR template (#317)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-28 00:09:45 +08:00
Andy Lee cb452b9627 fix upgrade button status if have errors (#316) 2025-05-27 15:24:42 +08:00
Andy Lee dbbad01b0f Add OS upgrade features (#311)
* Add failed and success banner after image uploaded

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

* add delete image feature

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

* add skip checking single-replica detached volumes checkbox

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

* change delete image flow

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

* Reuse ConfirmRelatedToRemoveDialog

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-27 14:29:48 +08:00
Yiya Chen b689e3aacf feat: disable label (#310)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-23 16:23:09 +08:00
Yiya Chen 796d98e61a fix: Harvester upgrade ISO related internal image can be selected to create VM when an upgrade is ongoing (#309)
* fix: filter out upgrade image

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: add label constant

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-23 15:49:30 +08:00
Andy Lee fe32b4372b give default valeu for uploadImage opt (#308)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-22 16:22:13 +08:00
Yiya Chen 3dcec52aff fix: hide error message (#305)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-22 11:07:20 +08:00
Andy Lee 6d8f6579c7 Fix OS upgrade page (#306)
* fix OS upgrade page

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

* move uploadFile action

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

* change os upgrage ui to standard feature

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

* Add uploadImage cancel

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
Signed-off-by: a110605 <andy.lee@suse.com>
2025-05-21 16:51:16 +08:00
Yiya ChenandAndy Lee 9e240792e8 doc: add QA testing guideline (#303)
* doc: add testing guideline

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* doc: update wording

Co-authored-by: Andy Lee <andy.lee@suse.com>
Signed-off-by: Yiya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Signed-off-by: Yiya Chen <yiya.chen@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-05-19 12:24:52 +08:00
Andy Lee aa9dd32cce Change isKVMDisable condition (#294)
* change condition to check isKVMDisable

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

* change condition eq 0

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

* add allNotExist back

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-19 10:17:07 +08:00
Yiya Chen 4f3e532327 chore: build main-head on PR merge (#302)
* chore: add main branch

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* ci: add dev branch

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-16 15:42:38 +08:00
Yiya Chen cbfcf4dbae fix: range condition (#299)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-15 16:17:04 +08:00
Andy Lee 8877dcf639 upgrade shell to 3.0.5-rc.2 (#297) 2025-05-15 11:21:06 +08:00
renovate[bot] 8d196c4244 chore(deps): update dependency follow-redirects to v1.15.6 [security] (#285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-15 10:48:02 +08:00
Yiya Chen 220e40feaa chore: add labels (#295)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-14 17:24:36 +08:00
Yiya ChenandJillian Maroket 258476876a refactor: the promptRemove.confirmRelatedResource message needs to be refactored (#293)
* feat: change dialog wordings

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: update protip wording

Co-authored-by: Jillian Maroket <67180770+jillian-maroket@users.noreply.github.com>
Signed-off-by: Yiya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Signed-off-by: Yiya Chen <yiya.chen@suse.com>
Co-authored-by: Jillian Maroket <67180770+jillian-maroket@users.noreply.github.com>
2025-05-14 15:27:06 +08:00
Andy Lee 63c4810b99 Reset network / user script when choosing ostype from windows to linux (#292)
* reset network / user script when choosing windows

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

* change OS type from Windows init user and network data

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-14 14:15:39 +08:00
Andy Lee 0cd5651c38 upgrade shell to 3.0.5-rc1 (#282)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-13 15:33:50 +08:00
renovate[bot] 6f66c80c63 chore(deps): update dependency ejs to v3.1.10 [security] (#284)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-13 15:25:11 +08:00
Yiya Chen 0e5a78d8a6 chore: add renovate (#283)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-12 05:54:35 -05:00
Andy Lee d16802365e Add live migration progress awareness in migration tab (#271)
* Embedded VM migration detail in migration tab

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

* update events condition

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-12 15:04:50 +08:00
Andy Lee 8410f75859 Remove unneeded condition (#279)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-12 10:48:01 +08:00
Andy Lee f8079c5924 Refactor feature flags structure (#276)
* refactor feature flags structure

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

* Replace with FEATURE_FLAGS and valid and sort versions

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-12 10:43:12 +08:00
mergify[bot]andAndy Lee 1e270268d8 fix #cloud-config disappear when add/remove ssh key (#273) (#277)
(cherry picked from commit 9dd8ae479a)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-05-08 17:34:15 +08:00
Yiya Chen 43064576d6 refactor: disable required (#274)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-08 15:17:01 +08:00
Yiya Chen 9dd36bbd87 fix: skip escapehtml (#272)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-08 15:12:54 +08:00
Andy Lee 235373045c Add some feature flags (#270)
* Add 1.4.2/1.5.1/1.6.0 feature flags

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

* bump version to v1.6.0-dev

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-08 13:18:45 +08:00
Yiya Chen 5841508b26 fix: hide btns before is ready (#267) 2025-05-05 20:49:08 +08:00
Yiya Chen b941902088 fix: support-bundle-expiration can be invalid value 000003333 (#248)
* fix: normalize number input

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* fix: leverage to other inputs

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-04-28 11:45:50 +08:00
Andy Lee 485db3066f add require backport 1.5 label in mergify.yml (#265)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-28 11:36:26 +08:00
Andy Lee af43ebbf6f bump to 1.5.0 (#263)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-23 17:10:48 +08:00
Andy Lee 74300f24b3 update RadioGroup update value action (#255)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-17 15:26:07 +08:00
Andy Lee 2993ddb82c fix unable to detach PCI & USB device in VM edit page (#247)
* fix unable to detach PCI & USB device in VM edit page

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

* refactor based on comment

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

* using set from shell/utils/object

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-16 16:09:33 +08:00
Andy Lee a5d4604dce bump to 1.5.0-rc5 (#245) 2025-04-16 00:19:35 +08:00
Andy Lee daa6d6942b pass showYaml prop to prevent double generate secret (#243)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-15 16:05:13 +08:00
Andy Lee eb92642b3b Add logic to block import harvester version lower than 1.3.0 (#241)
* add logic to block import harvester version lower than 1.3.0

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

* refactor console.error message

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-10 16:21:17 +08:00
Andy Lee 78234b9e1e filter golden image volume in hot plug volume modal (#239)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-10 13:17:30 +08:00
Andy Lee 3dc6dda1ca bump to 1.5.0-rc4 (#235)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-09 14:04:12 +08:00
Andy Lee fca05d8489 hide usb device tab in v1.3.x (#236)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-09 14:01:36 +08:00
Andy Lee 238c660796 Fix undefined error in VM edit as yaml page (#233)
* fix undefined error whenclick save in  edit as yaml

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

* fix VM isEqual logic and fix undefined error

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-07 15:30:59 +08:00
Andy Lee 19bea97106 fix secret name is different when edit as yaml (#230)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-07 10:47:50 +08:00
Jack Yu de4edbbf3b chore: use default imag repo name (#212)
Signed-off-by: Jack Yu <jack.yu@suse.com>
2025-04-06 21:08:23 -05:00
Andy Lee 9256391627 Bump ui-ext to 1.5.0-rc3 (#228)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-02 16:00:39 +08:00
Andy Lee 25439aee65 Fix error banner position in all dialogs (#226)
* fix error banner position in all dialogs

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

* update HarvesterImageDownloader.vue

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-31 16:27:13 +08:00
Andy Lee f2fe1f5bd6 change longhorn v2 access mode to RWX (#223) 2025-03-28 12:38:30 +08:00
Andy Lee f8d5aa1a21 update to take VM snapshot logic (#222) 2025-03-28 12:37:54 +08:00
Andy Lee 38cf667830 use harvester translation key forhost page CPU and memory (#220)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-27 13:24:55 +08:00
Andy Lee 18667836c2 upgrade ui-ext version to 1.5.0-rc2 (#208)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-25 11:42:16 +08:00
Andy Lee 2940f25fe0 fix volume won't be dismiss after deleting (#216)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-24 15:31:14 +08:00
Andy Lee a24d01717a fix image.repo to image.repository (#214)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-21 17:40:39 +08:00
Andy Lee 22a032e56c fix the doneOverride path for harvesterhci.io.virtualmachinetemplate (#211)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-20 17:06:06 +08:00
Andy Lee 9343813ace Add volum mode options in create VM volume tab (#209)
* add volume mode option for non-longhorn volume

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

* fix default storage class in create VM page

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-19 17:42:02 +08:00
Andy Lee 2dff7b0a93 upgrade rancher/shell to 3.0.2-rc6 (#206)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-14 16:47:04 +08:00
Andy Lee 54c5d77198 fine tune clone VM dialog error message (#203)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-14 15:09:16 +08:00
Jack Yu 1880043a80 chore: add building catalog GA (#181)
* chore: add building catalog GA

Signed-off-by: Jack Yu <jack.yu@suse.com>

* feat: should include prerelease

Signed-off-by: Jack Yu <jack.yu@suse.com>

---------

Signed-off-by: Jack Yu <jack.yu@suse.com>
2025-03-14 15:03:00 +08:00
Andy Lee d8bee7f4f5 Bring status object in harvesterhci.io.upgrade dismiss PUT API (#201)
* bring status object in PUT dismiss API

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

* add cleanForSave in harvester model

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-13 14:10:59 +08:00
Andy Lee 90c923b480 Add volumeMode dropdown for third-party storage (#197)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-12 14:25:37 +08:00
Andy Lee 374b904191 replace tag format v1.5.0 to harvester-1.5.0 (#190)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-12 12:00:09 +08:00
Andy Lee f2725e2773 Merge pull request #196 from a110605/fix_progresslist_typo
fix percent props typo
2025-03-11 16:53:55 +08:00
Andy Lee 487f9abc10 fix percent typo
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-11 14:52:50 +08:00
Andy Lee 1862cfcc08 Merge pull request #191 from a110605/add_box
Fix missing remove button in Infobox
2025-03-11 11:58:35 +08:00
Andy Lee 480f95a5cc Merge pull request #192 from a110605/remove_require_dot
remove require dot for non-longhorn v1
2025-03-11 11:34:30 +08:00
Andy Lee b939df9b7d remove require dot for non-longhorn v1
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-11 11:14:27 +08:00
Andy Lee 4e831cbc5f Fix infobox style
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-07 14:54:58 +08:00
Andy Lee 0283cfb2bb Merge pull request #183 from a110605/bump_rc1
Bump harvester-ui-extension to v1.5.0-rc1
2025-03-06 15:55:29 +08:00
Yiya Chen 4e2562190c Merge pull request #185 from houhoucoop/issue-7737
fix: allow set empty for backupVolumeSnapshotClassName in Setting csi-driver-config
2025-03-06 15:48:46 +08:00
Yi-Ya Chen 64b59a0c27 refactor: default should selectable
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-03-06 15:34:45 +08:00
Andy Lee 3e7b2338ff Merge pull request #186 from a110605/filter_export_lhv1
Filter exported SC for non-longhorn V1 volume
2025-03-06 15:34:33 +08:00
Andy Lee 104e98e390 Filter export SC for non-longhorn V1 volume
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-06 15:21:50 +08:00
Yi-Ya Chen 72415622d9 feat: gray out class name selector
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-03-06 15:00:58 +08:00
Andy Lee a861450874 Merge pull request #182 from a110605/add_image_downloader
Add HarvesterImageDownloader for cdi image
2025-03-06 14:49:24 +08:00
Andy Lee cbd5e45200 Remove converting sentence
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-06 14:39:50 +08:00
Andy Lee 8ad7a57ab4 bump to v1.5.0-rc1
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-06 11:53:49 +08:00
Andy Lee 966d4d6709 Add HarvesterImageDownloader
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-06 11:14:43 +08:00
Andy Lee 208f1f29f5 Merge pull request #175 from a110605/issue_5974_2
Add label tab to vm template
2025-03-04 17:35:24 +08:00
Andy Lee d923239b9d Merge pull request #178 from a110605/fix_error
Fix  undefined reading harvesterhci.io/storageClassName
2025-03-04 11:01:28 +08:00
Andy Lee 54f85963ea fix undefined reading harvesterhci.io/storageClassName
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-04 10:59:35 +08:00
Andy Lee 36257299e1 Merge pull request #173 from a110605/fix_stopping
Introduce VM Pending status
2025-03-04 10:48:35 +08:00
Andy Lee 91d078068f Merge pull request #174 from a110605/align_gi
fix: align all memory / storage / quota units to Gi/Mi
2025-03-04 10:46:59 +08:00
Andy Lee 8fbbfc90f1 update harvesterhci.io.host
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-04 10:43:12 +08:00
Andy Lee efdfdbf0b8 Add label tab to vm template
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-03 17:14:30 +08:00
Andy Lee 8db4de1c4a Extract variables to utils/unit
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-03 16:57:25 +08:00
Andy Lee 05ff8e4f19 introduce VM pending status
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-03 14:46:35 +08:00
Andy Lee 8f76d5ad30 fix: align all memory / storage / quota units to Gi/Mi
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-02-28 22:57:54 +08:00
Andy Lee ff4a865c55 Merge pull request #171 from a110605/feature_flag_refresh
Add refreshIntervalInSecond in 1.4.2 feature flag
2025-02-27 17:23:56 +08:00
andy.lee 30dc56cad5 add refreshIntervalInSecond in 1.4.2 feature flag
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-27 17:22:17 +08:00
Andy Lee c10573d24a Merge pull request #169 from a110605/fix_bug_third_party
fix golden image error when creating LVM volume
2025-02-27 13:45:55 +08:00
andy.lee ca7eccd688 fix golden image error when creating LVM volume
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-26 17:48:14 +08:00
Andy Lee fc606dc4d1 Merge pull request #167 from a110605/hide_external_link
Hide non-longhorn volume external link
2025-02-26 17:00:01 +08:00
andy.lee 046947de32 hide non longhorn volume external link
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-26 16:54:09 +08:00
Andy Lee 6e5532e497 Merge pull request #164 from a110605/issue_7693
Add refreshIntervalInSeconds in backup targe edit page
2025-02-26 16:17:30 +08:00
andy.lee 7e00a08e35 Add refreshIntervalInSeconds in backup targe edit page
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-26 15:53:13 +08:00
Andy Lee 48ec40fe3d Merge pull request #162 from a110605/fix_upgrade_modal
Fix upgrade cluster model styles
2025-02-26 09:31:32 +08:00
andy.lee 50901a2eaf change overallMessage color to red
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-25 15:45:45 +08:00
andy.lee ff439e66a9 fix upgrade model style
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-25 15:15:26 +08:00
Andy Lee b0b5e5f749 Merge pull request #159 from a110605/issue_5974
Add Labels Tab in VM edit page
2025-02-24 17:41:06 +08:00
Andy Lee be2db9d37c Merge pull request #152 from a110605/issue_7354
Fix usb and pci devices unable to passthrough
2025-02-24 16:40:08 +08:00
andy.lee 9cd3cd6492 Add labels tab in VM edit pae
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-24 14:56:48 +08:00
andy.lee e07042b9c9 update catalog.cattle.io/rancher-version to 2.11.0
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-21 16:56:29 +08:00
Andy Lee 799252a789 Merge pull request #157 from a110605/update_min_rancher
Limit harvester-ui-extension to install on Rancher 2.11.0+
2025-02-21 16:38:30 +08:00
andy.lee 36c68eefb1 Update catalog.cattle.io/rancher-version to 2.11.0
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-21 16:03:41 +08:00
Andy Lee dea3c098df Merge pull request #154 from a110605/bump_ver_v150
Bump harvester-ui-extension version to v1.5.0-dev
2025-02-21 15:00:51 +08:00
andy.lee 0a782fa4d5 bump to v1.5.0-dev
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-21 14:39:13 +08:00
Andy Lee 5905448b90 Merge pull request #148 from a110605/update_install_docs
Update installation flow in README.md
2025-02-21 14:13:22 +08:00
Andy Lee 4553bdc666 Merge pull request #151 from houhoucoop/issue-7593
feat: set up a way to test UI changes in Harvester UI when accessed via Rancher
2025-02-21 13:33:28 +08:00
Yi-Ya Chen 7f98dfe9c8 ci: rename file
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-21 12:33:33 +08:00
Yi-Ya Chen 6a3153a7ec ci: revert change
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-21 10:49:26 +08:00
andy.lee 188f058df8 add pcideviceclaim and usbdeviceclaim models
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-20 17:23:31 +08:00
Yi-Ya Chen 629cb0c601 ci: rename the workflow 2025-02-20 16:30:13 +08:00
Yi-Ya Chen 0c3fe22df7 ci: add ci pipeline
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-20 16:22:03 +08:00
Andy Lee f4edfb8f43 Merge pull request #149 from a110605/issue_7572_2
More third party storage UI change
2025-02-20 14:45:05 +08:00
andy.lee 66f53c8a00 More third party storage UI change
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-20 11:35:59 +08:00
andy.lee f855a0add5 Update installation flow in README.md
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-19 17:39:45 +08:00
Andy Lee 7c2317fae9 Merge pull request #145 from a110605/issue_7572
External/third-party storage UI change
2025-02-19 17:04:50 +08:00
Andy Lee 50fcec0c11 Merge pull request #143 from a110605/fix_issue_7632
Fix sshkey is not added to cloud init user data immediately
2025-02-19 16:22:17 +08:00
andy.lee 4a303e514c allow sorting storage class column in volume list page
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-19 14:07:32 +08:00
andy.lee b828c2f66d add thirdPartyStorage feature flag
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-19 13:33:30 +08:00
andy.lee 40794d89a0 add third party storage UI change
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-19 12:59:28 +08:00
Yiya Chen a6a99520a6 Merge pull request #142 from houhoucoop/fix/issue-7578
fix: no progress circle on the top after triggering upgrade
2025-02-19 09:15:16 +08:00
andy.lee e2daa1ea0c fix sshkey is not added to cloud init user data immediately
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-18 17:29:27 +08:00
Andy Lee 923f36a5c3 Merge pull request #140 from a110605/update_yarn_lock
update yarn.lock
2025-02-17 12:13:46 +08:00
Yi-Ya Chen b499f62c56 fix: register upgrade component in header
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-17 12:10:18 +08:00
andy.lee 9d02623b3a update yarn.lock
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-17 12:08:54 +08:00
Andy Lee 762ab100b9 Merge pull request #138 from houhoucoop/fix/issue-7007
fix: Harvester ui extension not working built from CI
2025-02-17 11:56:21 +08:00
Yi-Ya Chen cbe66f4660 feat: fix $ctx redefine issue
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-14 14:39:27 +08:00
Andy Lee 5b39432481 Merge pull request #133 from a110605/upgrade_shell
Upgrade @rancher/shell to v3.0.2-rc3
2025-02-14 13:29:57 +08:00
Andy Lee 9e32052329 Merge pull request #134 from a110605/move_schema
Move harvester schema.js into harvester folder
2025-02-14 13:29:33 +08:00
Yiya Chen a55353fd1b Merge pull request #132 from houhoucoop/feat/issue-7557
feat: add EFI persistent state checkbox in VM advanced tab
2025-02-13 18:03:33 +08:00
andy.lee 548bdd2835 move harvester schema.js into harvester folder
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-13 11:34:44 +08:00
andy.lee 0153f9d985 upgrade @rancher/shell to v3.0.2-rc3
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-12 16:33:06 +08:00
Yi-Ya Chen 70a56c75b0 lint: fix eslint
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-12 15:59:16 +08:00
Yi-Ya Chen 31a9abd5c2 refactor: remove persistent property if unchecked
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-12 15:38:11 +08:00
Yi-Ya Chen 6d5139a994 fix: state not update when the TPM checkbox is toggled
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-12 15:12:08 +08:00
Yi-Ya Chen b92f22fa75 feat: add efi persistent state
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-12 15:05:07 +08:00
Yiya Chen f9a277d893 Merge pull request #128 from houhoucoop/issue-7328
feat: improve Single-Replica Volume Detection for Upgrade
2025-02-12 09:09:33 +08:00
Yi-Ya Chen 3b054b35c6 feat: add divider
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-11 17:57:36 +08:00
Yi-Ya Chen 2493287334 Merge branch 'main' into issue-7328 2025-02-11 17:51:07 +08:00
Andy Lee 9af8526ef9 Merge pull request #127 from a110605/issue_7544
fix: unable to create new sshkey in VM create page
2025-02-11 17:29:43 +08:00
Andy Lee 07e0dc0d5e Merge pull request #119 from a110605/issue_6501
Add VlanL2Network / UntaggedNetwork types in network-storage setting
2025-02-11 17:16:53 +08:00
Yi-Ya Chen fd3f5ec1e6 refactor: align key spacing
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-11 15:41:11 +08:00
Yi-Ya Chen 72c6741af1 feat: add feature flag
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-11 15:38:41 +08:00
Yi-Ya Chen 375127a78a feat: add checkbox to skip checking
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-11 15:09:13 +08:00
andy.lee fc09e030a0 update feature flag
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-11 14:57:49 +08:00
andy.lee 5f76da4629 extract network_type to config/types
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-11 14:51:50 +08:00
andy.lee 3a3122f3a1 fix number of IP formula
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-11 14:51:50 +08:00
andy.lee 229cbf1f69 add network type in storage-network setting
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-11 14:51:50 +08:00
andy.lee 176946e5e9 fix: unable to create new sshkey in VM create page
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-11 14:48:07 +08:00
Andy Lee f3d2d2f6a8 Merge pull request #125 from a110605/issue_7003_ext
fix: vGPU number not correct
2025-02-11 13:13:02 +08:00
andy.lee a175d1e142 remove unneeded translation keys
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-11 11:34:30 +08:00
andy.lee 26afffc2f1 fix: vGPU number not correct
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-11 11:29:23 +08:00
Andy Lee 5eb19106ea Merge pull request #121 from a110605/issue_6341_ext
The hints on the branding page is using the wrong product name
2025-02-11 11:24:31 +08:00
Andy Lee 7b3857cb38 Merge pull request #122 from a110605/issue_7150
fix: isEncrypted breaks all the volume page
2025-02-11 11:20:10 +08:00
andy.lee 2232e56ba1 fix: isEncrypted breaks all the volume page
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-11 11:17:32 +08:00
Volker Theile d023fa1de4 The hints on the branding page is using the wrong product name
The hints on the `Branding` page at `Advanced > Settings > UI > branding` are using the product name `Rancher` instead of `Harvester`.

Related to: https://github.com/harvester/harvester/issues/6341

Signed-off-by: Volker Theile <vtheile@suse.com>
2025-02-11 11:09:24 +08:00
Andy Lee 527d31e4d0 Merge pull request #117 from houhoucoop/feat/issue-7187
feat: add support for persistent TPM in VMs
2025-02-11 10:27:50 +08:00
Yi-Ya Chen 1651e201e8 refactor: update feature flag
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-10 18:38:50 +08:00
Yi-Ya Chen 43b30ec3c3 feat: add feature flag
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-07 18:12:20 +08:00
Yi-Ya Chen 68ba934b5a refactor: rewrite render logic
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-07 17:07:00 +08:00
Andy Lee 67754427c7 Merge pull request #116 from a110605/issue_7516
Fix unable to add/delete new hard disk in edit node page
2025-02-07 11:23:08 +08:00
andy.lee cbb66175f6 fix unable to add/delete new hard disk
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-06 21:41:25 +08:00
Yi-Ya Chen 84bacfaff5 feat: add checkbox to create templates
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-06 18:49:34 +08:00
Yi-Ya Chen e8017e14c8 feat: add persistent state checkbox
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-06 18:34:58 +08:00
Andy Lee d11d9d4e37 Merge pull request #115 from a110605/fix_cpu_pinnging
Fix align CPU pinning checkbox position in cloud template
2025-02-06 14:36:18 +08:00
andy.lee 50bdd53186 align CPU pinning checkbox position in cloud template
(cherry picked from commit 59bf735f8b)
2025-02-06 14:24:51 +08:00
Andy Lee 5c96e90d58 Merge pull request #110 from a110605/issue_7510
Prevent display lock icon if this.volumes is empty array
2025-02-06 11:47:35 +08:00
Andy Lee 5c06287648 Merge pull request #109 from a110605/add_v142_feature_flag
Add v1.4.2 feature flag to master branch
2025-02-05 17:44:28 +08:00
andy.lee c75755a527 prevent display icon if this.volumes is empty array
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-05 14:06:12 +08:00
andy.lee e8f282395e add v1.4.2 feature flag
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-05 13:55:30 +08:00
Andy Lee 2bb65801d9 Merge pull request #107 from a110605/issue-7283
upgrade @rancher/shell to v3.0.2-rc2
2025-02-05 12:31:46 +08:00
Francesco Torchia ccd4ff2d7a Revert "Remove lint and bump 1.0.5"
This reverts commit ac6958fe40.
2025-01-27 12:00:35 +01:00
Francesco Torchia ac6958fe40 Remove lint and bump 1.0.5
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2025-01-27 11:59:31 +01:00
andy.lee 426e15084e upgrade @rancher/shell to v3.0.2-rc2
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-24 16:16:14 +08:00
Yiya Chen 8477f0754d Merge pull request #103 from houhoucoop/issue-7295
fix: VM Snapshot and Backup List Views Fail to Load After Creation
2025-01-24 10:07:42 +08:00
Yi-Ya Chen 12cadbf667 fix: get sourceSchedule exception
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-21 14:14:56 +08:00
Yiya Chen b9fab8bb57 Merge pull request #94 from houhoucoop/issue-7382
feat: add confirmation pop-up for VM stop and pause actions
2025-01-21 10:23:54 +08:00
Yiya Chen 69e639ff96 Merge pull request #83 from houhoucoop/issue-7081
fix: cluster member role user should not be able to delete node on Harvester Hosts page
2025-01-21 10:23:17 +08:00
Andy Lee cb4cdcb0c7 Merge pull request #89 from a110605/issue_6592
fix: remove witness node in VM node scheduling tab
2025-01-20 20:00:22 +08:00
Yi-Ya Chen c4e9a89ba5 style: change apply button color
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-20 15:55:17 +08:00
Yi-Ya Chen f2d5172224 feat: handle bulk actions
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-20 12:19:57 +08:00
Andy Lee f0a66774b8 Merge pull request #93 from a110605/issue_6792
fix: encrypt option can only choose encrypted storage class when creating VM image
2025-01-20 11:38:18 +08:00
Yi-Ya Chen e95955fad6 refactor: get value from parent class
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-20 11:13:17 +08:00
Yi-Ya Chen fc2be69469 Merge branch 'main' into issue-7081 2025-01-20 10:44:58 +08:00
Yi-Ya Chen fdc635955b refactor: remove the confirm input
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-17 18:06:04 +08:00
Andy Lee f56f46a543 Merge pull request #97 from a110605/update_year
chore: update copyright year to 2025
2025-01-17 17:27:10 +08:00
Yi-Ya Chen f5961f3a59 fix: check user delete permission
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-17 16:45:27 +08:00
andy.lee 8f3a2b202d chore: update company name to SUSE
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-17 16:17:49 +08:00
andy.lee 0ea3f56f2c chore: update copyright year
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-17 16:00:58 +08:00
andy.lee 6e9350e63d refactor: fallback to longhorn if not default storage class
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-17 14:57:39 +08:00
Yi-Ya Chen 526d1bb875 feat: add confirm popup
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-16 17:28:14 +08:00
andy.lee b0c7b1fefe fix: when choose encrypt can only choose encrypted storage class when creating image
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-16 16:18:43 +08:00
Yi-Ya Chen 3565b44e3a refactor: fix lint warnings
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-15 10:12:38 +08:00
Yi-Ya Chen 43b10b250e refactor: move logic to node model
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-15 10:02:33 +08:00
andy.lee 993706fead fix: remove witness node in VM node scheduling tab
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-14 16:20:20 +08:00
Yi-Ya Chen c791399a61 fix: disable delete action
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-13 14:01:35 +08:00
242 changed files with 20189 additions and 5578 deletions
+1
View File
@@ -14,3 +14,4 @@ dist-pkg
pkg/harvester/index.ts
pkg/harvester/store/*
scripts/pkgfile.js
tmp
+1 -1
View File
@@ -4,7 +4,7 @@ description: Setup node and install dependencies
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
+6
View File
@@ -0,0 +1,6 @@
addAssignees: author
addReviewers: true
numberOfReviewers: 0
reviewers:
- a110605
- houhoucoop
+12 -17
View File
@@ -1,19 +1,14 @@
pull_request_rules:
- name: Automatically open backport PR to release-harvester-v1.5
conditions:
- base=main
actions:
backport:
branches:
- "release-harvester-v1.5"
assignees:
- "{{ author }}"
labels:
- "v1.5 backport PR"
- name: Ask to resolve conflict
conditions:
- conflict
actions:
comment:
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
- name: Ask to resolve conflict
conditions:
- conflict
actions:
comment:
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
commands_restrictions:
backport:
conditions:
- or:
- sender-permission>=write
- sender=github-actions[bot]
+2 -7
View File
@@ -2,18 +2,13 @@
### Summary
### PR Checklists
- Do we need to backport this PR change to the [Harvester Dashboard](https://github.com/harvester/dashboard)?
- [ ] Yes, the relevant PR is at:
- Are backend engineers aware of UI changes?
- Are backend engineers aware of UI changes ?
- [ ] Yes, the backend owner is:
### Related Issue #
<!-- Define findings related to the feature or bug issue. -->
### Test screenshot/video
### Test screenshot or video
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->
### Extra technical notes summary
<!-- Outline technical changes which may pass unobserved or may help to understand the process of solving the issue -->
+40
View File
@@ -0,0 +1,40 @@
# .github/release.yml
changelog:
categories:
- title: Features
labels:
- feat
- title: Bug Fixes
labels:
- fix
- title: Dependencies
labels:
- deps
- title: Performance
labels:
- perf
- title: Documentation
labels:
- docs
- title: Style
labels:
- style
- title: Build & Continuous Integration
labels:
- build
- ci
- title: Security
labels:
- security
- title: Tests
labels:
- test
- title: Refactoring
labels:
- refactor
- title: Chores
labels:
- chore
- title: Others
labels:
- other
+47
View File
@@ -0,0 +1,47 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":semanticCommitTypeAll(deps)",
":semanticCommitScopeDisabled"
],
"baseBranches": [
"main",
"release-harvester-v1.7",
"release-harvester-v1.8"
],
"automergeMajor": false,
"semanticCommits": "enabled",
"semanticCommitType": "deps",
"prHourlyLimit": 12,
"timezone": "Asia/Taipei",
"schedule": ["after 10am on sunday"],
"postUpdateOptions": ["yarnDedupeFewer"],
"packageRules": [
{
"matchUpdateTypes": ["major"],
"enabled": false
},
{
"matchPackagePatterns": ["vue", "vue-router", "vuex"],
"matchUpdateTypes": ["major"],
"enabled": false
},
{
"matchPackageNames": ["@rancher/shell"],
"enabled": false
},
{
"matchUpdateTypes": ["minor"],
"groupName": "minor dependencies",
"labels": ["minor-update"],
"reviewers": ["a110605", "houhoucoop"]
},
{
"matchUpdateTypes": ["patch", "digest"],
"automerge": true,
"groupName": "patch digest dependencies",
"labels": ["patch-update", "automerge"]
}
]
}
@@ -0,0 +1,17 @@
name: "[PR Management] Auto Assign Reviewer & Assignee"
on:
pull_request_target:
types: [opened, ready_for_review]
permissions:
pull-requests: write
jobs:
auto-assign:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: rancher/gh-issue-mgr/auto-assign-action@b70f0bdf12a03e5e3f33e4f92ccb6c89deb3ebd9 # main
with:
configuration-path: .github/auto-assign-config.yaml
+94
View File
@@ -0,0 +1,94 @@
name: "[PR Management] Add Labels"
on:
pull_request_target:
types: [opened, reopened]
branches:
- main
- 'release-harvester-v*'
permissions:
pull-requests: write
jobs:
add-require-backport-label:
if: github.event.pull_request.draft == false &&
github.event.pull_request.base.ref == 'main'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.base_ref }}
- name: Fetch release branches and PR labels
id: fetch_info
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
repo="${{ github.repository }}"
pr_number=${{ github.event.pull_request.number }}
release_branches=$(gh api "repos/${repo}/branches" --paginate --jq '.[].name' | grep -E '^release-harvester-v[0-9]+\.[0-9]+$' || true)
if [[ -z "$release_branches" ]]; then
echo "should_label=false" >> "$GITHUB_OUTPUT"
exit 0
fi
latest_branch=$(echo "$release_branches" | sort -Vr | head -n1)
version="${latest_branch#release-harvester-v}"
release_tag="v${version}.0"
tags=$(gh api "repos/${repo}/releases" --paginate --jq '.[].tag_name')
if echo "$tags" | grep -Fxq "$release_tag"; then
echo "should_label=false" >> "$GITHUB_OUTPUT"
exit 0
fi
label="require backport/v${version}"
echo "should_label=true" >> "$GITHUB_OUTPUT"
echo "backport_label=$label" >> "$GITHUB_OUTPUT"
pr_labels=$(gh pr view "$pr_number" --repo "$repo" --json labels --jq '.labels[].name' || echo "")
pr_labels_csv=$(echo "$pr_labels" | tr '\n' ',' | sed 's/,$//')
echo "pr_labels=$pr_labels_csv" >> "$GITHUB_OUTPUT"
- name: Add label if needed
if: steps.fetch_info.outputs.should_label == 'true' && !contains(steps.fetch_info.outputs.pr_labels, steps.fetch_info.outputs.backport_label)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Adding label: ${{ steps.fetch_info.outputs.backport_label }}"
gh pr edit ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--add-label "${{ steps.fetch_info.outputs.backport_label }}"
add-backport-label:
if: github.event.pull_request.draft == false &&
startsWith(github.event.pull_request.base.ref, 'release-harvester-v')
runs-on: ubuntu-latest
steps:
- name: Check conditions for backport label
id: check
run: |
IS_MERGIFY=$(echo '${{ github.event.pull_request.user.login }}' | grep -iq 'mergify' && echo true || echo false)
TARGET_BRANCH=${{ github.event.pull_request.base.ref }}
echo "IS_MERGIFY=$IS_MERGIFY" >> $GITHUB_OUTPUT
echo "TARGET_BRANCH=$TARGET_BRANCH" >> $GITHUB_OUTPUT
- name: Add label if needed
if: steps.check.outputs.IS_MERGIFY == 'true' && startsWith(steps.check.outputs.TARGET_BRANCH, 'release-harvester-v')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TARGET_BRANCH="${{ steps.check.outputs.TARGET_BRANCH }}"
version="${TARGET_BRANCH#release-harvester-v}"
label="backport/v${version}"
echo "Adding label $label"
gh pr edit ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--add-label "$label"
+49
View File
@@ -0,0 +1,49 @@
name: "[PR Management] Request Backport via Mergify"
on:
pull_request_target:
types: [closed]
branches: [main]
permissions:
pull-requests: write
jobs:
comment-backport:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.base_ref }}
- name: Post Mergify backport command
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
pr_number=${{ github.event.pull_request.number }}
repo="${{ github.repository }}"
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
labels=$(echo "$labels_json" | jq -r '.[] // empty')
echo "Labels on PR: $labels"
matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true)
if [[ -z "$matches" ]]; then
echo "No back‑port labels found — skipping."
exit 0
fi
branches=$(echo "$matches" \
| sed -E 's/^require backport\/v/release-harvester-v/' \
| sort -u | tr '\n' ' ')
branches=${branches%% }
cmd="@Mergifyio backport $branches"
echo "Posting Mergify command: $cmd"
gh pr comment "$pr_number" --repo "$repo" --body "$cmd"
@@ -0,0 +1,36 @@
name: Build Harvester Catalog Image and Publish on Release
on:
workflow_dispatch:
release:
types: [published]
defaults:
run:
shell: bash
working-directory: ./
jobs:
check-version:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Check package version
env:
TAG_VERSION: ${{ github.event.release.tag_name }}
run: ./scripts/check-version.sh $TAG_VERSION
build-and-push-extension-catalog:
needs: check-version
uses: ./.github/workflows/build-extension-catalog.yml
permissions:
actions: write
contents: read
packages: write
id-token: write
with:
registry_target: docker.io
registry_user: rancher
secrets: inherit
@@ -1,4 +1,4 @@
name: Build Harvester Dashboard and Upload
name: Build Harvester Dashboard and Publish
on:
workflow_call:
@@ -25,12 +25,12 @@ jobs:
name: Build & Upload Hosted
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
with:
fetch-depth: 1
# Note - Cannot use the setup action here as it uses a different yarn install arg
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
@@ -45,19 +45,20 @@ jobs:
run: ./scripts/build-upload-gate
- name: Get gcs auth
uses: rancher-eio/read-vault-secrets@main
uses: rancher-eio/read-vault-secrets@0da85151ad1f19ed7986c41587e45aac1ace74b6 # v3
with:
secrets: |
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
- name: Apply gcs auth
# https://github.com/google-github-actions/auth
uses: 'google-github-actions/auth@v2'
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
with:
credentials_json: "${{ env.GOOGLE_AUTH }}"
- name: Upload build
uses: 'google-github-actions/upload-cloud-storage@v2'
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
# https://github.com/google-github-actions/upload-cloud-storage
with:
path: ${{steps.build-hosted.outputs.BUILD_HOSTED_DIR}}
@@ -71,12 +72,12 @@ jobs:
name: Build & Upload Embedded
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
with:
fetch-depth: 1
# Note - Cannot use the setup action here as it uses a different yarn install arg
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
@@ -89,19 +90,19 @@ jobs:
DISABLED_EMBED_PKG: https://releases.rancher.com/harvester-ui/plugin/harvester-1.0.3.tar.gz
- name: Get gcs auth
uses: rancher-eio/read-vault-secrets@main
uses: rancher-eio/read-vault-secrets@0da85151ad1f19ed7986c41587e45aac1ace74b6 # v3
with:
secrets: |
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
- name: Apply gcs auth
# https://github.com/google-github-actions/auth
uses: 'google-github-actions/auth@v2'
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
with:
credentials_json: "${{ env.GOOGLE_AUTH }}"
- name: Upload tar
uses: 'google-github-actions/upload-cloud-storage@v2'
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
with:
path: ${{steps.build-embedded.outputs.BUILD_EMBEDED_TGZ}}
destination: releases.rancher.com/harvester-ui/dashboard
@@ -114,12 +115,12 @@ jobs:
name: Build & Upload Plugin
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
with:
fetch-depth: 1
# Note - Cannot use the setup action here as it uses a different yarn install arg
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
@@ -133,19 +134,19 @@ jobs:
run: ./scripts/build-upload-gate
- name: Get gcs auth
uses: rancher-eio/read-vault-secrets@main
uses: rancher-eio/read-vault-secrets@0da85151ad1f19ed7986c41587e45aac1ace74b6 # v3
with:
secrets: |
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
- name: Apply gcs auth
# https://github.com/google-github-actions/auth
uses: 'google-github-actions/auth@v2'
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
with:
credentials_json: "${{ env.GOOGLE_AUTH }}"
- name: Upload plugin tar
uses: 'google-github-actions/upload-cloud-storage@v2'
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
with:
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_TARBALL}}
destination: releases.rancher.com/harvester-ui/plugin
@@ -155,7 +156,7 @@ jobs:
process_gcloudignore: false
- name: Upload plugin directory
uses: 'google-github-actions/upload-cloud-storage@v2'
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
with:
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_NAME}}
destination: releases.rancher.com/harvester-ui/plugin/${{steps.ci-build-pkg.outputs.PKG_NAME}}
@@ -1,21 +0,0 @@
name: Build Dashboard (Release)
on:
push:
tags:
- v[1-9].*
jobs:
build-validation:
name: Build Test
uses: ./.github/workflows/test.yaml
build:
name: Build and Upload Package
uses: ./.github/workflows/build-and-upload.yaml
needs:
- build-validation
permissions:
contents: read
packages: write
id-token: write
with:
CI_BUILD_TAG: ${{github.ref_name}}
@@ -0,0 +1,70 @@
name: Build and release Extension Catalog Image to registry
on:
workflow_call:
inputs:
registry_target:
required: true
type: string
registry_user:
required: true
type: string
outputs:
build-extension-catalog-job-status:
value: ${{ jobs.build-extension-catalog.outputs.build-status }}
jobs:
build-and-push-extension-catalog:
name: Build container image
if: github.ref_type == 'tag' || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
packages: write
id-token: write
outputs:
build-status: ${{ job.status }}
steps:
- name: Read Secrets
uses: rancher-eio/read-vault-secrets@0da85151ad1f19ed7986c41587e45aac1ace74b6 # v3
with:
secrets: |
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME ;
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD ;
- name: Checkout repository (normal flow)
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Enable Corepack
run: corepack enable
- name: Configure Git
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
username: ${{ env.DOCKER_USERNAME }}
password: ${{ env.DOCKER_PASSWORD }}
- name: Setup Helm
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
with:
version: v3.8.0
- name: Setup Nodejs with yarn caching
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: yarn
- name: Install dependencies
run: yarn
- name: Build and push UI image
run: |
publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
$publish
@@ -1,34 +0,0 @@
name: Build and Release Extension Charts
on:
workflow_dispatch:
release:
types: [released]
defaults:
run:
shell: bash
working-directory: ./
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run tests
uses: ./.github/actions/lint
build-extension-charts:
needs:
- lint
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
permissions:
actions: write
contents: write
deployments: write
pages: write
with:
target_branch: gh-pages
tagged_release: ${{ github.ref_name }}
@@ -0,0 +1,140 @@
name: Build and Release Extension Charts on PR Merge
on:
push:
branches:
- 'release-harvester-v*'
- 'main'
jobs:
setup-target-branch:
runs-on: ubuntu-latest
outputs:
target_branch: ${{ steps.get-version.outputs.target_branch }}
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Determine target branch
id: get-version
run: |
if [[ "${{ github.ref_name }}" == "main" ]]; then
TARGET_BRANCH="main-head"
elif [[ "${{ github.ref_name }}" =~ ^release-harvester-v([0-9]+\.[0-9]+)$ ]]; then
TARGET_BRANCH="v${BASH_REMATCH[1]}-head"
else
echo "Error: invalid branch format." && exit 1
fi
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_ENV
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_OUTPUT
- name: Ensure target branch exists
run: |
git fetch --all
if ! git ls-remote --exit-code --heads origin "${{ env.target_branch }}"; then
git checkout gh-pages
git checkout -b "${{ env.target_branch }}"
git push origin "${{ env.target_branch }}"
fi
extract-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Extract version from package.json
id: get_version
run: |
VERSION=$(jq -r '.version' pkg/harvester/package.json)
echo "VERSION=${VERSION}" >> $GITHUB_ENV
echo "version=${VERSION}" >> $GITHUB_OUTPUT
build-extension-charts:
needs:
- setup-target-branch
- extract-version
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup environment
run: |
corepack enable
yarn install --frozen-lockfile
- name: Setup Helm
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
with:
version: v3.8.0
- name: Build Helm charts
run: |
yarn publish-pkgs -s ${{ github.repository }} -b ${{ needs.setup-target-branch.outputs.target_branch }} -t harvester-${{ needs.extract-version.outputs.version }}
- name: Upload charts artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: charts
path: tmp
release:
needs:
- setup-target-branch
- extract-version
- build-extension-charts
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout release branch
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: '${{ github.ref_name }}'
- name: Get last commit hash
id: last_commit
run: |
LAST_COMMIT=$(git rev-parse HEAD)
echo "LAST_COMMIT=${LAST_COMMIT}" >> $GITHUB_ENV
- name: Checkout target branch
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: '${{ needs.setup-target-branch.outputs.target_branch }}'
- name: Remove old artifacts
run: |
rm -rf extensions/harvester/${{ needs.extract-version.outputs.version }}
rm -rf charts/harvester/${{ needs.extract-version.outputs.version }}
rm -f assets/harvester/harvester-${{ needs.extract-version.outputs.version }}.tgz
- name: Configure Git
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
- name: Download build artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: charts
- name: Commit and push artifacts
run: |
git add ./{assets,charts,extensions,index.yaml}
git commit -m "CI Build Artifacts (commit: ${{ env.LAST_COMMIT }}, version: ${{ needs.extract-version.outputs.version }})"
git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
- name: Run Helm chart releaser
uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0
with:
charts_dir: ./charts
env:
CR_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
CR_SKIP_EXISTING: true
@@ -0,0 +1,54 @@
name: Build and Release Extension Charts on Release
on:
workflow_dispatch:
release:
types: [released]
defaults:
run:
shell: bash
working-directory: ./
jobs:
setup-release-tag:
runs-on: ubuntu-latest
outputs:
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Determine release tag
id: determine_tag
run: |
if [[ "${{ github.event.release.tag_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
RELEASE_TAG="harvester-${BASH_REMATCH[1]}"
echo "${RELEASE_TAG}"
echo "release_tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT
else
echo "Error: invalid tag format." && exit 1
fi
check-version:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Check package version
env:
TAG_VERSION: ${{ github.event.release.tag_name }}
run: ./scripts/check-version.sh $TAG_VERSION
build-extension-charts:
needs:
- setup-release-tag
- check-version
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@9eb70a732e9be146722e1dbab431338366c2afc6 # creators-pkg-v3.0.10
permissions:
actions: write
contents: write
deployments: write
pages: write
with:
target_branch: gh-pages
tagged_release: '${{ needs.setup-release-tag.outputs.release_tag }}'
@@ -1,4 +1,4 @@
name: Build Dashboard (Branch)
name: Build Standalone on PR Merge
on:
push:
@@ -17,10 +17,10 @@ on:
jobs:
build-validation:
name: Build Test
uses: ./.github/workflows/test.yaml
uses: ./.github/workflows/run-lint.yaml
build:
name: Build and Upload Package
uses: ./.github/workflows/build-and-upload.yaml
uses: ./.github/workflows/build-and-publish-standalone.yaml
needs:
- build-validation
permissions:
@@ -0,0 +1,27 @@
name: Build Standalone on Release
on:
push:
tags:
- v[1-9].*
jobs:
check-version:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Check package version
env:
TAG_VERSION: ${{github.ref_name}}
run: ./scripts/check-version.sh $TAG_VERSION
build:
name: Build and Upload Package
uses: ./.github/workflows/build-and-publish-standalone.yaml
needs: check-version
permissions:
contents: read
packages: write
id-token: write
with:
CI_BUILD_TAG: ${{github.ref_name}}
+34
View File
@@ -0,0 +1,34 @@
name: FOSSA Scanning
on:
push:
branches: ["main", "release-harvester-v*"]
workflow_dispatch:
permissions:
contents: read
id-token: write
jobs:
fossa-scanning:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# The FOSSA token is shared between all repos in Harvester's GH org. It can
# be used directly and there is no need to request specific access to EIO.
- name: Read FOSSA token
uses: rancher-eio/read-vault-secrets@0da85151ad1f19ed7986c41587e45aac1ace74b6 # v3
with:
secrets: |
secret/data/github/org/harvester/fossa/credentials token | FOSSA_API_KEY_PUSH_ONLY
- name: FOSSA scan
uses: fossas/fossa-action@c414b9ad82eaad041e47a7cf62a4f02411f427a0 # v1.8.0
with:
api-key: ${{ env.FOSSA_API_KEY_PUSH_ONLY }}
# Only runs the scan and do not provide/returns any results back to the
# pipeline.
run-tests: false
+30
View File
@@ -0,0 +1,30 @@
name: "[PR Management] Add PR Label"
on:
pull_request_target:
types: [opened, reopened, edited]
branches:
- main
- 'release-harvester-v*'
permissions:
pull-requests: write
jobs:
auto-assign-pr-label:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.base_ref }}
- name: Setup Nodejs and yarn install
uses: ./.github/actions/setup
- name: Set PR label
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
echo "PR_LABEL = $PR_LABEL"
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "$PR_LABEL"
+73
View File
@@ -0,0 +1,73 @@
name: Tests
on:
workflow_call: # This tells GH that the workflow is reusable
push:
branches:
- main
- 'release-*'
pull_request:
types: [opened, reopened, edited, synchronize]
branches:
- main
- 'release-*'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0 # Need full history for commit-lint
- name: Run tests
uses: ./.github/actions/lint
- name: Validate PR title lint
if: github.event_name == 'pull_request'
shell: bash
run: echo "${{ github.event.pull_request.title }}" | npx commitlint
- name: Validate commit messages
shell: bash
run: |
echo "GITHUB_EVENT_NAME=$GITHUB_EVENT_NAME"
echo "GITHUB_BASE_SHA=$GITHUB_BASE_SHA"
echo "GITHUB_HEAD_SHA=$GITHUB_HEAD_SHA"
echo "GITHUB_BEFORE=$GITHUB_BEFORE"
echo "GITHUB_AFTER=$GITHUB_AFTER"
if [ "$GITHUB_EVENT_NAME" = "pull_request" ] && [ -n "$GITHUB_BASE_SHA" ] && [ -n "$GITHUB_HEAD_SHA" ]; then
FROM="$GITHUB_BASE_SHA"
TO="$GITHUB_HEAD_SHA"
elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then
if [ "$GITHUB_BEFORE" = "0000000000000000000000000000000000000000" ]; then
# first push to HEAD
FROM=""
TO="$GITHUB_AFTER"
else
FROM="$GITHUB_BEFORE"
TO="$GITHUB_AFTER"
fi
else
echo "No valid commit range found, skipping commitlint."
exit 0
fi
echo "FROM=$FROM"
echo "TO=$TO"
if [ -z "$FROM" ]; then
echo "Linting last commit $TO"
npx commitlint --last --verbose
else
echo "Linting commits from $FROM to $TO"
npx commitlint --from "$FROM" --to "$TO" --verbose
fi
env:
GITHUB_EVENT_NAME: ${{ github.event_name }}
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
GITHUB_BEFORE: ${{ github.event.before }}
GITHUB_AFTER: ${{ github.event.after }}
-23
View File
@@ -1,23 +0,0 @@
name: Tests
on:
workflow_call: # This tells GH that the workflow is reusable
push:
branches:
- main
- 'release-*'
pull_request:
branches:
- main
- 'release-*'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run tests
uses: ./.github/actions/lint
+1
View File
@@ -0,0 +1 @@
npx --no -- commitlint --edit $1
+1
View File
@@ -0,0 +1 @@
yarn lint
+2
View File
@@ -0,0 +1,2 @@
@a110605
@houhoucoop
+133 -40
View File
@@ -1,76 +1,169 @@
# harvester-ui-extension
Rancher Extension used in [rancher/dashboard](https://github.com/rancher/dashboard) for [Harvester](https://harvesterhci.io) Dashboard UI.
The Harvester UI Extension is a Rancher extension that provides the user interface for [Harvester](https://harvesterhci.io) within the [Rancher Dashboard](https://github.com/rancher/dashboard).
`
From Rancher 2.10.0, Harvester integration is provided by this new Harvester UI Extension.
`
> **Note:**
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
## Installation
To install the Harvester UI Extension in Rancher 2.10.0 and newer:
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
- Go to 'Extensions' in the Rancher UI (click on the jigsaw icon in the side bar)
- From the three-dots menu in the upper right of the page, open the menu and select 'Manage Repositories'
- From the Repositories page, click 'Create'
- For name, enter 'harvester-ui-extension'
- For Target, choose 'Git repository'
- For Git Repo URL, enter `https://github.com/harvester/harvester-ui-extension`
- For Git Branch, enter `gh-pages`
- Click 'Create`
- From the Repositories list, wait until the new Repository added updates to the 'Active' state
- Go back to 'Extensions'
- Click on the 'Available' tab if not already selected
- Click on 'Install' for the Harvester UI Extension
## Running for Development
This is what you probably want to get started.
### Prerequisite
Nodejs version >= 20
## Development Setup
Ensure **Node.js v20 or later** is installed for development and debugging.
### Standalone Mode
For development, serve with hot reload at https://localhost:8005
Run the extension standalone with hot reload at `https://localhost:8005`.
```bash
# Install dependencies
yarn install
# Use RANCHER_ENV to specify the endpoint for your Harvester API
# Start the development server
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
# Use VUE_APP_SERVER_VERSION to specify the Harvester server version
# in case your are running a dev build of Harvester
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=your-harvester-server-version API=https://your-harvester-ip yarn dev
# Example with specific server version
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev
```
# or put your env variables into a .env file
You may also define environment variables in a `.env` file:
# Example
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.4.0 API=https://192.168.1.123 yarn dev
```env
RANCHER_ENV=harvester
VUE_APP_SERVER_VERSION=v1.5.0
API=https://192.168.1.123
```
### Rancher Integration Mode
In this mode, you need to have Rancher and run Harvester UI Extension as Rancher extension, see more detail in [Rancher UI extension](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
To run as a Rancher extension, follow the [Rancher UI Extension Guide](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
```bash
API=https://your-rancher-ip yarn dev
# Example
API=https://192.168.1.123 yarn dev
```
## Commit Message Guidelines
This project uses [commit-lint](https://commitlint.js.org/) with [Conventional Commits](https://www.conventionalcommits.org/) to ensure consistent and meaningful commit messages.
### Commit Message Format
All commit messages must follow the conventional commit format:
```
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
```
### Supported Types
- **feat**: New features
- **fix**: Bug fixes
- **docs**: Documentation changes
- **style**: Code style changes (formatting, missing semicolons, etc.)
- **refactor**: Code refactoring
- **perf**: Performance improvements
- **test**: Adding or updating tests
- **build**: Build system or external dependencies
- **ci**: CI/CD changes
- **chore**: Other changes that don't modify src or test files
- **revert**: Reverts a previous commit
- **wip**: Work in progress
- **deps**: Dependency updates
- **security**: Security fixes
### Examples
```bash
# Feature
git commit -m "feat: add new virtual machine creation wizard"
# Bug fix
git commit -m "fix: resolve memory leak in VM console"
# Documentation
git commit -m "docs: update installation instructions"
# Breaking change
git commit -m "feat!: change API endpoint structure
BREAKING CHANGE: The /api/v1/vms endpoint has been replaced with /api/v2/vms"
```
### Git Hooks
The project uses [Husky](https://typicode.github.io/husky/) to automatically validate commit messages and run linting before commits:
- **pre-commit**: Runs ESLint to ensure code quality
- **commit-msg**: Validates commit message format using commit-lint
These hooks are automatically installed when you run `yarn install`.
### Manual Validation
You can manually validate commit messages:
```bash
# Validate the last commit
yarn commitlint
# Validate a specific commit
npx commitlint --from <commit-hash>
# Validate a range of commits
npx commitlint --from <start-hash> --to <end-hash>
```
## Branch Structure
- **`main`** – Main development branch
- **`release-harvester-vX.Y`** – Stable release branches per version series
- **`vX.Y-head`** – Testing branches for ongoing changes to extension builds in each release series
> **Note:**
> The `vX.Y-head` branches are auto-generated and kept in sync with release branches. Use these for testing the latest changes in each version series.
## Testing Guidelines
### UI Extension Testing
To validate changes in a release series, switch to the appropriate `vX.Y-head` branch. For main branch testing, use `main-head`.
- Examples:
- Test `1.0.x` series → `v1.0-head`
- Test `1.5.x` series → `v1.5-head`
**Steps:**
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
2. Refresh the Harvester repository using the target `vX.Y-head` branch
3. Go to the **Extensions** page and install the desired version
### Standalone Mode Testing
To test the standalone UI, configure Harvester to load the UI from an external source.
- Examples of `ui-index`:
- Main branch → `https://releases.rancher.com/harvester-ui/dashboard/latest/index.html`
- Release series `1.5.x` → `https://releases.rancher.com/harvester-ui/dashboard/release-harvester-v1.5/index.html`
**Steps:**
1. Go to **Harvester UI** → **Advanced** → **Settings** → **UI**
2. Set **ui-source** to `External`
3. Set **ui-index** to the desired URL
## Contributing
For developers, after reading through the introduction on this page, head over to our [Getting Started](https://extensions.rancher.io/extensions/next/extensions-getting-started) guide to learn more.
If you want to contribute, start by reading this document, then visit our [Getting Started guide](https://extensions.rancher.io/extensions/next/extensions-getting-started) to learn how to develop and submit changes.
License
=======
Copyright (c) 2014-2023 [Rancher Labs, Inc.](http://rancher.com)
## License
Copyright (c) 2014-2026 [SUSE, LLC.](https://www.suse.com/)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+47
View File
@@ -0,0 +1,47 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
// Enforce conventional commit format
'type-enum': [
2,
'always',
[
'feat', // New features
'fix', // Bug fixes
'docs', // Documentation changes
'style', // Code style changes (formatting, missing semicolons, etc.)
'refactor', // Code refactoring
'perf', // Performance improvements
'test', // Adding or updating tests
'build', // Build system or external dependencies
'ci', // CI/CD changes
'chore', // Other changes that don't modify src or test files
'revert', // Reverts a previous commit
'wip', // Work in progress
'deps', // Dependency updates
'security', // Security fixes
]
],
// Allows the scope to be empty. [0] means the rule is disabled.
'scope-empty': [0, 'always'],
// Allows any string for the scope.
'scope-case': [2, 'always', 'kebab-case'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'subject-case': [0, 'never'],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'subject-max-length': [0, 'never'],
'body-leading-blank': [2, 'always'],
'body-max-line-length': [2, 'always', 100],
'footer-leading-blank': [2, 'always'],
'footer-max-line-length': [2, 'always', 100],
},
// Ignore merge commits and revert commits
ignores: [
(commit) => commit.includes('Merge'),
(commit) => commit.includes('Revert'),
(commit) => commit.includes('merge'),
(commit) => commit.includes('revert'),
],
};
+26 -13
View File
@@ -1,35 +1,41 @@
{
"name": "harvester-ui-extension",
"version": "0.1.0",
"version": "1.8.0-rc4",
"private": false,
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@rancher/shell": "3.0.1-rc.4",
"@babel/plugin-transform-class-static-block": "7.28.6",
"@rancher/shell": "3.0.9-rc.6",
"@vue-flow/background": "^1.3.0",
"@vue-flow/controls": "^1.1.1",
"@vue-flow/core": "^1.33.5",
"@vue-flow/minimap": "^1.4.0",
"cache-loader": "^4.1.0",
"color": "4.2.3",
"ip": "2.0.1",
"node-polyfill-webpack-plugin": "^3.0.0",
"elkjs": "^0.11.0",
"vue-draggable-next": "^2.2.1",
"yaml": "^2.5.1"
},
"resolutions": {
"@types/node": "~20.10.0",
"cronstrue": "2.50.0",
"@types/node": "~20.19.0",
"cronstrue": "2.59.0",
"d3-color": "3.1.0",
"ejs": "3.1.9",
"follow-redirects": "1.15.2",
"ejs": "3.1.10",
"follow-redirects": "1.15.11",
"glob": "7.2.3",
"glob-parent": "6.0.2",
"json5": "2.2.3",
"@types/lodash": "4.17.5",
"@types/lodash": "4.17.24",
"merge": "2.1.1",
"node-forge": "1.3.1",
"node-forge": "1.3.3",
"nth-check": "2.1.1",
"qs": "6.11.1",
"roarr": "7.0.4",
"semver": "7.5.4",
"qs": "6.15.0",
"roarr": "7.21.4",
"semver": "7.7.4",
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
},
"scripts": {
@@ -37,13 +43,20 @@
"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 .",
"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"
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
"commitlint": "commitlint --edit",
"prepare": "husky"
},
"devDependencies": {
"@commitlint/load": "^19.8.1",
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^7.1.0"
"eslint-plugin-promise": "^7.1.0",
"husky": "^9.1.7"
}
}
+177
View File
@@ -0,0 +1,177 @@
# harvester-ui-extension
The Harvester UI Extension is a Rancher extension that provides the user interface for [Harvester](https://harvesterhci.io) within the [Rancher Dashboard](https://github.com/rancher/dashboard).
> **Note:**
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
## Installation
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
## Development Setup
Ensure **Node.js v20 or later** is installed for development and debugging.
### Standalone Mode
Run the extension standalone with hot reload at `https://localhost:8005`.
```bash
# Install dependencies
yarn install
# Start the development server
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
# Example with specific server version
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev
```
You may also define environment variables in a `.env` file:
```env
RANCHER_ENV=harvester
VUE_APP_SERVER_VERSION=v1.5.0
API=https://192.168.1.123
```
### Rancher Integration Mode
To run as a Rancher extension, follow the [Rancher UI Extension Guide](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
```bash
API=https://your-rancher-ip yarn dev
```
## Commit Message Guidelines
This project uses [commit-lint](https://commitlint.js.org/) with [Conventional Commits](https://www.conventionalcommits.org/) to ensure consistent and meaningful commit messages.
### Commit Message Format
All commit messages must follow the conventional commit format:
```
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
```
### Supported Types
- **feat**: New features
- **fix**: Bug fixes
- **docs**: Documentation changes
- **style**: Code style changes (formatting, missing semicolons, etc.)
- **refactor**: Code refactoring
- **perf**: Performance improvements
- **test**: Adding or updating tests
- **build**: Build system or external dependencies
- **ci**: CI/CD changes
- **chore**: Other changes that don't modify src or test files
- **revert**: Reverts a previous commit
- **wip**: Work in progress
- **deps**: Dependency updates
- **security**: Security fixes
### Examples
```bash
# Feature
git commit -m "feat: add new virtual machine creation wizard"
# Bug fix
git commit -m "fix: resolve memory leak in VM console"
# Documentation
git commit -m "docs: update installation instructions"
# Breaking change
git commit -m "feat!: change API endpoint structure
BREAKING CHANGE: The /api/v1/vms endpoint has been replaced with /api/v2/vms"
```
### Git Hooks
The project uses [Husky](https://typicode.github.io/husky/) to automatically validate commit messages and run linting before commits:
- **pre-commit**: Runs ESLint to ensure code quality
- **commit-msg**: Validates commit message format using commit-lint
These hooks are automatically installed when you run `yarn install`.
### Manual Validation
You can manually validate commit messages:
```bash
# Validate the last commit
yarn commitlint
# Validate a specific commit
npx commitlint --from <commit-hash>
# Validate a range of commits
npx commitlint --from <start-hash> --to <end-hash>
```
## Branch Structure
- **`main`** – Main development branch
- **`release-harvester-vX.Y`** – Stable release branches per version series
- **`vX.Y-head`** – Testing branches for ongoing changes to extension builds in each release series
> **Note:**
> The `vX.Y-head` branches are auto-generated and kept in sync with release branches. Use these for testing the latest changes in each version series.
## Testing Guidelines
### UI Extension Testing
To validate changes in a release series, switch to the appropriate `vX.Y-head` branch. For main branch testing, use `main-head`.
- Examples:
- Test `1.0.x` series → `v1.0-head`
- Test `1.5.x` series → `v1.5-head`
**Steps:**
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
2. Refresh the Harvester repository using the target `vX.Y-head` branch
3. Go to the **Extensions** page and install the desired version
### Standalone Mode Testing
To test the standalone UI, configure Harvester to load the UI from an external source.
- Examples of `ui-index`:
- Main branch → `https://releases.rancher.com/harvester-ui/dashboard/latest/index.html`
- Release series `1.5.x` → `https://releases.rancher.com/harvester-ui/dashboard/release-harvester-v1.5/index.html`
**Steps:**
1. Go to **Harvester UI** → **Advanced** → **Settings** → **UI**
2. Set **ui-source** to `External`
3. Set **ui-index** to the desired URL
## Contributing
If you want to contribute, start by reading this document, then visit our [Getting Started guide](https://extensions.rancher.io/extensions/next/extensions-getting-started) to learn how to develop and submit changes.
## License
Copyright (c) 2014-2026 [SUSE, LLC.](https://www.suse.com/)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+9 -1
View File
@@ -1 +1,9 @@
module.exports = require('@rancher/shell/pkg/babel.config');
const baseConfig = require('@rancher/shell/pkg/babel.config');
module.exports = {
...baseConfig,
plugins: [
...(baseConfig.plugins || []),
'@babel/plugin-transform-class-static-block'
]
};
+35 -8
View File
@@ -5,8 +5,10 @@ import { Checkbox } from '@components/Form/Checkbox';
import ModalWithCard from '@shell/components/ModalWithCard';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { Banner } from '@components/Banner';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { HCI } from '../types';
import UpgradeInfo from './UpgradeInfo';
export default {
name: 'HarvesterUpgrade',
@@ -28,14 +30,15 @@ export default {
data() {
return {
upgrade: [],
upgradeMessage: [],
errors: '',
selectMode: true,
version: '',
enableLogging: true,
readyReleaseNote: false,
isOpen: false
upgrade: [],
upgradeMessage: [],
errors: '',
selectMode: true,
version: '',
enableLogging: true,
skipSingleReplicaDetachedVol: false,
readyReleaseNote: false,
isOpen: false
};
},
@@ -68,6 +71,10 @@ export default {
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG);
},
skipSingleReplicaDetachedVolFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
},
releaseLink() {
return `https://github.com/harvester/harvester/releases/tag/${ this.version }`;
}
@@ -104,6 +111,11 @@ export default {
spec: { version: this.version }
};
if (this.skipSingleReplicaDetachedVolFeatureEnabled && this.skipSingleReplicaDetachedVol) {
upgradeValue.metadata.annotations =
{ [HCI_ANNOTATIONS.SKIP_SINGLE_REPLICA_DETACHED_VOL]: JSON.stringify(this.skipSingleReplicaDetachedVol) };
}
if (this.canEnableLogging) {
upgradeValue.spec.logEnabled = this.enableLogging;
}
@@ -190,6 +202,21 @@ export default {
/>
</div>
<div
v-if="skipSingleReplicaDetachedVolFeatureEnabled"
class="mb-5"
>
<Checkbox
v-model:value="skipSingleReplicaDetachedVol"
class="check"
type="checkbox"
:label="t('harvester.upgradePage.skipSingleReplicaDetachedVol')"
/>
</div>
<hr
v-if="version"
class="divider"
/>
<div v-if="version">
<p
v-clean-html="t('harvester.upgradePage.releaseTip', {url: releaseLink}, true)"
@@ -190,6 +190,7 @@ export default {
v-clean-tooltip="{
placement: 'bottom-left',
}"
popper-class="upgrade-header-dropdown"
class="hand"
>
<slot name="button-content">
@@ -272,7 +273,7 @@ export default {
<p
v-if="overallMessage"
class="text-warning mb-20"
class="text-error mb-20"
>
{{ overallMessage }}
</p>
@@ -300,14 +301,14 @@ export default {
<ProgressBarList
:title="t('harvester.upgradePage.upgradeNode')"
:precent="nodesPercent"
:percent="nodesPercent"
:list="nodesStatus"
/>
<p class="bordered-section"></p>
<ProgressBarList
:title="t('harvester.upgradePage.upgradeSysService')"
:precent="sysServiceTotal"
:percent="sysServiceTotal"
:list="sysServiceUpgradeMessage"
/>
</div>
@@ -338,6 +339,12 @@ export default {
</div>
</template>
<style lang="scss">
.upgrade-header-dropdown .v-popper__arrow-container {
display: none;
}
</style>
<style lang="scss" scoped>
a {
float: right;
@@ -1,6 +1,8 @@
<script>
import Collapse from '@shell/components/Collapse';
import PercentageBar from '@shell/components/PercentageBar';
import { HCI } from '../types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
export default {
name: 'HarvesterUpgradeProgressList',
@@ -12,7 +14,7 @@ export default {
default: ''
},
precent: {
percent: {
type: Number,
default: 0
},
@@ -25,13 +27,45 @@ export default {
}
},
async fetch() {
await this.$store.dispatch('harvester/findAll', { type: HCI.UPGRADE });
},
data() {
return { open: true };
},
computed: {
showResumeButton() {
return this.title === 'Upgrading Node';
},
latestUpgradeCR() {
return this.$store.getters['harvester/all'](HCI.UPGRADE).find( (U) => U.isLatestUpgrade);
},
resumeUpgradePausedNodeEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('resumeUpgradePausedNode');
},
},
methods: {
handleSwitch() {
this.open = !this.open;
},
async resumeNodeUpgrade(nodeName) {
if (!this.latestUpgradeCR || !nodeName) return;
try {
const upgradePauseMapString = this.latestUpgradeCR.metadata.annotations[HCI_ANNOTATIONS.NODE_UPGRADE_PAUSE_MAP] || '{}';
const upgradePauseMap = JSON.parse(upgradePauseMapString);
// update the upgrade CR annotation harvesterhci.io/node-upgrade-pause-map to unpause the node upgrade process
upgradePauseMap[`${ nodeName }`] = 'unpause';
this.latestUpgradeCR.setAnnotation(HCI_ANNOTATIONS.NODE_UPGRADE_PAUSE_MAP, JSON.stringify(upgradePauseMap));
await this.latestUpgradeCR.save();
} catch (e) {
console.error(`unable to update harvester upgrade CR annotations: ${ this.latestUpgradeCR.id }.`, e); // eslint-disable-line no-console
return false;
}
}
}
};
@@ -39,16 +73,18 @@ export default {
<template>
<div class="bar-list">
<h4>{{ title }} <span class="float-r text-info">{{ precent }}%</span></h4>
<h4>{{ title }} <span class="float-r text-info">{{ percent }}%</span></h4>
<div>
<div>
<Collapse v-model:open="open">
<template #title>
<div class="total-bar">
<span class="bar"><PercentageBar
:model-value="precent"
preferred-direction="MORE"
/></span>
<span class="bar">
<PercentageBar
:model-value="percent"
preferred-direction="MORE"
/>
</span>
<span
class="on-off"
@click="handleSwitch"
@@ -56,28 +92,42 @@ export default {
</div>
</template>
<template>
<div class="custom-content">
<div
v-for="(item, i) in list"
:key="i"
>
<p>
{{ item.name }} <span
<div class="custom-content">
<div
v-for="(item, i) in list"
:key="i"
>
<div class="upgrade-node-header">
<div class="upgrade-node-title">
<p>
{{ item.name }}
</p>
<span
class="status"
:class="{ [item.state]: true }"
>{{ item.state }}</span>
</p>
<PercentageBar
:model-value="item.percent"
preferred-direction="MORE"
/>
<p class="warning">
{{ item.message }}
</p>
>
{{ item.state }}
</span>
</div>
<button
v-if="showResumeButton && resumeUpgradePausedNodeEnabled && item.state === 'Node-upgrade paused'"
type="button"
class="btn bg-info btn-sm"
data-testid="add-item"
@click="resumeNodeUpgrade(item.name)"
>
{{ t('action.resume') }}
</button>
</div>
<PercentageBar
:model-value="item.percent"
preferred-direction="MORE"
/>
<p class="warning">
{{ item.message }}
</p>
</div>
</template>
</div>
</Collapse>
</div>
</div>
@@ -93,7 +143,7 @@ export default {
.total-bar {
display: flex;
user-select: none;
.bar {
> .bar {
width: 85%;
}
.on-off {
@@ -102,25 +152,35 @@ export default {
}
}
.custom-content {
.item {
margin-bottom: 14px;
p {
margin-bottom: 4px;
}
.status {
float: right;
}
.Succeeded, .Upgrading, .Pending {
color: var(--success);
}
.failed {
color: var(--error)
}
.warning {
color: var(--error);
}
.upgrade-node-title {
flex: 1 0 80%;
margin-right: 10px;
display: flex;
justify-content: space-between;
}
.upgrade-node-header {
display:flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
margin-bottom: 14px;
.status {
float: right;
}
.Succeeded, .Upgrading, .Pending {
color: var(--success);
}
.failed {
color: var(--error)
}
.warning {
color: var(--error);
margin-bottom: 8px;
margin-top: 4px;
}
}
}
</style>
+128 -28
View File
@@ -1,14 +1,16 @@
<script>
import { mapGetters } from 'vuex';
import ActionMenu from '@shell/components/ActionMenuShell';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import { HCI_ALLOWED_SETTINGS, HCI_SETTING } from '../config/settings';
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
import { DOC } from '../config/doc-links';
import { docLink } from '../utils/feature-flags';
const CATEGORY = {
ui: [
'branding',
'ui-source',
'ui-plugin-index',
'ui-index',
]
};
@@ -19,6 +21,7 @@ export default {
components: {
AsyncButton,
Banner,
ActionMenu
},
props: {
@@ -30,15 +33,23 @@ export default {
category: {
type: String,
required: true,
},
searchQuery: {
type: String,
default: ''
}
},
data() {
const categorySettings = this.filterCategorySettings();
const filteredSettings = this.filterSearchSettings(categorySettings, this.searchQuery);
return {
HCI_SETTING,
categorySettings,
filteredSettings,
originalHideMap: this.createHideMap(categorySettings)
};
},
@@ -48,12 +59,85 @@ export default {
settings: {
deep: true,
handler() {
this['categorySettings'] = this.filterCategorySettings();
this.categorySettings = this.filterCategorySettings();
this.filteredSettings = this.filterSearchSettings(this.categorySettings, this.searchQuery);
}
},
searchQuery: {
immediate: true,
handler(newQuery) {
const filtered = this.filterSearchSettings(this.categorySettings, newQuery);
this.filteredSettings = newQuery ? this.openJsonSettings(filtered) : filtered.map((s) => ({ ...s, hide: this.originalHideMap[s.id] ?? false }));
}
}
},
methods: {
createHideMap(settings = []) {
const map = settings.reduce((acc, s) => {
acc[s.id] = s.hide ?? false;
return acc;
}, {} );
return map;
},
filterSearchSettings(settings, searchKey) {
if (!searchKey) {
return this.filterCategorySettings();
}
const searchQuery = searchKey.toLowerCase();
return settings.filter((setting) => {
const id = setting.id?.toLowerCase() || '';
// filter by id
if (id.includes(searchQuery) ) {
return true;
}
let description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || '';
// remove <a> tags from description
if (description.includes('<a')) {
description = description.replace(/<a[^>]*>(.*?)<\/a>/g, '$1');
}
// filter by description
if (description.includes(searchQuery)) {
return true;
}
// filter by customized value
if (setting.customized === true && setting.data?.value) {
const value = setting.data.value?.toLowerCase() || '';
return value.includes(searchQuery);
}
// filter by json value
if (setting.kind === 'json' && setting.json) {
try {
const json = JSON.parse(setting.json);
const jsonString = JSON.stringify(json).toLowerCase();
return jsonString.includes(searchQuery);
} catch (e) {
console.error(`${ setting.id }: wrong format`, e); // eslint-disable-line no-console
return false;
}
}
// filter by default value
if (setting.data?.default) {
return setting.data?.default.includes(searchQuery);
}
return false;
});
},
filterCategorySettings() {
return this.settings.filter((s) => {
if (!this.getFeatureEnabled(s.featureFlag)) {
@@ -74,25 +158,21 @@ export default {
return id ? this.$store.getters['harvester-common/getFeatureEnabled'](id) : true;
},
showActionMenu(e, setting) {
const actionElement = e.srcElement;
this.$store.commit(`action-menu/show`, {
resources: setting.data,
elem: actionElement
});
},
getSettingOption(id) {
return HCI_ALLOWED_SETTINGS.find((setting) => setting.id === id);
},
openJsonSettings(settings) {
return settings.map((s) => s.hide ? { ...s, hide: false } : s);
},
toggleHide(s) {
this.categorySettings.find((setting) => {
if (setting.id === s.id) {
setting.hide = !setting.hide;
}
});
const setting = this.filteredSettings.find((setting) => setting.id === s.id);
if (setting) {
setting.hide = !setting.hide;
this.originalHideMap[setting.id] = setting.hide;
}
},
async testConnect(buttonDone, value) {
@@ -118,6 +198,19 @@ export default {
}
buttonDone(false);
}
},
getDocLinkParams(setting) {
const settingConfig = HCI_ALLOWED_SETTINGS[setting.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[setting.id];
if (settingConfig?.docPath) {
const version = this.$store.getters['harvester-common/getServerVersion']();
const url = docLink(DOC[settingConfig.docPath], version);
return { url };
}
return {};
}
},
};
@@ -126,7 +219,7 @@ export default {
<template>
<div>
<div
v-for="(setting, i) in categorySettings"
v-for="(setting, i) in filteredSettings"
:key="i"
class="advanced-setting mb-20"
>
@@ -148,7 +241,7 @@ export default {
Experimental
</span>
</h1>
<h2 v-clean-html="t(setting.description, {}, true)">
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
</h2>
</div>
<div
@@ -156,15 +249,12 @@ export default {
:id="setting.id"
class="action"
>
<button
aria-haspopup="true"
aria-expanded="false"
type="button"
class="btn btn-sm role-multi-action actions"
@click="showActionMenu($event, setting)"
>
<i class="icon icon-actions" />
</button>
<ActionMenu
:resource="setting.data"
:button-aria-label="t('advancedSettings.edit.label')"
data-testid="action-button"
button-role="tertiary"
/>
</div>
</div>
<div value>
@@ -221,6 +311,12 @@ export default {
{{ setting.data.errMessage }}
</Banner>
</div>
<div
v-if="filteredSettings.length === 0"
class="advanced-setting mb-20 no-search-match"
>
<p> {{ t('harvester.setting.noSearchMatch') }} </p>
</div>
</div>
</template>
@@ -271,4 +367,8 @@ export default {
padding: 2px 10px;
font-size: 12px;
}
.no-search-match {
text-align: center;
}
</style>
+1 -1
View File
@@ -43,7 +43,7 @@ export default {
{{ t('harvester.upgradePage.upgradeInfo.tip') }}
</p>
<p class="mb-5">
<p>
{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a
:href="releaseVersion"
target="_blank"
@@ -257,8 +257,12 @@ export default {
});
},
reconnect() {
this.$refs.novncConsole.reconnect();
},
softReboot() {
this.vmResource.softrebootVM();
this.vmResource.doSoftReboot();
},
showKeysModal() {
@@ -306,6 +310,13 @@ export default {
{{ 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"
@@ -372,3 +383,15 @@ export default {
background: rgb(40, 40, 40);
}
</style>
<style lang="scss">
.vm-console .v-popper__arrow-container {
display: none;
}
.vm-console .v-popper__popper{
margin-top: 8px;
}
.vm-console .v-popper__inner{
overflow-y: visible;
}
</style>
@@ -6,6 +6,7 @@ import CreateEditView from '@shell/mixins/create-edit-view';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI_SETTING } from '../../config/settings';
import UnitInput from '@shell/components/form/UnitInput';
const DEFAULT_TYPE = 's3';
@@ -13,7 +14,7 @@ export default {
name: 'HarvesterEditBackupTarget',
components: {
LabeledInput, LabeledSelect, Tip, Password, MessageLink
LabeledInput, LabeledSelect, Tip, Password, MessageLink, UnitInput
},
mixins: [CreateEditView],
@@ -24,7 +25,9 @@ export default {
try {
parseDefaultValue = JSON.parse(this.value.value);
} catch (error) {
parseDefaultValue = { type: '', endpoint: '' };
parseDefaultValue = {
type: '', endpoint: '', refreshIntervalInSeconds: 0
};
}
// set default type to s3
@@ -62,6 +65,10 @@ export default {
}];
},
refreshIntervalInSecondEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('refreshIntervalInSecond');
},
isS3() {
return this.parseDefaultValue.type === DEFAULT_TYPE;
},
@@ -83,7 +90,9 @@ export default {
try {
parseDefaultValue = JSON.parse(neu.value);
} catch (err) {
parseDefaultValue = { type: '', endpoint: '' };
parseDefaultValue = {
type: '', endpoint: '', refreshIntervalInSeconds: 0
};
}
this['parseDefaultValue'] = parseDefaultValue;
@@ -111,7 +120,6 @@ export default {
if (this.isS3 && !this.parseDefaultValue.virtualHostedStyle) {
this.parseDefaultValue.virtualHostedStyle = false;
}
if (!this.parseDefaultValue.type) {
delete this.value['value'];
} else {
@@ -120,7 +128,9 @@ export default {
},
useDefault() {
this['parseDefaultValue'] = { type: '', endpoint: '' };
this['parseDefaultValue'] = {
type: '', endpoint: '', refreshIntervalInSeconds: 0
};
}
}
};
@@ -139,6 +149,23 @@ export default {
:options="typeOption"
@update:value="update"
/>
<UnitInput
v-if="refreshIntervalInSecondEnabled"
v-model:value="parseDefaultValue.refreshIntervalInSeconds"
:suffix="parseDefaultValue.refreshIntervalInSeconds <= 1 ? 'Second' : 'Seconds'"
:label="t('harvester.backup.refreshInterval.label')"
:mode="mode"
:positive="true"
class="mb-5"
required
@update:value="update"
/>
<Tip
v-if="refreshIntervalInSecondEnabled"
class="mb-20"
icon="icon icon-info"
:text="t('harvester.backup.refreshInterval.tip')"
/>
<LabeledInput
v-model:value="parseDefaultValue.endpoint"
class="mb-5"
@@ -0,0 +1,123 @@
<script>
import MessageLink from '@shell/components/MessageLink';
import CreateEditView from '@shell/mixins/create-edit-view';
import { LabeledInput } from '@components/Form/LabeledInput';
import { HCI_SETTING } from '../../config/settings';
import { Checkbox } from '@components/Form/Checkbox';
import { Banner } from '@components/Banner';
export default {
name: 'HarvesterEditClusterRegistrationURL',
components: {
LabeledInput, MessageLink, Checkbox, Banner
},
mixins: [CreateEditView],
data() {
let parseDefaultValue = {};
try {
parseDefaultValue = JSON.parse(this.value.value);
} catch (error) {
parseDefaultValue.url = this.value.value;
parseDefaultValue.insecureSkipTLSVerify = true;
}
return {
parseDefaultValue,
errors: []
};
},
computed: {
toCA() {
return `${ HCI_SETTING.ADDITIONAL_CA }?mode=edit`;
},
clusterRegistrationTLSVerifyEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('clusterRegistrationTLSVerify');
},
registrationURL: {
get() {
return this.clusterRegistrationTLSVerifyEnabled ? this.parseDefaultValue.url : this.parseDefaultValue;
},
set(value) {
if (this.clusterRegistrationTLSVerifyEnabled) {
this.parseDefaultValue.url = value;
} else {
this.parseDefaultValue = value;
}
}
}
},
methods: {
getDefaultValue() {
if (this.clusterRegistrationTLSVerifyEnabled) {
return { url: '', insecureSkipTLSVerify: false };
} else {
return '';
}
},
updateUrl() {
this.update();
},
update() {
if (this.clusterRegistrationTLSVerifyEnabled) {
this.value['value'] = JSON.stringify(this.parseDefaultValue);
} else {
this.value['value'] = this.parseDefaultValue;
}
},
useDefault() {
this.parseDefaultValue = this.getDefaultValue();
},
updateInsecureSkipTLSVerify(newValue) {
const { url = '' } = this.parseDefaultValue;
this.parseDefaultValue = { url, insecureSkipTLSVerify: newValue };
this.update();
},
}
};
</script>
<template>
<div
class="row"
>
<div class="col span-12">
<Banner color="info">
<MessageLink
:to="toCA"
target="_blank"
prefix-label="harvester.setting.clusterRegistrationUrl.tip.prefix"
middle-label="harvester.setting.clusterRegistrationUrl.tip.middle"
suffix-label="harvester.setting.clusterRegistrationUrl.tip.suffix"
/>
</Banner>
<LabeledInput
v-model:value="registrationURL"
class="mb-20"
:mode="mode"
:label="t('harvester.setting.clusterRegistrationUrl.url')"
@update:value="updateUrl"
/>
<div v-if="clusterRegistrationTLSVerifyEnabled">
<Checkbox
v-model:value="parseDefaultValue.insecureSkipTLSVerify"
class="check mb-5"
type="checkbox"
:label="t('harvester.setting.clusterRegistrationUrl.insecureSkipTLSVerify')"
@update:value="updateInsecureSkipTLSVerify"
/>
</div>
</div>
</div>
</template>
@@ -75,7 +75,11 @@ export default {
const csiDrivers = this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
return this.configArr.length >= csiDrivers.length;
}
},
allowEmptySnapshotClassNameFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('allowEmptySnapshotClassName');
},
},
methods: {
@@ -139,7 +143,7 @@ export default {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.csiDriverConfig.volumeSnapshotClassName') }, true));
}
if (!config.value.backupVolumeSnapshotClassName) {
if (!this.allowEmptySnapshotClassNameFeatureEnabled && !config.value.backupVolumeSnapshotClassName) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName') }, true));
}
});
@@ -158,10 +162,18 @@ export default {
this.configArr.splice(idx, 1);
},
isBackupVolumeSnapshotRequired(driver) {
return driver === LONGHORN_DRIVER;
},
disableEdit(driver) {
return driver === LONGHORN_DRIVER;
},
isBackupVolumeSnapshotClassNameDisabled(driver) {
return driver === LONGHORN_DRIVER || this.allowEmptySnapshotClassNameFeatureEnabled;
},
add() {
this.configArr.push({
key: '',
@@ -184,6 +196,7 @@ export default {
<InfoBox
v-for="(driver, idx) in configArr"
:key="idx"
class="box"
>
<button
:disabled="disableEdit(driver.key)"
@@ -226,9 +239,9 @@ export default {
<LabeledSelect
v-model:value="driver.value.backupVolumeSnapshotClassName"
:mode="mode"
required
:disabled="disableEdit(driver.key)"
:disabled="isBackupVolumeSnapshotClassNameDisabled(driver.key)"
:options="getVolumeSnapshotOptions(driver.key)"
:required="isBackupVolumeSnapshotRequired(driver.key)"
:label="t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName')"
@keydown.native.enter.prevent="()=>{}"
@update:value="update"
@@ -0,0 +1,294 @@
<script>
import { _EDIT } from '@shell/config/query-params';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import InfoBox from '@shell/components/InfoBox';
import { Banner } from '@components/Banner';
import { allHash } from '@shell/utils/promise';
import { CSI_DRIVER } from '../../types';
import { LONGHORN_DRIVER } from '@shell/config/types';
export default {
name: 'CSIOnlineExpandValidation',
components: {
Banner,
InfoBox,
LabeledSelect,
},
props: {
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
default: () => ({}),
},
registerBeforeHook: {
type: Function,
required: true,
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
try {
await allHash({ csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }) });
this.fetchError = null;
} catch (error) {
console.error('Failed to fetch CSI drivers:', error); // eslint-disable-line no-console
this.fetchError = this.t(
'harvester.setting.csiOnlineExpandValidation.failedToLoadDrivers',
{ error: error.message || error },
true
);
}
},
data() {
return {
configArr: [],
parseError: null,
fetchError: null,
};
},
created() {
const initValue = this.value.value || this.value.default || '{}';
this.configArr = this.parseValue(initValue);
this.registerBeforeHook?.(this.willSave, 'willSave');
},
computed: {
allErrors() {
const errors = [];
if (this.fetchError) {
errors.push(this.fetchError);
}
if (this.parseError) {
errors.push(this.parseError);
}
return errors;
},
csiDrivers() {
if (this.fetchError) return [];
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
},
provisioners() {
const usedKeys = this.configArr.map(({ key }) => key);
return this.csiDrivers
.filter(({ name }) => !usedKeys.includes(name))
.map(({ name }) => name);
},
provisionerValue() {
return [
{ label: 'True', value: true },
{ label: 'False', value: false },
];
},
disableAdd() {
return this.parseError || this.fetchError || this.configArr.length >= this.csiDrivers.length;
},
disableConfigEditing() {
return this.parseError || this.fetchError;
}
},
watch: {
'value.value'(newVal, oldVal) {
if (newVal !== oldVal) {
this.configArr = this.parseValue(newVal || '{}');
}
}
},
methods: {
_convertToBoolean(value) {
if (typeof value === 'boolean') return value;
if (typeof value === 'string') {
const lowerCaseValue = value.toLowerCase();
if (lowerCaseValue === 'true') return true;
if (lowerCaseValue === 'false') return false;
}
return false; // default to false for any other string or non-boolean type
},
parseValue(raw) {
try {
const json = JSON.parse(raw);
this.parseError = null;
return Object.entries(json).map(([key, value]) => ({
key,
value: this._convertToBoolean(value),
}));
} catch (e) {
console.error('[CSIOnlineExpandValidation] JSON Parsing Error:', raw, e); // eslint-disable-line no-console
this.parseError = this.t(
'harvester.setting.csiOnlineExpandValidation.invalidJsonFormat',
{ error: e.message },
true
);
return [];
}
},
stringifyConfig() {
const obj = {};
this.configArr.forEach(({ key, value }) => {
obj[key] = value;
});
return this.configArr.length ? JSON.stringify(obj) : '';
},
update() {
this.value.value = this.stringifyConfig();
},
willSave() {
const errors = [];
this.configArr.forEach(({ key }) => {
if (!key) {
errors.push(
this.t('validation.required', { key: this.t('harvester.setting.csiOnlineExpandValidation.provisioner') }, true)
);
}
});
this.value.value = this.stringifyConfig();
return errors.length ? Promise.reject(errors) : Promise.resolve();
},
useDefault() {
this.configArr = this.parseValue(this.value.default || '{}');
this.update();
},
disableEdit(driverKey) {
return this.fetchError || driverKey === LONGHORN_DRIVER;
},
add() {
if (this.disableConfigEditing) return;
this.configArr.push({ key: '', value: true });
},
remove(index) {
if (this.disableConfigEditing) return;
this.configArr.splice(index, 1);
this.update();
},
onValueChange(idx, newVal) {
if (this.disableConfigEditing) return;
const val = newVal === 'true' ? true : newVal === 'false' ? false : newVal;
this.configArr[idx].value = val;
this.update();
},
},
};
</script>
<template>
<div>
<Banner
v-for="(errorMsg, index) in allErrors"
:key="index"
color="error"
>
{{ errorMsg }}
</Banner>
<InfoBox
v-for="(driver, idx) in configArr"
:key="idx"
class="box"
>
<button
class="role-link btn btn-sm remove"
type="button"
:disabled="disableEdit(driver.key)"
@click="remove(idx)"
>
<i class="icon icon-x" />
</button>
<div class="row">
<div class="col span-4">
<LabeledSelect
v-model:value="driver.key"
label-key="harvester.setting.csiOnlineExpandValidation.provisioner"
required
searchable
:mode="mode"
:disabled="disableEdit(driver.key)"
:options="provisioners"
@update:value="update"
@keydown.native.enter.prevent
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model:value="driver.value"
:value="driver.value.toString()"
label-key="harvester.setting.csiOnlineExpandValidation.value"
required
searchable
:mode="mode"
:disabled="disableEdit(driver.key)"
:options="provisionerValue"
@update:value="val => onValueChange(idx, val)"
@keydown.native.enter.prevent
/>
</div>
</div>
</InfoBox>
<button
class="btn btn-sm role-primary"
:disabled="disableAdd"
@click="add"
>
{{ t('generic.add') }}
</button>
</div>
</template>
<style lang="scss" scoped>
.box {
position: relative;
padding-top: 40px;
}
.remove {
position: absolute;
top: 10px;
right: 10px;
padding: 0;
}
</style>
@@ -1,6 +1,5 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import { Banner } from '@components/Banner';
import { LabeledInput } from '@components/Form/LabeledInput';
@@ -62,7 +61,7 @@ export default {
<div
class="row"
@update:value="update"
@input="update"
>
<div class="col span-12">
<LabeledInput
@@ -0,0 +1,104 @@
<script>
import UnitInput from '@shell/components/form/UnitInput';
import { Banner } from '@components/Banner';
export default {
name: 'HarvesterInstanceManagerResources',
components: {
UnitInput,
Banner,
},
props: {
value: {
type: Object,
default: () => ({
value: '',
default: '{}'
})
},
},
data() {
const resources = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
return {
resources,
parseError: null,
};
},
methods: {
parseJSON(string) {
try {
return JSON.parse(string);
} catch (e) {
this.parseError = this.t('harvester.setting.instanceManagerResources.parseError', { error: e.message });
return null;
}
},
update() {
if (!this.value) return;
const cpu = { ...this.resources?.cpu };
if (cpu.v1 !== null && cpu.v1 !== undefined) cpu.v1 = String(cpu.v1);
if (cpu.v2 !== null && cpu.v2 !== undefined) cpu.v2 = String(cpu.v2);
this.value.value = JSON.stringify({ ...this.resources, cpu });
},
useDefault() {
if (this.value?.default) {
this.resources = this.parseJSON(this.value.default) || {};
this.update();
}
},
},
};
</script>
<template>
<div>
<Banner
v-if="parseError"
color="error"
>
{{ parseError }}
</Banner>
<div class="row">
<div class="col span-12">
<UnitInput
v-model:value="resources.cpu.v1"
:label="t('harvester.setting.instanceManagerResources.v1')"
suffix="%"
:delay="0"
type="number"
min="0"
max="100"
required
:mode="mode"
class="mb-20"
@update:value="update"
/>
<UnitInput
v-model:value="resources.cpu.v2"
:label="t('harvester.setting.instanceManagerResources.v2')"
suffix="%"
:delay="0"
type="number"
min="0"
max="100"
required
:mode="mode"
class="mb-20"
@update:value="update"
/>
</div>
</div>
</div>
</template>
@@ -0,0 +1,154 @@
<script>
import { LabeledInput } from '@components/Form/LabeledInput';
import { RadioGroup } from '@components/Form/Radio';
import UnitInput from '@shell/components/form/UnitInput';
import { MEBIBYTE } from '../../utils/unit';
import { Banner } from '@components/Banner';
export default {
name: 'KubevirtMigration',
components: {
LabeledInput,
UnitInput,
RadioGroup,
Banner
},
props: {
registerBeforeHook: {
type: Function,
required: true
},
value: {
type: Object,
default: () => ({
value: '',
default: '{}'
})
},
},
data() {
const migration = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
return {
MEBIBYTE,
migration,
parseError: null,
};
},
created() {
this.registerBeforeHook?.(this.willSave, 'willSave');
},
methods: {
parseJSON(string) {
try {
return JSON.parse(string);
} catch (e) {
this.parseError = this.t('kubevirtMigration.parseError', { error: e.message });
// eslint-disable-next-line no-console
console.error('Failed to parse JSON:', e.message);
return null;
}
},
updateValue() {
if (this.value) {
this.value.value = JSON.stringify(this.migration);
}
},
useDefault() {
if (this.value?.default) {
const defaultMigration = this.parseJSON(this.value.default) || {};
this.migration = defaultMigration;
this.updateValue();
}
},
async willSave() {
this.updateValue();
return Promise.resolve();
},
},
};
</script>
<template>
<div>
<Banner
v-if="parseError"
color="error"
>
{{ parseError }}
</Banner>
<div class="migration-field">
<LabeledInput
v-model:value.number="migration.parallelMigrationsPerCluster"
:label="t('harvester.setting.kubevirtMigration.parallelMigrationsPerCluster')"
type="number"
min="1"
/>
<LabeledInput
v-model:value.number="migration.parallelOutboundMigrationsPerNode"
:label="t('harvester.setting.kubevirtMigration.parallelOutboundMigrationsPerNode')"
type="number"
min="1"
/>
<UnitInput
v-model:value="migration.bandwidthPerMigration"
min="0"
:label="t('harvester.setting.kubevirtMigration.bandwidthPerMigration')"
:mode="mode"
:suffix="MEBIBYTE"
:tooltip="t('harvester.setting.kubevirtMigration.bandwidthPerMigrationTooltip', _, true)"
/>
<UnitInput
v-model:value="migration.completionTimeoutPerGiB"
:label="t('harvester.setting.kubevirtMigration.completionTimeoutPerGiB')"
:mode="mode"
:suffix="migration.completionTimeoutPerGiB === 1 ? 'Second' : 'Seconds'"
min="10"
/>
<UnitInput
v-model:value="migration.progressTimeout"
:label="t('harvester.setting.kubevirtMigration.progressTimeout')"
:mode="mode"
:suffix="migration.progressTimeout === 1 ? 'Second' : 'Seconds'"
min="10"
/>
<div
v-for="field in ['allowAutoConverge','allowPostCopy','unsafeMigrationOverride','allowWorkloadDisruption','disableTLS','matchSELinuxLevelOnMigration']"
:key="field"
>
<label
class="mb-5"
:for="field"
>{{ t(`harvester.setting.kubevirtMigration.${field}`) }}</label>
<RadioGroup
:id="field"
v-model:value="migration[field]"
:options="[
{ label: t('advancedSettings.edit.trueOption'), value: true },
{ label: t('advancedSettings.edit.falseOption'), value: false },
]"
/>
</div>
</div>
</div>
</template>
<style scoped>
.migration-field {
display: flex;
flex-direction: column;
gap: 12px;
}
</style>
@@ -0,0 +1,222 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import { RadioGroup } from '@components/Form/Radio';
import { SECRET } from '@shell/config/types';
import { exceptionToErrorsArray } from '@shell/utils/error';
import FileSelector, { createOnSelected } from '@shell/components/form/FileSelector';
import YamlEditor from '@shell/components/YamlEditor';
export default {
name: 'HarvesterRancherCluster',
components: {
RadioGroup,
FileSelector,
YamlEditor
},
mixins: [CreateEditView],
props: {
value: {
type: Object,
default: () => ({}),
},
registerBeforeHook: {
type: Function,
required: false,
default: () => {},
},
},
data() {
let parseDefaultValue = {};
try {
const data = this.value.value || this.value.default || '{}';
const parsed = JSON.parse(data);
parseDefaultValue = {
kubeConfig: '',
removeUpstreamClusterWhenNamespaceIsDeleted: parsed.removeUpstreamClusterWhenNamespaceIsDeleted || false
};
} catch (error) {
parseDefaultValue = {
kubeConfig: '',
removeUpstreamClusterWhenNamespaceIsDeleted: false
};
}
return {
parseDefaultValue,
errors: [],
existingSecret: null,
};
},
async created() {
await this.checkExistingSecret();
if (this.registerBeforeHook) {
this.registerBeforeHook(this.willSave, 'willSave');
}
},
methods: {
onKeySelected: createOnSelected('parseDefaultValue.kubeConfig'),
update() {
if (this.parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted) {
this.value['value'] = JSON.stringify({ removeUpstreamClusterWhenNamespaceIsDeleted: true });
} else {
this.value['value'] = this.value.default || '{}';
}
},
async checkExistingSecret() {
const inStore = this.$store.getters['currentProduct'].inStore;
await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
const secrets = this.$store.getters[`${ inStore }/all`](SECRET) || [];
this.existingSecret = secrets.find((secret) => secret.metadata.name === 'rancher-cluster-config' &&
secret.metadata.namespace === 'harvester-system'
);
// If the secret exists and has data, populate the kubeConfig
if (this.existingSecret?.data?.kubeConfig) {
const decodedContent = atob(this.existingSecret.data.kubeConfig);
this.parseDefaultValue.kubeConfig = decodedContent;
this.$nextTick(() => {
this.update();
});
}
},
async createOrUpdateRancherKubeConfigSecret() {
this.errors = [];
// Check if kubeConfig is provided
if (!this.parseDefaultValue.kubeConfig) {
this.errors.push(this.t('validation.required', { key: this.t('harvester.setting.rancherCluster.kubeConfig') }, true));
return Promise.reject(this.errors);
}
try {
let secret;
if (this.existingSecret) {
secret = this.existingSecret;
secret.setData('kubeConfig', this.parseDefaultValue.kubeConfig);
} else {
const inStore = this.$store.getters['currentProduct'].inStore;
secret = await this.$store.dispatch(`${ inStore }/create`, {
apiVersion: 'v1',
kind: 'Secret',
metadata: {
name: 'rancher-cluster-config',
namespace: 'harvester-system'
},
type: 'secret',
data: { kubeConfig: btoa(this.parseDefaultValue.kubeConfig) }
});
}
await secret.save();
return Promise.resolve();
} catch (err) {
this.errors = exceptionToErrorsArray(err);
return Promise.reject(this.errors);
}
},
async deleteRancherKubeConfigSecret() {
if (this.existingSecret) {
this.existingSecret.remove();
}
},
async willSave() {
// Only create or update secret if enabled
if (this.parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted) {
await this.createOrUpdateRancherKubeConfigSecret();
} else {
await this.deleteRancherKubeConfigSecret();
}
return Promise.resolve();
},
useDefault() {
this.parseDefaultValue = {
kubeConfig: '',
removeUpstreamClusterWhenNamespaceIsDeleted: false
};
}
},
watch: {
'parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted'(val, oldVal) {
if (val && !oldVal && this.existingSecret?.data?.kubeConfig) {
// Populate kubeConfig with the existing secret value
this.parseDefaultValue.kubeConfig = atob(this.existingSecret.data.kubeConfig);
}
},
'parseDefaultValue.kubeConfig'(val) {
this.$refs.yaml?.updateValue(val);
}
}
};
</script>
<template>
<div>
<div class="row mt-20">
<div class="col span-12">
<RadioGroup
v-model:value="parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted"
:label="t('harvester.setting.rancherCluster.removeUpstreamClusterWhenNamespaceIsDeleted')"
name="removeUpstreamClusterWhenNamespaceIsDeleted"
:options="[true, false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@update:value="update"
/>
</div>
</div>
<div
v-if="parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted"
class="row mt-20"
>
<div class="col span-12">
<FileSelector
class="btn btn-sm bg-primary mb-10"
:label="t('generic.readFromFile')"
@selected="onKeySelected"
/>
<YamlEditor
ref="yaml"
v-model:value="parseDefaultValue.kubeConfig"
class="yaml-editor"
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
@update:value="update"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
$yaml-height: 540px;
:deep() .yaml-editor{
flex: 1;
min-height: $yaml-height;
& .code-mirror .CodeMirror {
position: initial;
height: auto;
min-height: $yaml-height;
}
}
</style>
@@ -0,0 +1,371 @@
<script>
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
import { RadioGroup } from '@components/Form/Radio';
import ArrayList from '@shell/components/form/ArrayList';
import { isValidCIDR } from '@shell/utils/validators/cidr';
import { _EDIT } from '@shell/config/query-params';
import { Banner } from '@components/Banner';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../../types';
import { NETWORK_TYPE } from '../../config/types';
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
const SHARE_STORAGE_NETWORK = 'share-storage-network';
const NETWORK = 'network';
const DEFAULT_DEDICATED_NETWORK = {
vlan: '',
clusterNetwork: '',
range: '',
exclude: [],
};
export default {
name: 'RwxNetworkSetting',
components: {
RadioGroup,
Banner,
ArrayList,
LabeledInput,
LabeledSelect,
},
props: {
registerBeforeHook: {
type: Function,
required: true,
},
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
default: () => {
return {};
},
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
});
},
data() {
let enabled = false; // enabled / disabled options
let shareStorageNetwork = false; // shareStorageNetwork / dedicatedRwxNetwork options
let dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
let networkType = L2VLAN;
let exclude = [];
try {
const parsedValue = JSON.parse(this.value.value || this.value.default || '{}');
const parsedNetwork = parsedValue?.[NETWORK] || parsedValue || {};
if (parsedValue && typeof parsedValue === 'object') {
shareStorageNetwork = !!parsedValue[SHARE_STORAGE_NETWORK];
networkType = 'vlan' in parsedNetwork ? L2VLAN : UNTAGGED;
dedicatedNetwork = {
vlan: parsedNetwork.vlan || '',
clusterNetwork: parsedNetwork.clusterNetwork || '',
range: parsedNetwork.range || '',
};
exclude = parsedNetwork?.exclude?.toString().split(',') || [];
enabled = shareStorageNetwork || !!(parsedNetwork.vlan || parsedNetwork.clusterNetwork || parsedNetwork.range);
}
} catch (error) {
enabled = false;
shareStorageNetwork = false;
dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
}
return {
enabled,
shareStorageNetwork,
dedicatedNetwork,
networkType,
exclude,
defaultAddValue: '',
};
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.willSave, 'willSave');
}
},
computed: {
showDedicatedNetworkConfig() {
return this.enabled && !this.shareStorageNetwork;
},
showVlan() {
return this.networkType === L2VLAN;
},
networkTypes() {
return [L2VLAN, UNTAGGED];
},
clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
const clusterNetworksOptions = this.networkType === UNTAGGED ? clusterNetworks.filter((network) => network.id !== 'mgmt') : clusterNetworks;
return clusterNetworksOptions.map((network) => {
const disabled = !network.isReadyForStorageNetwork;
return {
label: disabled ? `${ network.id } (${ this.t('generic.notReady') })` : network.id,
value: network.id,
disabled,
};
});
},
},
methods: {
onUpdateEnabled() {
if (!this.enabled) {
this.shareStorageNetwork = false;
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
}
this.update();
},
onUpdateNetworkType() {
if (this.shareStorageNetwork) {
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
}
this.update();
},
onUpdateDedicatedType(neu) {
this.dedicatedNetwork.clusterNetwork = '';
if (neu === L2VLAN) {
this.dedicatedNetwork.vlan = '';
} else {
delete this.dedicatedNetwork.vlan;
}
this.update();
},
inputVlan(neu) {
if (neu === '') {
this.dedicatedNetwork.vlan = '';
this.update();
return;
}
const newValue = Number(neu);
if (newValue > 4094) {
this.dedicatedNetwork.vlan = 4094;
} else if (newValue < 1) {
this.dedicatedNetwork.vlan = 1;
} else {
this.dedicatedNetwork.vlan = newValue;
}
this.update();
},
useDefault() {
this.enabled = false;
this.shareStorageNetwork = false;
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
this.update();
},
update() {
const value = { [SHARE_STORAGE_NETWORK]: false };
if (this.enabled && this.shareStorageNetwork) {
value[SHARE_STORAGE_NETWORK] = true;
}
if (this.showDedicatedNetworkConfig) {
value[NETWORK] = {};
if (this.showVlan) {
value[NETWORK].vlan = this.dedicatedNetwork.vlan;
}
value[NETWORK].clusterNetwork = this.dedicatedNetwork.clusterNetwork;
value[NETWORK].range = this.dedicatedNetwork.range;
const excludeList = this.exclude.filter((ip) => ip);
if (Array.isArray(excludeList) && excludeList.length > 0) {
value[NETWORK].exclude = excludeList;
}
}
this.value.value = JSON.stringify(value);
},
willSave() {
this.update();
if (!this.showDedicatedNetworkConfig) {
return Promise.resolve();
}
const errors = [];
if (this.showVlan && !this.dedicatedNetwork.vlan) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
}
if (!this.dedicatedNetwork.clusterNetwork) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.clusterNetwork') }, true));
}
if (!this.dedicatedNetwork.range) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.range.label') }, true));
} else if (!isValidCIDR(this.dedicatedNetwork.range)) {
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
}
if (this.exclude) {
const hasInvalidCIDR = this.exclude.find((cidr) => {
return cidr && !isValidCIDR(cidr);
});
if (hasInvalidCIDR) {
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', null, true));
}
}
if (errors.length > 0) {
return Promise.reject(errors);
}
return Promise.resolve();
},
},
};
</script>
<template>
<div :class="mode">
<Banner color="warning">
<t
k="harvester.setting.rwxNetwork.warning"
:raw="true"
/>
</Banner>
<RadioGroup
v-model:value="enabled"
class="mb-20"
name="rwx-network-enable"
:options="[true,false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@update:value="onUpdateEnabled"
/>
<RadioGroup
v-if="enabled"
v-model:value="shareStorageNetwork"
class="mb-20"
name="rwx-network-type"
:options="[true,false]"
:labels="[t('harvester.setting.rwxNetwork.shareStorageNetwork'), t('harvester.setting.rwxNetwork.dedicatedRwxNetwork')]"
@update:value="onUpdateNetworkType"
/>
<Banner
v-if="shareStorageNetwork"
class="mb-20"
color="warning"
>
<t
k="harvester.setting.rwxNetwork.shareStorageNetworkWarning"
:raw="true"
/>
</Banner>
<template v-if="showDedicatedNetworkConfig">
<LabeledSelect
v-model:value="networkType"
class="mb-20"
:options="networkTypes"
:mode="mode"
:label="t('harvester.fields.type')"
required
@update:value="onUpdateDedicatedType"
/>
<LabeledInput
v-if="showVlan"
v-model:value.number="dedicatedNetwork.vlan"
type="number"
class="mb-20"
:mode="mode"
required
placeholder="e.g. 1 - 4094"
label-key="harvester.setting.storageNetwork.vlan"
@update:value="inputVlan"
/>
<LabeledSelect
v-model:value="dedicatedNetwork.clusterNetwork"
label-key="harvester.setting.storageNetwork.clusterNetwork"
class="mb-20"
required
:options="clusterNetworkOptions"
@update:value="update"
/>
<LabeledInput
v-model:value="dedicatedNetwork.range"
class="mb-5"
:mode="mode"
required
:placeholder="t('harvester.setting.storageNetwork.range.placeholder')"
label-key="harvester.setting.storageNetwork.range.label"
@update:value="update"
/>
<ArrayList
v-model:value="exclude"
:show-header="true"
:default-add-value="defaultAddValue"
:mode="mode"
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
class="mt-20"
@update:value="update"
>
<template #column-headers>
<div class="box mb-10">
<div class="key">
{{ t('harvester.setting.storageNetwork.exclude.label') }}
</div>
</div>
</template>
<template #columns="scope">
<div class="key">
<input
v-model="scope.row.value"
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
@update:value="update"
/>
</div>
</template>
</ArrayList>
</template>
</div>
</template>
@@ -12,6 +12,9 @@ import { NODE } from '@shell/config/types';
import { HCI } from '../../types';
import { DOC } from '../../config/doc-links';
import { docLink } from '../../utils/feature-flags';
import { NETWORK_TYPE } from '../../config/types';
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
export default {
name: 'HarvesterEditStorageNetwork',
@@ -57,11 +60,14 @@ export default {
data() {
let parsedDefaultValue = {};
let openVlan = false;
let networkType = L2VLAN;
try {
parsedDefaultValue = JSON.parse(this.value.value);
networkType = 'vlan' in parsedDefaultValue ? L2VLAN : UNTAGGED; // backend doesn't provide networkType, so we check if vlan is provided instead
openVlan = true;
} catch (error) {
networkType = L2VLAN;
parsedDefaultValue = {
vlan: '',
clusterNetwork: '',
@@ -73,6 +79,7 @@ export default {
return {
openVlan,
networkType,
errors: [],
exclude,
parsedDefaultValue,
@@ -87,16 +94,35 @@ export default {
},
computed: {
showVlan() {
return this.networkType === L2VLAN;
},
networkTypes() {
const types = [L2VLAN];
if (this.untaggedNetworkSettingEnabled) {
types.push(UNTAGGED);
}
return types;
},
storageNetworkExampleLink() {
const version = this.$store.getters['harvester-common/getServerVersion']();
return docLink(DOC.STORAGE_NETWORK_EXAMPLE, version);
},
untaggedNetworkSettingEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('untaggedNetworkSetting');
},
clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
// untaggedNetwork filter out mgmt cluster network
const clusterNetworksOptions = this.networkType === UNTAGGED ? clusterNetworks.filter((net) => net.id !== 'mgmt') : clusterNetworks;
return clusterNetworks.map((n) => {
return clusterNetworksOptions.map((n) => {
const disabled = !n.isReadyForStorageNetwork;
return {
@@ -108,7 +134,50 @@ export default {
},
},
watch: {
networkType: {
handler(neu) {
this.parsedDefaultValue.clusterNetwork = '';
if (neu === L2VLAN) {
this.parsedDefaultValue.vlan = '';
} else {
delete this.parsedDefaultValue.vlan;
}
},
deep: true
}
},
methods: {
inputVlan(neu) {
if (neu === '') {
this.parsedDefaultValue.vlan = '';
return;
}
const newValue = Number(neu);
if (newValue > 4094) {
this.parsedDefaultValue.vlan = 4094;
} else if (newValue < 1) {
this.parsedDefaultValue.vlan = 1;
} else {
this.parsedDefaultValue.vlan = newValue;
}
},
useDefault() {
this.openVlan = false;
this.networkType = L2VLAN;
this.parsedDefaultValue = {
vlan: '',
clusterNetwork: '',
range: '',
exclude: []
};
},
update() {
const exclude = this.exclude.filter((ip) => ip);
@@ -138,7 +207,7 @@ export default {
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
}
if (!this.parsedDefaultValue.vlan) {
if (this.networkType === L2VLAN && !this.parsedDefaultValue.vlan) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
}
@@ -191,13 +260,25 @@ export default {
/>
<div v-if="openVlan">
<LabeledSelect
v-model:value="networkType"
class="mb-20"
:options="networkTypes"
:mode="mode"
:label="t('harvester.fields.type')"
required
/>
<LabeledInput
v-if="showVlan"
v-model:value.number="parsedDefaultValue.vlan"
type="number"
class="mb-20"
:mode="mode"
required
placeholder="e.g. 1 - 4094"
label-key="harvester.setting.storageNetwork.vlan"
@update:value="inputVlan"
/>
<LabeledSelect
@@ -3,6 +3,9 @@ import { NAMESPACE } from '@shell/config/types';
import CreateEditView from '@shell/mixins/create-edit-view';
import LabeledSelect from '@shell/components/form/LabeledSelect';
const SELECT_ALL = 'select_all';
const UNSELECT_ALL = 'unselect_all';
export default {
name: 'HarvesterBundleNamespaces',
@@ -11,47 +14,92 @@ export default {
mixins: [CreateEditView],
async fetch() {
this.loading = true;
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
if (this.customSupportBundleFeatureEnabled) {
try {
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
const response = await this.$store.dispatch('harvester/request', { url });
this.defaultNamespaces = response.data || [];
} catch (error) {
this.defaultNamespaces = [];
}
} else {
this.defaultNamespaces = [];
}
this.loading = false;
},
data() {
let namespaces = [];
const namespacesStr = this.value?.value || this.value?.default || '';
const namespaces = namespacesStr ? namespacesStr.split(',') : [];
if (namespacesStr) {
namespaces = namespacesStr.split(',');
}
return { namespaces };
return {
namespaces,
defaultNamespaces: [],
loading: true
};
},
computed: {
customSupportBundleFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
},
allNamespaces() {
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
},
filteredNamespaces() {
if (!this.customSupportBundleFeatureEnabled) {
return this.allNamespaces;
}
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
},
namespaceOptions() {
return this.$store.getters['harvester/all'](NAMESPACE).map((N) => {
return {
label: N.id,
value: N.id
};
});
const mappedNamespaces = this.filteredNamespaces.map((ns) => ({ label: ns, value: ns }));
if (!this.customSupportBundleFeatureEnabled) {
return mappedNamespaces;
}
const allSelected =
this.namespaces.length === this.filteredNamespaces.length &&
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
return [controlOption, ...mappedNamespaces];
}
},
methods: {
update() {
const namespaceStr = this.namespaces.join(',');
update(selected) {
if (selected.includes(SELECT_ALL)) {
this.namespaces = [...this.filteredNamespaces];
} else if (selected.includes(UNSELECT_ALL)) {
this.namespaces = [];
} else {
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
}
this.value['value'] = namespaceStr;
this.value.value = this.namespaces.join(',');
}
},
watch: {
'value.value': {
handler(neu) {
if (neu === this.value.default || !neu) {
this.namespaces = [];
}
},
deep: true
'value.value'(newVal) {
const raw = newVal || this.value.default || '';
this.namespaces = raw ? raw.split(',') : [];
}
}
};
@@ -62,6 +110,7 @@ export default {
<div class="col span-12">
<LabeledSelect
v-model:value="namespaces"
:loading="loading"
:multiple="true"
label-key="nameNsDescription.namespace.label"
:mode="mode"
@@ -0,0 +1,224 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { RadioGroup } from '@components/Form/Radio';
import { mapGetters } from 'vuex';
import { allHash } from '@shell/utils/promise';
import { NODE } from '@shell/config/types';
export default {
name: 'HarvesterUpgradeConfig',
components: {
LabeledInput,
LabeledSelect,
RadioGroup
},
mixins: [CreateEditView],
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = { nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }) };
await allHash(hash);
},
data() {
let parseDefaultValue = {};
try {
parseDefaultValue = this.value.value ? JSON.parse(this.value.value) : JSON.parse(this.value.default);
} catch (error) {
parseDefaultValue = JSON.parse(this.value.default);
}
parseDefaultValue = this.normalizeValue(parseDefaultValue);
return {
parseDefaultValue,
errors: []
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
strategyOptions() {
return [
{ value: 'sequential', label: 'sequential' },
{ value: 'skip', label: 'skip' },
{ value: 'parallel', label: 'parallel' }
];
},
nodeUpgradeOptions() {
return [
{ value: 'auto', label: 'auto' },
{ value: 'manual', label: 'manual' }
];
},
nodesOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
return nodes.map((node) => ({ value: node.id, label: node.name }));
},
showPauseNodes() {
return this.parseDefaultValue.nodeUpgradeOption?.strategy?.mode === 'manual';
},
resumeUpgradePausedNodeEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('resumeUpgradePausedNode');
},
},
created() {
this.update();
},
methods: {
normalizeValue(obj) {
// handle nodeUpgradeOption.strategy
if (obj?.nodeUpgradeOption?.strategy?.mode === 'auto') {
delete obj.nodeUpgradeOption.strategy.pauseNodes;
}
if (obj?.nodeUpgradeOption?.strategy?.mode === 'manual') {
if (!Array.isArray(obj.nodeUpgradeOption.strategy.pauseNodes)) {
obj.nodeUpgradeOption.strategy.pauseNodes = this.nodesOptions.map((node) => node.value);
}
}
// handle imagePreloadOption.strategy
if (!obj.imagePreloadOption) {
obj.imagePreloadOption = { strategy: { type: 'sequential' } };
}
if (!obj.imagePreloadOption.strategy) {
obj.imagePreloadOption.strategy = { type: 'sequential' };
}
if (!obj.imagePreloadOption.strategy.type) {
obj.imagePreloadOption.strategy.type = 'sequential';
}
// Only set concurrency if type is 'parallel'
if (obj.imagePreloadOption.strategy.type === 'parallel') {
if (typeof obj.imagePreloadOption.strategy.concurrency !== 'number') {
obj.imagePreloadOption.strategy.concurrency = 0;
}
} else {
delete obj.imagePreloadOption.strategy.concurrency;
}
if (typeof obj.restoreVM !== 'boolean') {
obj.restoreVM = false;
}
return obj;
},
update() {
try {
// Clone to avoid mutating the form state
const valueToSave = JSON.parse(JSON.stringify(this.parseDefaultValue));
if (valueToSave.imagePreloadOption && valueToSave.imagePreloadOption.strategy) {
if (valueToSave.imagePreloadOption.strategy.type !== 'parallel') {
delete valueToSave.imagePreloadOption.strategy.concurrency;
}
}
this.value['value'] = JSON.stringify(valueToSave, null, 2);
this.errors = [];
} catch (e) {
this.errors = ['Invalid JSON'];
}
}
},
watch: {
value: {
handler(neu) {
let parseDefaultValue;
try {
parseDefaultValue = JSON.parse(neu.value);
} catch (err) {
parseDefaultValue = JSON.parse(this.value.default);
}
parseDefaultValue = this.normalizeValue(parseDefaultValue);
this['parseDefaultValue'] = parseDefaultValue;
this.update();
},
deep: true
},
},
};
</script>
<template>
<div>
<div
class="row"
@input="update"
>
<div class="col span-12">
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.imagePreloadStrategy') }}</b></label>
<LabeledSelect
v-model:value="parseDefaultValue.imagePreloadOption.strategy.type"
class="mb-20"
:mode="mode"
:label="t('harvester.setting.upgrade.strategyType')"
:options="strategyOptions"
@update:value="update"
/>
<LabeledInput
v-if="parseDefaultValue.imagePreloadOption.strategy.type === 'parallel'"
v-model:value.number="parseDefaultValue.imagePreloadOption.strategy.concurrency"
class="mb-20"
:mode="mode"
:label="t('harvester.setting.upgrade.concurrency')"
min="0"
type="number"
/>
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.restoreVM') }}</b></label>
<RadioGroup
v-model:value="parseDefaultValue.restoreVM"
class="mb-20"
name="restoreVM"
:options="[true, false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@update:value="update"
/>
<div v-if="resumeUpgradePausedNodeEnabled">
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.nodeUpgradeOption') }}</b></label>
<LabeledSelect
v-model:value="parseDefaultValue.nodeUpgradeOption.strategy.mode"
class="mb-20 label-select"
:mode="mode"
:label="t('harvester.setting.upgrade.strategy')"
:options="nodeUpgradeOptions"
@update:value="update"
/>
<LabeledSelect
v-if="showPauseNodes"
v-model:value="parseDefaultValue.nodeUpgradeOption.strategy.pauseNodes"
class="mb-20 label-select"
:clearable="true"
:multiple="true"
:mode="mode"
:label="t('harvester.setting.upgrade.pauseNodes')"
:options="nodesOptions"
@update:value="update"
/>
</div>
<div
v-if="errors.length"
class="error"
>
{{ errors[0] }}
</div>
</div>
</div>
</div>
</template>
<style scoped>
.error {
color: #d9534f;
margin-top: 5px;
}
</style>
@@ -59,7 +59,7 @@ export default {
name="model"
:options="[true,false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@input="update"
@update:value="update"
/>
<LabeledInput
@@ -69,6 +69,15 @@ export default {
:mode="mode"
label-key="harvester.setting.vmForceDeletionPolicy.period"
/>
<LabeledInput
v-if="parseDefaultValue.enable"
v-model:value.number="parseDefaultValue.vmMigrationTimeout"
class="mb-20"
type="number"
:mode="mode"
label-key="harvester.setting.vmForceDeletionPolicy.vmMigrationTimeout"
/>
</div>
</div>
</template>
@@ -0,0 +1,270 @@
<script>
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
import { RadioGroup } from '@components/Form/Radio';
import { Banner } from '@components/Banner';
import ArrayList from '@shell/components/form/ArrayList';
import { allHash } from '@shell/utils/promise';
import { isValidCIDR } from '@shell/utils/validators/cidr';
import { NODE } from '@shell/config/types';
import { _EDIT } from '@shell/config/query-params';
import { HCI } from '../../types';
const DEFAULT_NETWORK = {
clusterNetwork: '',
vlan: '',
range: '',
exclude: [],
};
export default {
name: 'VMMigrationNetwork',
components: {
LabeledInput,
LabeledSelect,
RadioGroup,
Banner,
ArrayList,
},
props: {
registerBeforeHook: {
type: Function,
required: true,
},
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
default: () => ({ value: '' }),
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
try {
await allHash({
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
});
this.fetchError = null;
} catch (e) {
console.error('Failed to fetch network data:', e); // eslint-disable-line no-console
this.fetchError = this.t('harvester.setting.vmMigrationNetwork.fetchError', { error: e.message || e }, true);
}
},
data() {
const { parsed, enabled, parseError } = this.parseInitialValue();
return {
enabled,
network: { ...DEFAULT_NETWORK, ...parsed },
fetchError: null,
parseError,
};
},
created() {
this.registerBeforeHook?.(this.willSave, 'willSave');
},
computed: {
allErrors() {
return [this.fetchError, this.parseError].filter(Boolean);
},
clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const networks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
return networks.map((net) => ({
label: net.isReadyForStorageNetwork ? net.id : `${ net.id } (${ this.t('generic.notReady') })`,
value: net.id,
disabled: !net.isReadyForStorageNetwork,
}));
},
disableEdit() {
return !!(this.fetchError || this.parseError);
},
},
methods: {
parseInitialValue() {
let parsed = {};
let enabled = false;
let parseError = null;
try {
if (typeof this.value.value === 'string' && this.value.value.trim()) {
parsed = JSON.parse(this.value.value);
enabled = true;
}
} catch (e) {
console.error('[VMMigrationNetwork] Failed to parse value:', e); // eslint-disable-line no-console
parseError = this.t('harvester.setting.vmMigrationNetwork.parseError', { error: e.message }, true);
}
if (!Array.isArray(parsed.exclude)) {
parsed.exclude = [];
}
return {
parsed, enabled, parseError
};
},
clearErrors() {
this.fetchError = null;
this.parseError = null;
},
inputVlan(val) {
this.network.vlan = val ? Math.min(4094, Math.max(1, Number(val))) : '';
this.update();
},
useDefault() {
this.network = { ...DEFAULT_NETWORK };
this.value.value = '';
this.enabled = false;
this.clearErrors();
},
update() {
try {
this.value.value = this.enabled ? JSON.stringify({
...this.network,
exclude: (this.network.exclude || []).filter((e) => !!e?.trim()),
}) : '';
} catch (e) {
console.error('Failed to stringify network config:', e); // eslint-disable-line no-console
this.value.value = '';
}
},
validateInputs() {
const errors = [];
if (!this.network.clusterNetwork) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.clusterNetwork') }, true));
}
if (!this.network.range) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.range.label') }, true));
} else if (!isValidCIDR(this.network.range)) {
errors.push(this.t('harvester.setting.vmMigrationNetwork.range.invalid', null, true));
}
if (this.network.vlan === '') {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.vlan') }, true));
} else {
const vlan = Number(this.network.vlan);
if (isNaN(vlan) || vlan < 1 || vlan > 4094) {
errors.push(this.t('validation.between', {
key: this.t('harvester.setting.vmMigrationNetwork.vlan'),
min: 1,
max: 4094,
}, true));
}
}
for (const cidr of this.network.exclude || []) {
if (cidr && !isValidCIDR(cidr)) {
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', { value: cidr }, true));
}
}
return errors;
},
async willSave() {
if (!this.enabled) {
this.useDefault();
return Promise.resolve();
}
this.update();
const errors = this.validateInputs();
return errors.length ? Promise.reject(errors) : Promise.resolve();
},
},
};
</script>
<template>
<div>
<Banner
v-for="(errorMsg, index) in allErrors"
:key="index"
color="error"
>
{{ errorMsg }}
</Banner>
<RadioGroup
v-model:value="enabled"
class="mb-20"
name="enableMigrationNetwork"
:options="[true, false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@update:value="update"
/>
<template v-if="enabled">
<LabeledSelect
v-model:value="network.clusterNetwork"
required
label-key="harvester.setting.vmMigrationNetwork.clusterNetwork"
class="mb-20"
:mode="mode"
:options="clusterNetworkOptions"
:disabled="disableEdit"
@update:value="update"
/>
<LabeledInput
v-model:value.number="network.vlan"
required
type="number"
class="mb-20"
:min="1"
:max="4094"
:mode="mode"
placeholder="e.g. 1 - 4094"
label-key="harvester.setting.vmMigrationNetwork.vlan"
:disabled="disableEdit"
@update:value="inputVlan"
/>
<LabeledInput
v-model:value="network.range"
required
class="mb-5"
:mode="mode"
:placeholder="t('harvester.setting.vmMigrationNetwork.range.placeholder')"
label-key="harvester.setting.vmMigrationNetwork.range.label"
:disabled="disableEdit"
@update:value="update"
/>
<ArrayList
v-model:value="network.exclude"
:show-header="true"
:default-add-value="''"
:mode="mode"
:add-disabled="disableEdit"
:add-label="t('harvester.setting.vmMigrationNetwork.exclude.addButton')"
:value-label="t('harvester.setting.vmMigrationNetwork.exclude.label')"
:value-placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
@update:value="update"
/>
</template>
</div>
</template>
+10 -5
View File
@@ -1,8 +1,13 @@
// suffix of doc link, see utils/feature-flags.js how to get complete doc link
export const DOC = {
CONSOLE_URL: `/host/#remote-console`,
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
UPGRADE_URL: `/upgrade/index`
CONSOLE_URL: `/host/#remote-console`,
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
UPGRADE_URL: `/upgrade/index`,
UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`,
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
VPC_CONFIGURATION_EXAMPLES: `/networking/kubeovn-vpc#vpc-peering-configuration-examples`,
NETWORK_POLICY: `/networking/kubeovn-vm-isolation/#network-policies`,
TRANSPARENT_HUGEPAGES: `https://docs.kernel.org/admin-guide/mm/transhuge.html`,
};
+84 -55
View File
@@ -1,57 +1,86 @@
// https://github.com/harvester/dashboard/releases/tag/v1.3.0
const featuresV130 = [
'supportHarvesterClusterVersion'
];
import semver from 'semver';
// https://github.com/harvester/dashboard/releases/tag/v1.3.1
const featuresV131 = [
...featuresV130,
'autoRotateRke2CertsSetting',
'supportBundleNodeCollectionTimeoutSetting'
];
// https://github.com/harvester/dashboard/releases/tag/v1.3.2
const featuresV132 = [
...featuresV131,
'kubeconfigDefaultTokenTTLMinutesSetting',
'improveMaintenanceMode',
];
// TODO: add v1.3.3 official release note
// https://github.com/harvester/dashboard/releases/tag/v1.3.3-dev-20250105
const featuresV133 = [
...featuresV132,
];
// https://github.com/harvester/dashboard/releases/tag/v1.4.0
const featuresV140 = [
...featuresV132,
'cpuPinning',
'usbPassthrough',
'volumeEncryption',
'schedulingVMBackup',
'vmSnapshotQuota',
'longhornV2LVMSupport',
'improveMaintenanceMode',
];
// TODO: add v1.4.1 official release note
// https://github.com/harvester/dashboard/releases/tag/v1.4.1-rc1
const featuresV141 = [
...featuresV140
];
// TODO: add v1.5.0 official release note
const featuresV150 = [
...featuresV141
];
export const RELEASE_FEATURES = {
'v1.3.0': featuresV130,
'v1.3.1': featuresV131,
'v1.3.2': featuresV132,
'v1.3.3': featuresV133,
'v1.4.0': featuresV140,
'v1.4.1': featuresV141,
'v1.5.0': featuresV150
const FEATURE_FLAGS = {
'v1.3.0': [
'supportHarvesterClusterVersion'
],
'v1.3.1': [
'autoRotateRke2CertsSetting',
'supportBundleNodeCollectionTimeoutSetting'
],
'v1.3.2': [
'kubeconfigDefaultTokenTTLMinutesSetting',
'improveMaintenanceMode',
],
'v1.3.3': [],
'v1.4.0': [
'cpuPinning',
'usbPassthrough',
'volumeEncryption',
'schedulingVMBackup',
'vmSnapshotQuota',
'longhornV2LVMSupport',
'improveMaintenanceMode',
'upgradeConfigSetting'
],
'v1.4.1': [],
'v1.4.2': [
'refreshIntervalInSecond',
'allowEmptySnapshotClassName'
],
'v1.4.3': [],
'v1.5.0': [
'tpmPersistentState',
'efiPersistentState',
'untaggedNetworkSetting',
'skipSingleReplicaDetachedVol',
'thirdPartyStorage',
'liveMigrationProgress'
],
'v1.5.1': [],
'v1.6.0': [
'customSupportBundle',
'csiOnlineExpandValidation',
'vmNetworkMigration',
'kubeovnVpcSubnet',
'rancherClusterSetting',
'cpuMemoryHotplug',
'cdiSettings',
'vmCloneRunStrategy',
],
'v1.6.1': [],
'v1.7.0': [
'vmMachineTypeAuto',
'lhV2VolExpansion',
'l2VlanTrunkMode',
'kubevirtMigration',
'hotplugNic',
'resumeUpgradePausedNode',
],
'v1.7.1': [],
'v1.8.0': [
'hotplugCdRom',
'supportBundleFileNameSetting',
'clusterRegistrationTLSVerify',
'vGPUAsPCIDevice',
'instanceManagerResourcesSetting',
'rwxNetworkSetting',
'createPVCWithDataVolume'
],
};
const generateFeatureFlags = () => {
const versions = [...Object.keys(FEATURE_FLAGS)].filter((version) => semver.valid(version)).sort(semver.compare);
const generatedFlags = {};
versions.forEach((version, index) => {
const previousVersion = versions[index - 1];
generatedFlags[version] = previousVersion ? [...generatedFlags[previousVersion], ...FEATURE_FLAGS[version]] : [...FEATURE_FLAGS[version]];
});
return generatedFlags;
};
export const RELEASE_FEATURES = generateFeatureFlags();
+210 -2
View File
@@ -10,6 +10,7 @@ import {
LOGGING,
STORAGE_CLASS,
SECRET,
NETWORK_POLICY
} from '@shell/config/types';
import { HCI, VOLUME_SNAPSHOT } from '../types';
import {
@@ -24,6 +25,7 @@ import {
CONFIGURED_PROVIDERS,
SUB_TYPE,
ADDRESS,
DESCRIPTION,
} from '@shell/config/table-headers';
import { IF_HAVE } from '@shell/store/type-map';
import {
@@ -31,8 +33,23 @@ import {
FINGERPRINT,
IMAGE_PROGRESS,
SNAPSHOT_TARGET_VOLUME,
IMAGE_VIRTUAL_SIZE
IMAGE_VIRTUAL_SIZE,
IMAGE_STORAGE_CLASS,
HARVESTER_DESCRIPTION,
VM_IMPORT_SOURCE_VM,
VM_IMPORT_SOURCE_CLUSTER,
VM_IMPORT_STATUS,
VM_IMPORT_SOURCE_V_DC,
VM_IMPORT_SOURCE_V_ENDPOINT,
VM_IMPORT_SOURCE_V_STATUS,
VM_IMPORT_SOURCE_O_REGION,
VM_IMPORT_SOURCE_O_ENDPOINT,
VM_IMPORT_SOURCE_O_STATUS,
VM_IMPORT_SOURCE_OVA_URL,
VM_IMPORT_SOURCE_OVA_STATUS,
} from './table-headers';
import { ADD_ONS } from './harvester-map';
import { registerAddonSideNav } from '../utils/dynamic-nav';
const TEMPLATE = HCI.VM_VERSION;
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
@@ -191,6 +208,142 @@ export function init($plugin, store) {
exact: false
});
// ===========================================================================
// VM Import Controller UI Flow
// ===========================================================================
// Define group (Hidden by default)
weightGroup('vmimport', 0, false);
// VirtualMachineImport
headers(HCI.VMIMPORT, [
STATE,
NAME_COL,
NAMESPACE_COL,
VM_IMPORT_SOURCE_VM,
VM_IMPORT_SOURCE_CLUSTER,
VM_IMPORT_STATUS,
AGE
]);
configureType(HCI.VMIMPORT, {
resource: HCI.VMIMPORT,
resourceDetail: HCI.VMIMPORT,
resourceEdit: HCI.VMIMPORT,
location: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT }
}
});
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
name: HCI.VMIMPORT,
labelKey: 'harvester.addons.vmImport.labels.vmimport',
group: 'vmimport',
namespaced: true,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT }
}
});
// Source: VMware
headers(HCI.VMIMPORT_SOURCE_V, [
STATE,
NAME_COL,
VM_IMPORT_SOURCE_V_ENDPOINT,
VM_IMPORT_SOURCE_V_DC,
VM_IMPORT_SOURCE_V_STATUS,
AGE
]);
configureType(HCI.VMIMPORT_SOURCE_V, {
resource: HCI.VMIMPORT_SOURCE_V,
resourceDetail: HCI.VMIMPORT_SOURCE_V,
resourceEdit: HCI.VMIMPORT_SOURCE_V,
location: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_V }
}
});
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
name: HCI.VMIMPORT_SOURCE_V,
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare',
group: 'vmimport',
namespaced: true,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_V }
}
});
// Source: OpenStack
headers(HCI.VMIMPORT_SOURCE_O, [
STATE,
NAME_COL,
VM_IMPORT_SOURCE_O_ENDPOINT,
VM_IMPORT_SOURCE_O_REGION,
VM_IMPORT_SOURCE_O_STATUS,
AGE
]);
configureType(HCI.VMIMPORT_SOURCE_O, {
resource: HCI.VMIMPORT_SOURCE_O,
resourceDetail: HCI.VMIMPORT_SOURCE_O,
resourceEdit: HCI.VMIMPORT_SOURCE_O,
location: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_O }
}
});
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
name: HCI.VMIMPORT_SOURCE_O,
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack',
group: 'vmimport',
namespaced: true,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_O }
}
});
// Source: OVA
headers(HCI.VMIMPORT_SOURCE_OVA, [
STATE,
NAME_COL,
VM_IMPORT_SOURCE_OVA_URL,
VM_IMPORT_SOURCE_OVA_STATUS,
AGE
]);
configureType(HCI.VMIMPORT_SOURCE_OVA, {
resource: HCI.VMIMPORT_SOURCE_OVA,
resourceDetail: HCI.VMIMPORT_SOURCE_OVA,
resourceEdit: HCI.VMIMPORT_SOURCE_OVA,
location: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
}
});
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
name: HCI.VMIMPORT_SOURCE_OVA,
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA',
group: 'vmimport',
namespaced: true,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
}
});
// Enable SideNav based on Addon Status
registerAddonSideNav(store, PRODUCT_NAME, {
addonName: ADD_ONS.VM_IMPORT_CONTROLLER,
resourceType: HCI.ADD_ONS,
navGroup: 'vmimport',
types: [
HCI.VMIMPORT_SOURCE_V,
HCI.VMIMPORT_SOURCE_O,
HCI.VMIMPORT_SOURCE_OVA,
HCI.VMIMPORT
]
});
// ===========================================================================
basicType([HCI.VOLUME]);
configureType(HCI.VOLUME, {
location: {
@@ -221,6 +374,8 @@ export function init($plugin, store) {
STATE,
NAME_COL,
NAMESPACE_COL,
HARVESTER_DESCRIPTION,
IMAGE_STORAGE_CLASS,
IMAGE_PROGRESS,
IMAGE_DOWNLOAD_SIZE,
IMAGE_VIRTUAL_SIZE,
@@ -253,7 +408,7 @@ export function init($plugin, store) {
});
if (isSingleVirtualCluster) {
headers(NAMESPACE, [STATE, NAME_UNLINKED, AGE]);
headers(NAMESPACE, [STATE, NAME_UNLINKED, DESCRIPTION, AGE]);
basicType([NAMESPACE]);
virtualType({
labelKey: 'harvester.namespace.label',
@@ -419,6 +574,8 @@ export function init($plugin, store) {
[
HCI.CLUSTER_NETWORK,
HCI.NETWORK_ATTACHMENT,
HCI.VPC,
NETWORK_POLICY,
HCI.LB,
HCI.IP_POOL,
],
@@ -449,6 +606,7 @@ export function init($plugin, store) {
HCI.PCI_DEVICE,
HCI.SR_IOVGPU_DEVICE,
HCI.VGPU_DEVICE,
HCI.MIG_CONFIGURATION,
HCI.USB_DEVICE,
HCI.ADD_ONS,
HCI.SECRET,
@@ -545,6 +703,36 @@ export function init($plugin, store) {
exact: false
});
configureType(HCI.VPC, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.vpc.label',
name: HCI.VPC,
namespaced: true,
weight: 187,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VPC }
},
exact: false,
ifHaveType: HCI.VPC,
});
configureType(NETWORK_POLICY, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.networkPolicy.label',
name: NETWORK_POLICY,
namespaced: true,
weight: 186,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: NETWORK_POLICY }
},
exact: false,
ifHaveType: NETWORK_POLICY,
});
configureType(HCI.SNAPSHOT, {
isCreatable: false,
location: {
@@ -811,6 +999,26 @@ export function init($plugin, store) {
]
});
virtualType({
labelKey: 'harvester.migconfiguration.label',
group: 'advanced',
weight: 12,
name: HCI.MIG_CONFIGURATION,
namespaced: false,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.MIG_CONFIGURATION }
},
exact: false,
ifHaveType: HCI.MIG_CONFIGURATION,
});
configureType(HCI.MIG_CONFIGURATION, {
isCreatable: false,
hiddenNamespaceGroupButton: true,
canYaml: false,
});
virtualType({
labelKey: 'harvester.usb.label',
group: 'advanced',
+21
View File
@@ -1,3 +1,6 @@
import { HCI } from '../types';
// TODO: delete this not used variable
export const MemoryUnit = [{
label: 'Mi',
value: 'Mi'
@@ -36,6 +39,12 @@ export const VOLUME_TYPE = [{
value: 'cd-rom'
}];
export const VOLUME_HOTPLUG_ACTION = {
INSERT_CDROM_IMAGE: 'INSERT_CDROM_IMAGE',
EJECT_CDROM_IMAGE: 'EJECT_CDROM_IMAGE',
DETACH_DISK: 'DETACH_DISK'
};
export const ACCESS_CREDENTIALS = {
RESET_PWD: 'userPassword',
INJECT_SSH: 'sshPublicKey'
@@ -69,6 +78,7 @@ export const ADD_ONS = {
RANCHER_MONITORING: 'rancher-monitoring',
VM_IMPORT_CONTROLLER: 'vm-import-controller',
LVM_DRIVER: 'lvm.driver.harvesterhci.io',
KUBEOVN_OPERATOR: 'kubeovn-operator',
};
export const CSI_SECRETS = {
@@ -79,3 +89,14 @@ export const CSI_SECRETS = {
CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name',
CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace',
};
// Some harvester CRD type is not equal to model file name, define the mapping here
export const HARVESTER_CRD_MAP = {
node: HCI.HOST,
configmap: HCI.CLOUD_TEMPLATE,
persistentvolumeclaim: HCI.VOLUME,
'snapshot.storage.k8s.io.volumesnapshot': HCI.SNAPSHOT,
// specific groupable table detail page
'network.harvesterhci.io.vlanconfig': HCI.CLUSTER_NETWORK,
'kubeovn.io.subnet': HCI.VPC
};
+74 -56
View File
@@ -7,60 +7,78 @@ export const LABELS_TO_IGNORE_REGEX = [
];
export const HCI = {
CLOUD_INIT: 'harvesterhci.io/cloud-init-template',
CURRENT_IP: 'rke2.io/internal-ip',
IMAGE_ID: 'harvesterhci.io/imageId',
SSH_NAMES: 'harvesterhci.io/sshNames',
NETWORK_IPS: 'network.harvesterhci.io/ips',
TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName',
CREATOR: 'harvesterhci.io/creator',
OS: 'harvesterhci.io/os',
NETWORK_TYPE: 'network.harvesterhci.io/type',
VM_NAME: 'harvesterhci.io/vmName',
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
HOST_CONSOLE_URL: 'harvesterhci.io/host-console-url',
RESTORE_NAME: 'restore.harvesterhci.io/name',
NODE_ROLE_MASTER: 'node-role.kubernetes.io/master',
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
MIGRATION_STATE: 'harvesterhci.io/migrationState',
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
IMAGE_NAME: 'harvesterhci.io/image-name',
INIT_IP: 'etcd.rke2.cattle.io/node-address',
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
NETWORK_ROUTE: 'network.harvesterhci.io/route',
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
UPGRADE_STATE: 'harvesterhci.io/upgradeState',
REAY_MESSAGE: 'harvesterhci.io/read-message',
DYNAMIC_SSHKEYS_NAMES: 'harvesterhci.io/dynamic-ssh-key-names',
DYNAMIC_SSHKEYS_USERS: 'harvesterhci.io/dynamic-ssh-key-users',
IMAGE_SUFFIX: 'harvesterhci.io/image-type',
OS_TYPE: 'harvesterhci.io/os-type',
STORAGE_PROVISIONER: 'harvesterhci.io/storageProvisioner',
HOST_REQUEST: 'management.cattle.io/pod-requests',
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx',
NODE_MANUFACTURER: 'manufacturer',
NODE_MODEL: 'model',
NODE_SERIAL_NUMBER: 'serialNumber',
VM_INSUFFICIENT: 'harvesterhci.io/insufficient-resource-quota',
NODE_NTP_SYNC_STATUS: 'node.harvesterhci.io/ntp-service',
PARENT_SRIOV: 'harvesterhci.io/parent-sriov-network-device',
PARENT_SRIOV_GPU: 'harvesterhci.io/parentSRIOVGPUDevice',
VM_MAINTENANCE_MODE_STRATEGY: 'harvesterhci.io/maintain-mode-strategy',
NODE_CPU_MANAGER_UPDATE_STATUS: 'harvesterhci.io/cpu-manager-update-status',
CPU_MANAGER: 'cpumanager',
VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
CLOUD_INIT: 'harvesterhci.io/cloud-init-template',
CURRENT_IP: 'rke2.io/internal-ip',
IMAGE_ID: 'harvesterhci.io/imageId',
SSH_NAMES: 'harvesterhci.io/sshNames',
NETWORK_IPS: 'network.harvesterhci.io/ips',
TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName',
CREATOR: 'harvesterhci.io/creator',
OS: 'harvesterhci.io/os',
GOLDEN_IMAGE: 'harvesterhci.io/goldenImage',
NETWORK_TYPE: 'network.harvesterhci.io/type',
VM_NAME: 'harvesterhci.io/vmName',
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
HOST_CONSOLE_URL: 'harvesterhci.io/host-console-url',
RESTORE_NAME: 'restore.harvesterhci.io/name',
NODE_ROLE_MASTER: 'node-role.kubernetes.io/master',
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
CLONE_BACKEND_STORAGE_STATUS: 'harvesterhci.io/clone-backend-storage-status',
MIGRATION_STATE: 'harvesterhci.io/migrationState',
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
IMAGE_NAME: 'harvesterhci.io/image-name',
INIT_IP: 'etcd.rke2.cattle.io/node-address',
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
NETWORK_ROUTE: 'network.harvesterhci.io/route',
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
UPGRADE: 'harvesterhci.io/upgrade',
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
UPGRADE_STATE: 'harvesterhci.io/upgradeState',
REAY_MESSAGE: 'harvesterhci.io/read-message',
DYNAMIC_SSHKEYS_NAMES: 'harvesterhci.io/dynamic-ssh-key-names',
DYNAMIC_SSHKEYS_USERS: 'harvesterhci.io/dynamic-ssh-key-users',
IMAGE_SUFFIX: 'harvesterhci.io/image-type',
OS_TYPE: 'harvesterhci.io/os-type',
STORAGE_PROVISIONER: 'harvesterhci.io/storageProvisioner',
SKIP_SINGLE_REPLICA_DETACHED_VOL: 'harvesterhci.io/skipSingleReplicaDetachedVol',
HOST_REQUEST: 'management.cattle.io/pod-requests',
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
ADDON_DISPLAYNAME: 'addon.harvesterhci.io/displayName',
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx',
NODE_MANUFACTURER: 'manufacturer',
NODE_MODEL: 'model',
NODE_SERIAL_NUMBER: 'serialNumber',
VM_INSUFFICIENT: 'harvesterhci.io/insufficient-resource-quota',
NODE_NTP_SYNC_STATUS: 'node.harvesterhci.io/ntp-service',
PARENT_SRIOV: 'harvesterhci.io/parent-sriov-network-device',
PARENT_SRIOV_GPU: 'harvesterhci.io/parentSRIOVGPUDevice',
VM_MAINTENANCE_MODE_STRATEGY: 'harvesterhci.io/maintain-mode-strategy',
NODE_CPU_MANAGER_UPDATE_STATUS: 'harvesterhci.io/cpu-manager-update-status',
CPU_MANAGER: 'cpumanager',
VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
K8S_ARCH: 'kubernetes.io/arch',
IMAGE_DISPLAY_NAME: 'harvesterhci.io/imageDisplayName',
CUSTOM_IP: 'harvesterhci.io/custom-ip',
IMPORTED_IMAGE: 'migration.harvesterhci.io/imported',
VM_CPU_MEMORY_HOTPLUG: 'harvesterhci.io/enableCPUAndMemoryHotplug',
FILESYSTEM_OVERHEAD: 'cdi.harvesterhci.io/filesystemOverhead',
CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy',
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
MAC_ADDRESS: 'harvesterhci.io/mac-address',
NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map',
CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind',
};
+1
View File
@@ -1 +1,2 @@
export const CLUSTER_NETWORK = 'clusterNetwork';
export const VPC = 'vpc';
+52 -14
View File
@@ -4,7 +4,6 @@ export const HCI_SETTING = {
LOG_LEVEL: 'log-level',
SERVER_VERSION: 'server-version',
UI_INDEX: 'ui-index',
UI_PLUGIN_INDEX: 'ui-plugin-index',
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
VLAN: 'vlan',
@@ -17,9 +16,11 @@ export const HCI_SETTING = {
DEFAULT_STORAGE_CLASS: 'default-storage-class',
SUPPORT_BUNDLE_TIMEOUT: 'support-bundle-timeout',
SUPPORT_BUNDLE_EXPIRATION: 'support-bundle-expiration',
SUPPORT_BUNDLE_FILE_NAME: 'support-bundle-file-name',
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT: 'support-bundle-node-collection-timeout',
STORAGE_NETWORK: 'storage-network',
RWX_NETWORK: 'rwx-network',
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
SSL_CERTIFICATES: 'ssl-certificates',
SSL_PARAMETERS: 'ssl-parameters',
@@ -28,12 +29,19 @@ export const HCI_SETTING = {
RELEASE_DOWNLOAD_URL: 'release-download-url',
CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
CSI_DRIVER_CONFIG: 'csi-driver-config',
CSI_ONLINE_EXPAND_VALIDATION: 'csi-online-expand-validation',
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds',
NTP_SERVERS: 'ntp-servers',
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
UPGRADE_CONFIG: 'upgrade-config',
VM_MIGRATION_NETWORK: 'vm-migration-network',
RANCHER_CLUSTER: 'rancher-cluster',
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
KUBEVIRT_MIGRATION: 'kubevirt-migration',
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources'
};
export const HCI_ALLOWED_SETTINGS = {
@@ -52,20 +60,31 @@ export const HCI_ALLOWED_SETTINGS = {
from: 'import',
featureFlag: 'autoRotateRke2CertsSetting'
},
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
[HCI_SETTING.ADDITIONAL_CA]: {
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
[HCI_SETTING.CSI_ONLINE_EXPAND_VALIDATION]: {
kind: 'json', from: 'import', featureFlag: 'csiOnlineExpandValidation'
},
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
[HCI_SETTING.ADDITIONAL_CA]: {
kind: 'multiline', canReset: true, from: 'import'
},
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: {},
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: {},
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: { kind: 'number' },
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: { kind: 'number' },
[HCI_SETTING.SUPPORT_BUNDLE_FILE_NAME]: {
kind: 'string', canReset: true, featureFlag: 'supportBundleFileNameSetting'
},
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { kind: 'number', featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
[HCI_SETTING.STORAGE_NETWORK]: { kind: 'custom', from: 'import' },
[HCI_SETTING.STORAGE_NETWORK]: {
kind: 'custom', from: 'import', canReset: true
},
[HCI_SETTING.RWX_NETWORK]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'rwxNetworkSetting'
},
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
[HCI_SETTING.SSL_PARAMETERS]: {
@@ -74,7 +93,6 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES]: { from: 'import', canReset: true },
[HCI_SETTING.AUTO_DISK_PROVISION_PATHS]: { canReset: true },
[HCI_SETTING.RELEASE_DOWNLOAD_URL]: { kind: 'url' },
[HCI_SETTING.UI_PLUGIN_INDEX]: { kind: 'url' },
[HCI_SETTING.CONTAINERD_REGISTRY]: {
kind: 'json', from: 'import', canReset: true
},
@@ -87,18 +105,38 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.NTP_SERVERS]: {
kind: 'json', from: 'import', canReset: true
},
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { kind: 'number', featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: {
kind: 'boolean',
experimental: true,
featureFlag: 'longhornV2LVMSupport'
},
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
[HCI_SETTING.UPGRADE_CONFIG]: {
kind: 'json',
from: 'import',
featureFlag: 'upgradeConfigSetting',
docPath: 'UPGRADE_CONFIG_URL'
},
[HCI_SETTING.RANCHER_CLUSTER]: {
kind: 'custom', from: 'import', canReset: true, featureFlag: 'rancherClusterSetting'
},
[HCI_SETTING.MAX_HOTPLUG_RATIO]: { kind: 'number', featureFlag: 'cpuMemoryHotplug' },
[HCI_SETTING.VM_MIGRATION_NETWORK]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
},
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'kubevirtMigration',
},
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
}
};
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
[HCI_SETTING.CLUSTER_REGISTRATION_URL]: {
kind: 'url',
kind: 'custom',
from: 'import',
canReset: true,
},
[HCI_SETTING.UI_PL]: {
+142
View File
@@ -1,6 +1,7 @@
/**
* Harvester
*/
import { DESCRIPTION } from '@shell/config/table-headers';
// image
export const IMAGE_DOWNLOAD_SIZE = {
@@ -88,3 +89,144 @@ export const MACHINE_POOLS = {
align: 'center',
width: 100,
};
// The STORAGE_CLASS column in VM image list page
export const IMAGE_STORAGE_CLASS = {
name: 'imageStorageClass',
labelKey: 'harvester.tableHeaders.storageClass',
sort: 'imageStorageClass',
value: 'imageStorageClass',
align: 'left',
width: 150,
};
export const HARVESTER_DESCRIPTION = {
...DESCRIPTION,
width: 150,
};
// The CIDR_BLOCK column in VPC list page
export const CIDR_BLOCK = {
name: 'cidrBlock',
labelKey: 'harvester.subnet.cidrBlock.label',
sort: 'cidrBlock',
value: 'spec.cidrBlock',
align: 'left',
};
// The Protocol column in VPC list page
export const PROTOCOL = {
name: 'protocol',
labelKey: 'harvester.subnet.protocol.label',
sort: 'protocol',
value: 'spec.protocol',
align: 'left',
};
// The Provider column in VPC list page
export const PROVIDER = {
name: 'provider',
labelKey: 'harvester.subnet.provider.label',
sort: 'provider',
value: 'spec.provider',
align: 'left',
};
// Source VM column in migration.harvesterhci.io.virtualmachineimport list page
export const VM_IMPORT_SOURCE_VM = {
name: 'sourceVm',
labelKey: 'harvester.tableHeaders.vmImportSourceVm',
value: 'spec.virtualMachineName',
sort: 'spec.virtualMachineName',
align: 'left',
};
// Source Cluster column in migration.harvesterhci.io.virtualmachineimport list page
export const VM_IMPORT_SOURCE_CLUSTER = {
name: 'sourceCluster',
labelKey: 'harvester.tableHeaders.vmImportSourceCluster',
value: 'spec.sourceCluster.name',
sort: 'spec.sourceCluster.name',
align: 'left',
};
// Import Status column in migration.harvesterhci.io.virtualmachineimport list page
export const VM_IMPORT_STATUS = {
name: 'importStatus',
labelKey: 'harvester.tableHeaders.vmImportStatus',
value: 'status.importStatus',
sort: 'status.importStatus',
align: 'left',
};
// Datacenter column in migration.harvesterhci.io.vmwaresource list page
export const VM_IMPORT_SOURCE_V_DC = {
name: 'datacenter',
labelKey: 'harvester.tableHeaders.vmImportSourceVDatacenter',
value: 'spec.dc',
sort: 'spec.dc',
align: 'left',
};
// Endpoint column in migration.harvesterhci.io.vmwaresource list page
export const VM_IMPORT_SOURCE_V_ENDPOINT = {
name: 'endpoint',
labelKey: 'harvester.tableHeaders.vmImportSourceVEndpoint',
value: 'spec.endpoint',
sort: 'spec.endpoint',
align: 'left',
};
// Cluster Status column in migration.harvesterhci.io.vmwaresource list page
export const VM_IMPORT_SOURCE_V_STATUS = {
name: 'clusterStatus',
labelKey: 'harvester.tableHeaders.vmImportSourceVClusterStatus',
value: 'status.status',
sort: 'status.status',
align: 'left',
};
// Region column in migration.harvesterhci.io.openstacksource list page
export const VM_IMPORT_SOURCE_O_REGION = {
name: 'region',
labelKey: 'harvester.tableHeaders.vmImportSourceORegion',
value: 'spec.region',
sort: 'spec.region',
align: 'left',
};
// Endpoint column in migration.harvesterhci.io.openstacksource list page
export const VM_IMPORT_SOURCE_O_ENDPOINT = {
name: 'endpoint',
labelKey: 'harvester.tableHeaders.vmImportSourceOEndpoint',
value: 'spec.endpoint',
sort: 'spec.endpoint',
align: 'left',
};
// Cluster Status column in migration.harvesterhci.io.openstacksource list page
export const VM_IMPORT_SOURCE_O_STATUS = {
name: 'clusterStatus',
labelKey: 'harvester.tableHeaders.vmImportSourceOClusterStatus',
value: 'status.status',
sort: 'status.status',
align: 'left',
};
// URL column in migration.harvesterhci.io.ovasource list page
export const VM_IMPORT_SOURCE_OVA_URL = {
name: 'url',
labelKey: 'harvester.tableHeaders.vmImportSourceOVAUrl',
value: 'spec.url',
sort: 'spec.url',
align: 'left',
};
// Status column in migration.harvesterhci.io.ovasource list page
export const VM_IMPORT_SOURCE_OVA_STATUS = {
name: 'status',
labelKey: 'harvester.tableHeaders.vmImportSourceOVAStatus',
value: 'status.status',
sort: 'status.status',
align: 'left',
};
+44
View File
@@ -2,3 +2,47 @@ export const BACKUP_TYPE = {
BACKUP: 'backup',
SNAPSHOT: 'snapshot'
};
export const NETWORK_TYPE = {
L2VLAN: 'L2VlanNetwork',
UNTAGGED: 'UntaggedNetwork',
OVERLAY: 'OverlayNetwork',
L2TRUNK_VLAN: 'L2VlanTrunkNetwork',
};
export const VOLUME_MODE = {
BLOCK: 'Block',
FILE_SYSTEM: 'Filesystem'
};
export const NETWORK_PROTOCOL = {
IPv4: 'IPv4',
IPv6: 'IPv6',
};
export const INTERNAL_STORAGE_CLASS = {
VMSTATE_PERSISTENCE: 'vmstate-persistence',
LONGHORN_STATIC: 'longhorn-static',
};
export const L2VLAN_MODE = {
ACCESS: 'access',
TRUNK: 'trunk',
};
export const VMIMPORT_SOURCE_PROVIDER = {
VMWARE: 'vmware',
OPENSTACK: 'openstack',
OVA: 'ova',
};
export const VMIMPORT_SOURCE_KINDS = {
VMWARE: 'VmwareSource',
OPENSTACK: 'OpenstackSource',
OVA: 'OvaSource',
};
export const CDI_POPULATOR_KIND = {
VOLUME_IMPORT_SOURCE: 'VolumeImportSource',
VOLUME_CLONE_SOURCE: 'VolumeCloneSource',
};
@@ -0,0 +1,124 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import SortableTable from '@shell/components/SortableTable';
export default {
components: {
ResourceTabs,
Tab,
SortableTable,
},
mixins: [CreateEditView],
props: {
value: {
type: Object,
default: () => {
return {};
}
}
},
computed: {
headers() {
return [
{
name: 'profileName',
labelKey: 'harvester.migconfiguration.tableHeaders.profileName',
value: 'name',
width: 75,
sort: 'name',
dashIfEmpty: true,
},
{
name: 'vGPUID',
labelKey: 'harvester.migconfiguration.tableHeaders.vGPUID',
value: 'vGPUID',
width: 75,
sort: 'vGPUID',
dashIfEmpty: true,
},
{
name: 'available',
labelKey: 'harvester.migconfiguration.tableHeaders.available',
value: 'available',
width: 75,
sort: 'available',
align: 'center',
dashIfEmpty: true,
},
{
name: 'requested',
labelKey: 'harvester.migconfiguration.tableHeaders.requested',
value: 'requested',
width: 75,
sort: 'requested',
align: 'center',
dashIfEmpty: true,
},
{
name: 'total',
labelKey: 'harvester.migconfiguration.tableHeaders.total',
value: 'total',
width: 75,
sort: 'total',
align: 'center',
dashIfEmpty: true,
},
];
},
rows() {
let out = (this.value?.status?.profileStatus || []).map((profile) => {
const {
id, name, total, available
} = profile;
return {
id,
name,
total,
available,
vGPUID: profile.vGPUID?.join(', ') || '',
};
});
out = out.map((row) => {
const requested = this.value?.spec?.profileSpec.find((p) => p.id === row.id)?.requested || 0;
return { ...row, requested };
});
return out;
},
},
};
</script>
<template>
<ResourceTabs
:value="value"
:need-events="false"
:need-related="false"
:mode="mode"
>
<Tab
name="Profile Status"
:label="t('harvester.migconfiguration.profileStatus')"
>
<SortableTable
:headers="headers"
:rows="rows"
key-field="condition"
default-sort-by="condition"
:table-actions="false"
:row-actions="false"
:search="false"
/>
</Tab>
</ResourceTabs>
</template>
@@ -4,6 +4,7 @@ import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { LONGHORN, METRIC } from '@shell/config/types';
import { Banner } from '@components/Banner';
import { UNIT_SUFFIX } from '../../utils/unit';
import HarvesterCPUUsed from '../../formatters/HarvesterCPUUsed';
import HarvesterMemoryUsed from '../../formatters/HarvesterMemoryUsed';
import HarvesterStorageUsed from '../../formatters/HarvesterStorageUsed';
@@ -121,7 +122,7 @@ export default {
memoryUnits() {
const exponent = exponentNeeded(this.memoryTotal, 1024);
return `${ UNITS[exponent] }iB`;
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`;
},
nodeType() {
@@ -0,0 +1,119 @@
<script>
import LabelValue from '@shell/components/LabelValue';
import { HCI } from '../../types';
import { DOC } from '../../config/doc-links';
export default {
name: 'HarvesterHugepages',
components: { LabelValue },
props: {
node: {
type: Object,
required: true,
},
},
computed: {
docsTransparentHugepagesLink() {
return DOC.TRANSPARENT_HUGEPAGES;
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
this.hugepages = hash.find((node) => {
return node.id === this.node.id;
}) || {};
},
data() {
return { hugepages: {} };
},
};
</script>
<template>
<div>
<template v-if="hugepages.status">
<h2>{{ t('harvester.host.hugepages.meminfo') }}</h2>
<div class="row mb-20">
<div class="col span-6">
<LabelValue
:name="t('harvester.host.hugepages.status.anon')"
:value="hugepages.status.meminfo.anonHugePages"
/>
</div>
<div class="col span-6">
<LabelValue
:name="t('harvester.host.hugepages.status.size')"
:value="hugepages.status.meminfo.hugepageSize"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-3">
<LabelValue
:name="t('harvester.host.hugepages.status.total')"
:value="hugepages.status.meminfo.hugePagesTotal"
/>
</div>
<div class="col span-3">
<LabelValue
:name="t('harvester.host.hugepages.status.free')"
:value="hugepages.status.meminfo.hugePagesFree"
/>
</div>
<div class="col span-3">
<LabelValue
:name="t('harvester.host.hugepages.status.rsvd')"
:value="hugepages.status.meminfo.hugePagesRsvd"
/>
</div>
<div class="col span-3">
<LabelValue
:name="t('harvester.host.hugepages.status.surp')"
:value="hugepages.status.meminfo.hugePagesSurp"
/>
</div>
</div>
<div>
<hr class="divider" />
<h3>
<t
k="harvester.host.hugepages.transparent.title"
:raw="true"
:url="docsTransparentHugepagesLink"
/>
</h3>
<div class="row mb-20">
<div class="col span-4">
<LabelValue
:name="t('harvester.host.hugepages.transparent.enabled')"
:value="hugepages.spec.transparent.enabled"
/>
</div>
<div class="col span-4">
<LabelValue
:name="t('harvester.host.hugepages.transparent.shmemEnabled')"
:value="hugepages.spec.transparent.shmemEnabled"
/>
</div>
<div class="col span-4">
<LabelValue
:name="t('harvester.host.hugepages.transparent.defrag')"
:value="hugepages.spec.transparent.defrag"
/>
</div>
</div>
</div>
</template>
</div>
</template>
@@ -47,20 +47,26 @@ export default {
STATE,
NAME,
{
name: 'vmCPU',
labelKey: 'tableHeaders.cpu',
search: false,
sort: ['spec.template.spec.domain.cpu.cores'],
value: 'spec.template.spec.domain.cpu.cores',
width: 120
name: 'CPU',
label: 'CPU',
sort: ['displayCPU'],
value: 'displayCPU',
align: 'center',
dashIfEmpty: true,
},
{
name: 'vmRAM',
labelKey: 'glance.memory',
search: false,
sort: ['memorySort'],
value: 'spec.template.spec.domain.resources.limits.memory',
width: 120
name: 'Memory',
value: 'displayMemory',
sort: ['memorySort'],
align: 'center',
labelKey: 'tableHeaders.memory',
formatter: 'Si',
formatterOpts: {
opts: {
increment: 1024, addSuffix: true, maxExponent: 3, minExponent: 3, suffix: 'i',
},
needParseSi: true
},
},
{
name: 'ip',
@@ -9,7 +9,7 @@ import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
import Loading from '@shell/components/Loading.vue';
import SortableTable from '@shell/components/SortableTable';
import Banner from '@components/Banner/Banner.vue';
import { UNIT_SUFFIX } from '../../utils/unit';
import metricPoller from '@shell/mixins/metric-poller';
import {
METRIC, NODE, LONGHORN, POD, EVENT
@@ -27,6 +27,7 @@ import Instance from './VirtualMachineInstance';
import Disk from './HarvesterHostDisk';
import VlanStatus from './VlanStatus';
import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
import HarvesterHugepages from './HarvesterHugepages.vue';
import HarvesterSeeder from './HarvesterSeeder';
const LONGHORN_SYSTEM = 'longhorn-system';
@@ -46,6 +47,7 @@ export default {
VlanStatus,
LabelValue,
HarvesterKsmtuned,
HarvesterHugepages,
Loading,
SortableTable,
HarvesterSeeder,
@@ -178,7 +180,7 @@ export default {
minExponent: 3,
maxExponent: 3,
maxPrecision: 2,
suffix: 'iB',
suffix: UNIT_SUFFIX,
};
const longhornDisks = Object.keys(diskStatus).map((key) => {
@@ -209,6 +211,12 @@ export default {
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
},
hasHugepagesSchema() {
const inStore = this.$store.getters['currentProduct'].inStore;
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.HUGEPAGES);
},
hasBlockDevicesSchema() {
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
},
@@ -468,6 +476,16 @@ export default {
/>
</Tab>
<Tab
v-if="hasHugepagesSchema"
name="hugepages"
:weight="0"
:show-header="false"
:label="t('harvester.host.tabs.hugepages')"
>
<HarvesterHugepages :node="value" />
</Tab>
<Tab
v-if="seederEnabled"
name="seeder"
@@ -75,7 +75,7 @@ export default {
<div class="row">
<div class="col span-6 mb-20">
<LabelValue
:name="t('harvester.schedule.cron')"
:name="t('harvester.schedule.cron.label')"
:value="cronExpression"
/>
</div>
@@ -125,6 +125,7 @@ export default {
:resource="value"
:mode="mode"
:apply-hooks="applyHooks"
@error="e=>errors=e"
>
<Tabbed
v-if="spec"
@@ -171,6 +172,9 @@ export default {
:cpu="cpu"
:mode="mode"
:memory="memory"
:max-cpu="maxCpu"
:max-memory="maxMemory"
:enable-hot-plug="cpuMemoryHotplugEnabled"
/>
</div>
<div class="row">
@@ -118,6 +118,10 @@ export default {
imageName() {
return this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-';
},
sourceType() {
return this.value?.spec?.sourceType;
},
}
};
</script>
@@ -252,6 +256,16 @@ export default {
</div>
</div>
<div class="row">
<div class="col span-12">
<LabelValue
:name="t('harvester.image.source')"
:value="sourceType"
class="mb-20"
/>
</div>
</div>
<div
v-if="errorMessage !== '-'"
class="row"
@@ -125,6 +125,7 @@ export default {
:resource="value"
:mode="mode"
:apply-hooks="applyHooks"
@error="e=>errors=e"
>
<Tabbed
v-if="spec"
@@ -171,6 +172,9 @@ export default {
:cpu="cpu"
:mode="mode"
:memory="memory"
:max-cpu="maxCpu"
:max-memory="maxMemory"
:enable-hot-plug="cpuMemoryHotplugEnabled"
/>
<div class="row mb-10">
File diff suppressed because it is too large Load Diff
@@ -5,6 +5,7 @@ import CreateEditView from '@shell/mixins/create-edit-view';
import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
import VMConsoleBar from '../../../components/VMConsoleBar';
import { HCI } from '../../../types';
import { getVmCPUMemoryValues } from '../../../utils/cpuMemory';
const UNDEFINED = 'n/a';
@@ -91,9 +92,9 @@ export default {
},
flavor() {
const domain = this.value?.spec?.template?.spec?.domain;
const { cpu, memory } = getVmCPUMemoryValues(this.value);
return `${ domain.cpu?.cores } vCPU , ${ domain.resources?.limits?.memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
return `${ cpu } vCPU , ${ memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
},
kernelRelease() {
@@ -17,6 +17,13 @@ export default {
default: () => {
return {};
}
},
vmimResource: {
type: Object,
required: true,
default: () => {
return {};
}
}
},
@@ -25,6 +32,12 @@ export default {
},
computed: {
liveMigrationProgressEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
},
migrationPhase() {
return this.vmimResource?.status?.phase || 'N/A';
},
migrationState() {
return this.localResource?.status?.migrationState;
},
@@ -58,6 +71,18 @@ export default {
<template>
<div>
<div
v-if="liveMigrationProgressEnabled"
class="row mb-20"
>
<div class="col span-6">
<LabelValue
:name="t('harvester.virtualMachine.detail.details.phase')"
:value="migrationPhase"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabelValue
@@ -27,6 +27,8 @@ import { formatSi } from '@shell/utils/units';
const VM_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-detail-1/vm-info-detail?orgId=1';
const VM_MIGRATION_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-migration-details-1/harvester-vm-migration-details?orgId=1';
export default {
name: 'VMIDetailsPage',
@@ -63,6 +65,7 @@ export default {
hasResourceQuotaSchema: false,
switchToCloud: false,
VM_METRICS_DETAIL_URL,
VM_MIGRATION_DETAIL_URL,
showVmMetrics: false,
};
},
@@ -78,6 +81,7 @@ export default {
events: this.$store.dispatch(`${ inStore }/findAll`, { type: EVENT }),
allSSHs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SSH }),
vmis: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMI }),
vmims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM }),
restore: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESTORE }),
};
@@ -131,6 +135,39 @@ export default {
return this.$store.getters[`${ inStore }/all`](EVENT);
},
vmim() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vmimList = this.$store.getters[`${ inStore }/all`](HCI.VMIM) || [];
const vmiName = this.vmi?.name || '';
// filter the corresponding vmims by vmim.spec.vmiName and find the latest one by creationTimestamp
const vmim = vmimList.filter((VMIM) => VMIM?.spec?.vmiName === vmiName).sort((a, b) => {
if (a?.metadata?.creationTimestamp > b?.metadata?.creationTimestamp) {
return -1;
}
return 1;
});
return vmim.length > 0 && vmim[0] ? vmim[0] : null;
},
migrationEvents() {
const migrationVMName = this.vmim?.metadata.name || '';
if (migrationVMName === '') {
return [];
}
return this.allEvents.filter((e) => {
const { creationTimestamp } = this.value?.metadata || {};
const involvedName = e?.involvedObject?.name;
return involvedName === migrationVMName && e.firstTimestamp >= creationTimestamp;
}).sort((a, b) => a.lastTimestamp > b.lastTimestamp);
},
events() {
return this.allEvents.filter((e) => {
const { name, creationTimestamp } = this.value?.metadata || {};
@@ -138,7 +175,6 @@ export default {
const pvcName = this.value.persistentVolumeClaimName || [];
const involvedName = e?.involvedObject?.name;
const matchPVC = pvcName.find((name) => name === involvedName);
return (involvedName === name || involvedName === podName || matchPVC) && e.firstTimestamp >= creationTimestamp;
@@ -157,6 +193,10 @@ export default {
vm: this.value.name
};
},
liveMigrationProgressEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
},
},
methods: {
@@ -173,6 +213,7 @@ export default {
const diskRows = this.getDiskRows(neu);
this['diskRows'] = diskRows;
this['networkRows'] = this.getNetworkRows(neu, { fromTemplate: false, init: false });
},
deep: true
}
@@ -225,6 +266,7 @@ export default {
<Network
v-model:value="networkRows"
mode="view"
:vm="value"
/>
</Tab>
@@ -346,6 +388,20 @@ export default {
<Migration
:value="value"
:vmi-resource="vmi"
:vmim-resource="vmim"
/>
<DashboardMetrics
v-if="showVmMetrics && liveMigrationProgressEnabled"
:detail-url="VM_MIGRATION_DETAIL_URL"
graph-height="640px"
:has-summary-and-detail="false"
:vars="graphVars"
class="mb-30"
/>
<Events
v-if="liveMigrationProgressEnabled"
:resource="vmi"
:events="migrationEvents"
/>
</Tab>
+26 -11
View File
@@ -1,12 +1,13 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex';
import { runStrategies as runStrategyOptions } from '../config/harvester-map';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import { Checkbox } from '@components/Form/Checkbox';
import AsyncButton from '@shell/components/AsyncButton';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'CloneVMModal',
@@ -14,7 +15,7 @@ export default {
emits: ['close'],
components: {
AsyncButton, Banner, Checkbox, Card, LabeledInput
AsyncButton, Banner, Checkbox, Card, LabeledInput, LabeledSelect
},
props: {
@@ -26,9 +27,11 @@ export default {
data() {
return {
name: '',
cloneData: true,
errors: []
name: '',
cloneData: true,
errors: [],
runStrategy: runStrategyOptions[1],
runStrategyOptions
};
},
@@ -38,6 +41,9 @@ export default {
actionResource() {
return this.resources[0];
},
vmCloneRunStrategyEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('vmCloneRunStrategy');
},
},
methods: {
@@ -58,7 +64,8 @@ export default {
// deep clone
try {
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
const payload = this.vmCloneRunStrategyEnabled ? { targetVm: this.name, runStrategy: this.runStrategy } : { targetVm: this.name };
const res = await this.actionResource.doAction('clone', payload, {}, false);
if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', {
@@ -106,6 +113,19 @@ export default {
:label="t('harvester.modal.cloneVM.name')"
required
/>
<LabeledSelect
v-if="vmCloneRunStrategyEnabled"
v-model:value="runStrategy"
label-key="harvester.virtualMachine.runStrategy"
:options="runStrategyOptions"
:mode="mode"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template
@@ -127,11 +147,6 @@ export default {
@click="create"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</template>
</Card>
</template>
@@ -0,0 +1,175 @@
<script>
import { mapState } from 'vuex';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { alternateLabel } from '@shell/utils/platform';
import AsyncButton from '@shell/components/AsyncButton';
import { Banner } from '@components/Banner';
import { Card } from '@components/Card';
import { escapeHtml } from '@shell/utils/string';
/**
* @name ConfirmExecutionDialog
* @description Dialog component to confirm the related resources before executing the action.
*/
export default {
name: 'ConfirmExecutionDialog',
emits: ['close'],
components: {
AsyncButton,
Banner,
Card,
},
props: {
/**
* @property resources to be deleted.
* @type {Resource[]} Array of the resource model's instance
*/
resources: {
type: Array,
required: true
}
},
data() {
return { errors: [] };
},
computed: {
...mapState('action-menu', ['modalData']),
warningMessageKey() {
return this.modalData.warningMessageKey;
},
names() {
return this.resources.map((obj) => obj.nameDisplay).slice(0, 5);
},
resourceNames() {
return this.names.reduce((res, name, i) => {
if (i >= 5) {
return res;
}
res += `<b>${ escapeHtml(name) }</b>`;
if (i === this.names.length - 1) {
res += this.plusMore;
} else {
res += i === this.resources.length - 2 ? ' and ' : ', ';
}
return res;
}, '');
},
plusMore() {
const remaining = this.resources.length - this.names.length;
return this.t('dialog.confirmExecution.andOthers', { count: remaining }, true);
},
type() {
const types = new Set(this.resources.reduce((array, each) => {
array.push(each.type);
return array;
}, []));
if (types.size > 1) {
return this.t('generic.resource', { count: this.resources.length });
}
const schema = this.resources[0]?.schema;
if ( !schema ) {
return `resource${ this.resources.length === 1 ? '' : 's' }`;
}
return this.$store.getters['type-map/labelFor'](schema, this.resources.length);
},
protip() {
return this.t('dialog.confirmExecution.protip', { alternateLabel });
},
},
methods: {
escapeHtml,
close() {
this.errors = [];
this.$emit('close');
},
async apply(buttonDone) {
try {
for (const resource of this.resources) {
await resource.doActionGrowl(this.modalData.action, {});
}
buttonDone(true);
this.close();
} catch (e) {
this.errors = exceptionToErrorsArray(e);
buttonDone(false);
}
}
}
};
</script>
<template>
<Card :show-highlight-border="false">
<template #title>
<h4 class="text-default-text">
{{ t('dialog.confirmExecution.title') }}
</h4>
</template>
<template #body>
<div class="pl-10 pr-10">
<span
v-clean-html="t(warningMessageKey, { type, names: resourceNames }, true)"
></span>
<div class="text-info mt-20">
{{ protip }}
</div>
<Banner
v-for="(error, i) in errors"
:key="i"
:label="error"
color="error"
/>
</div>
</template>
<template #actions>
<div class="actions">
<button
class="btn role-secondary"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="apply"
class="btn bg-primary ml-10"
:disabled="applyDisabled"
@click="apply"
/>
</div>
</template>
</Card>
</template>
<style lang='scss' scoped>
.modal-container {
max-width: 400px;
}
.actions {
width: 100%;
text-align: right;
}
</style>
@@ -32,7 +32,7 @@ export default {
resources: {
type: Array,
required: true
}
},
},
data() {
@@ -42,8 +42,25 @@ export default {
computed: {
...mapState('action-menu', ['modalData']),
warningMessageKey() {
return this.modalData.warningMessageKey;
title() {
return this.modalData?.title || 'dialog.promptRemove.title';
},
formattedType() {
return this.type.toLowerCase();
},
warningMessage() {
if (this.modalData?.warningMessage) return this.modalData.warningMessage;
const isPlural = this.type.endsWith('s');
const thisOrThese = isPlural ? 'these' : 'this';
const defaultMessage = this.t('dialog.promptRemove.warningMessage', {
type: this.formattedType,
thisOrThese,
});
return defaultMessage;
},
names() {
@@ -70,6 +87,12 @@ export default {
return this.resources[0].nameDisplay;
},
needConfirmation() {
const { needConfirmation = true } = this.modalData ;
return needConfirmation === true;
},
plusMore() {
const remaining = this.resources.length - this.names.length;
@@ -97,11 +120,15 @@ export default {
},
deleteDisabled() {
if (!this.needConfirmation) {
return false;
}
return this.confirmName !== this.nameToMatch;
},
protip() {
return this.t('promptRemove.protip', { alternateLabel });
return this.t('dialog.promptRemove.protip', { alternateLabel });
},
},
@@ -118,6 +145,7 @@ export default {
try {
for (const resource of this.resources) {
await resource.remove();
if (this.modalData?.extraActionAfterRemove) await this.modalData.extraActionAfterRemove();
}
buttonDone(true);
this.close();
@@ -137,35 +165,45 @@ export default {
>
<template #title>
<h4 class="text-default-text">
{{ t('promptRemove.title') }}
{{ t(title, { type }, true) }}
</h4>
</template>
<template #body>
<div class="pl-10 pr-10">
<span
v-clean-html="t(warningMessageKey, { type, names: resourceNames }, true)"
v-clean-html="warningMessage"
></span>
<div class="mt-10 mb-10">
<span
v-clean-html="t('promptRemove.confirmName', { nameToMatch: escapeHtml(nameToMatch) }, true)"
></span>
</div>
<div class="mb-10">
<CopyToClipboardText :text="nameToMatch" />
</div>
<input
id="confirm"
v-model="confirmName"
type="text"
/>
<div class="text-info mt-20">
{{ protip }}
<div
v-if="needConfirmation"
class="mt-20"
>
<div class="mt-10 mb-10">
<span
v-clean-html="t('dialog.promptRemove.confirmName', {
type: formattedType,
nameToMatch: escapeHtml(nameToMatch)
}, true)"
></span>
</div>
<div class="mb-10">
<CopyToClipboardText :text="nameToMatch" />
</div>
<input
id="confirm"
v-model="confirmName"
type="text"
/>
<div class="text-info mt-20">
{{ protip }}
</div>
</div>
<Banner
v-for="(error, i) in errors"
:key="i"
:label="error"
color="error"
/>
</div>
</template>
-150
View File
@@ -1,150 +0,0 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import { Checkbox } from '@components/Form/Checkbox';
import AsyncButton from '@shell/components/AsyncButton';
import { LabeledInput } from '@components/Form/LabeledInput';
export default {
name: 'CloneVMModal',
emits: ['close'],
components: {
AsyncButton, Banner, Checkbox, Card, LabeledInput
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
return {
name: '',
cloneData: true,
errors: []
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0];
},
},
methods: {
close() {
this.name = '';
this.$emit('close');
},
async create(buttonCb) {
// shadow clone
if (!this.cloneData) {
this.resources[0].goToClone();
buttonCb(false);
this.close();
return;
}
// deep clone
try {
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', {
title: this.t('harvester.notification.title.succeed'),
message: this.t('harvester.modal.cloneVM.message.success', { name: this.name })
}, { root: true });
this.close();
buttonCb(true);
} else {
const error = [res?.data] || exceptionToErrorsArray(res);
this['errors'] = error;
buttonCb(false);
}
} catch (err) {
const error = err?.data || err;
const message = exceptionToErrorsArray(error);
this['errors'] = message;
buttonCb(false);
}
}
},
};
</script>
<template>
<Card :show-highlight-border="false">
<template #title>
{{ t('harvester.modal.cloneVM.title') }}
</template>
<template #body>
<Checkbox
v-model:value="cloneData"
class="mb-10"
label-key="harvester.modal.cloneVM.type"
/>
<LabeledInput
v-show="cloneData"
v-model:value="name"
class="mb-20"
:label="t('harvester.modal.cloneVM.name')"
required
/>
</template>
<template #actions>
<div class="actions">
<div class="buttons">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="create"
:action-label="cloneData ? t('harvester.modal.cloneVM.action.create') : t('harvester.modal.cloneVM.action.clone')"
:disabled="cloneData && !name"
@click="create"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -0,0 +1,212 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex';
import { NETWORK_ATTACHMENT } from '@shell/config/types';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { NETWORK_TYPE } from '../config/types';
export default {
name: 'AddHotplugNic',
emits: ['close'],
components: {
AsyncButton,
Card,
LabeledInput,
LabeledSelect,
Banner
},
props: {
resources: {
type: Array,
required: true
}
},
async fetch() {
try {
this.allVMNetworks = await this.$store.dispatch('harvester/findAll', { type: NETWORK_ATTACHMENT });
} catch (err) {
this.errors = exceptionToErrorsArray(err);
this.allVMNetworks = [];
}
},
data() {
return {
interfaceName: '',
networkName: '',
macAddress: '',
allVMNetworks: [],
errors: [],
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources?.[0];
},
isFormValid() {
return this.interfaceName !== '' && this.networkName !== '';
},
vmNetworksOption() {
return this.allVMNetworks
.filter((network) => {
const labels = network.metadata?.labels || {};
const type = labels[HCI_ANNOTATIONS.NETWORK_TYPE];
const isValidType = [
NETWORK_TYPE.L2VLAN,
NETWORK_TYPE.UNTAGGED,
NETWORK_TYPE.L2TRUNK_VLAN,
].includes(type);
return isValidType && !network.isSystem;
})
.map((network) => {
const label = network.isNotReady ? `${ network.id } (${ this.t('generic.notReady') })` : network.id;
return ({
label,
value: network.id || '',
disabled: network.isNotReady,
});
});
}
},
methods: {
close() {
this.interfaceName = '';
this.networkName = '';
this.macAddress = '';
this.errors = [];
this.$emit('close');
},
async save(buttonCb) {
if (!this.actionResource) {
buttonCb(false);
return;
}
const payload = {
interfaceName: this.interfaceName,
networkName: this.networkName
};
if (this.macAddress) {
payload.macAddress = this.macAddress;
}
try {
const res = await this.actionResource.doAction('addNic', payload);
if ([200, 204].includes(res?._status)) {
this.$store.dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.modal.hotplugNic.success', {
interfaceName: this.interfaceName,
vm: this.actionResource.nameDisplay
})
}, { root: true });
this.close();
buttonCb(true);
} else {
this.errors = exceptionToErrorsArray(res);
buttonCb(false);
}
} catch (err) {
this.errors = exceptionToErrorsArray(err);
buttonCb(false);
}
}
}
};
</script>
<template>
<Card
ref="modal"
name="modal"
:show-highlight-border="false"
>
<template #title>
<h4
v-clean-html="t('harvester.modal.hotplugNic.title')"
class="text-default-text"
/>
</template>
<template #body>
<LabeledInput
v-model:value="interfaceName"
:label="t('generic.name')"
required
/>
<LabeledSelect
v-model:value="networkName"
class="mt-20"
:label="t('harvester.modal.hotplugNic.vmNetwork')"
:options="vmNetworksOption"
required
/>
<LabeledInput
v-model:value="macAddress"
class="mt-20"
label-key="harvester.modal.hotplugNic.macAddress"
:tooltip="t('harvester.modal.hotplugNic.macAddressTooltip', _, true)"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
:label="err"
color="error"
/>
</template>
<template #actions>
<div class="actions">
<div class="buttons">
<button
type="button"
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="apply"
:disabled="!isFormValid"
@click="save"
/>
</div>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -11,7 +11,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'HotplugModal',
name: 'HotplugVolumeModal',
emits: ['close'],
@@ -57,8 +57,12 @@ export default {
if (!!pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
return false;
}
// we won't show golden image volume in the hot plug volume modal
if (pvc.isGoldenImageVolume) {
return false;
}
return !pvc.attachVM;
return true;
})
.map((pvc) => {
return {
@@ -86,7 +90,7 @@ export default {
if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.modal.hotplug.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
message: this.t('harvester.modal.hotplugVolume.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
}, { root: true });
this.close();
@@ -118,7 +122,7 @@ export default {
>
<template #title>
<h4
v-clean-html="t('harvester.modal.hotplug.title')"
v-clean-html="t('harvester.modal.hotplugVolume.title')"
class="text-default-text"
/>
</template>
@@ -137,6 +141,12 @@ export default {
class="mt-20"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
:label="err"
color="error"
/>
</template>
<template #actions>
@@ -156,11 +166,6 @@ export default {
@click="save"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div>
</template>
</Card>
@@ -103,6 +103,12 @@ export default {
:label="t('generic.name')"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -121,13 +127,6 @@ export default {
@click="save"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</div>
</template>
</Card>
@@ -0,0 +1,185 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex';
import { getVmCPUMemoryValues } from '../utils/cpuMemory';
import UnitInput from '@shell/components/form/UnitInput';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import { GIBIBYTE } from '../utils/unit';
export default {
name: 'CPUMemoryHotplugModal',
emits: ['close'],
components: {
AsyncButton, Card, Banner, UnitInput
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
const { cpu, memory } = getVmCPUMemoryValues(this.resources[0] || {});
return {
cpu,
memory,
errors: [],
GIBIBYTE
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0] || {};
},
maxResourcesMessage() {
const { maxCpu, maxMemory } = getVmCPUMemoryValues(this.actionResource);
if (maxCpu && maxMemory) {
return this.t('harvester.modal.cpuMemoryHotplug.maxResourcesMessage', { maxCpu, maxMemory });
}
return '';
}
},
methods: {
close() {
this.cpu = '';
this.memory = '';
this.$emit('close');
},
change() {
if (parseInt(this.memory, 10) < 1 ) {
this.memory = '1Gi';
}
if (this.cpu < 1) {
this.cpu = 1;
}
},
async save(buttonCb) {
if (this.actionResource) {
try {
const res = await this.actionResource.doAction('cpuAndMemoryHotplug', { sockets: this.cpu, memory: this.memory });
if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.modal.cpuMemoryHotplug.success', { vm: this.actionResource.nameDisplay })
}, { root: true });
this.close();
buttonCb(true);
} else {
const error = [res?.data] || exceptionToErrorsArray(res);
this['errors'] = error;
buttonCb(false);
}
} catch (err) {
const error = err?.data || err;
const message = exceptionToErrorsArray(error);
this['errors'] = message;
buttonCb(false);
}
}
},
}
};
</script>
<template>
<Card
ref="modal"
name="modal"
:show-highlight-border="false"
>
<template #title>
<h4
v-clean-html="t('harvester.modal.cpuMemoryHotplug.title')"
class="text-default-text"
/>
</template>
<template #body>
<Banner
v-if="maxResourcesMessage"
:label="maxResourcesMessage"
color="info"
/>
<UnitInput
v-model:value="cpu"
:label="t('harvester.virtualMachine.input.cpu')"
:delay="0"
required
suffix="C"
class="mb-20"
:mode="mode"
:min="1"
@update:value="change"
/>
<UnitInput
v-model:value="memory"
:label="t('harvester.virtualMachine.input.memory')"
:mode="mode"
:input-exponent="3"
:delay="0"
:min="1"
:increment="1024"
:output-modifier="true"
:disabled="disabled"
:suffix="GIBIBYTE"
class="mb-20"
@update:value="change"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
:label="err"
color="error"
/>
</template>
<template #actions>
<div class="actions">
<div class="buttons">
<button
type="button"
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="apply"
@click="save"
/>
</div>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -120,6 +120,12 @@ export default {
v-model:value="description"
:label="t('harvester.modal.createTemplate.description')"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -138,11 +144,6 @@ export default {
@click="save"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div>
</template>
</Card>
@@ -0,0 +1,183 @@
<script>
import { mapGetters } from 'vuex';
import { STORAGE_CLASS } from '@shell/config/types';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { sortBy } from '@shell/utils/sort';
import { isInternalStorageClass } from '../utils/storage-class';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import { LabeledInput } from '@components/Form/LabeledInput';
import AsyncButton from '@shell/components/AsyncButton';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'HarvesterDataMigrationDialog',
emits: ['close'],
components: {
AsyncButton, Banner, Card, LabeledInput, LabeledSelect
},
props: {
resources: {
type: Array,
required: true
}
},
async fetch() {
this.storageClasses = await this.$store.dispatch('harvester/findAll', { type: STORAGE_CLASS });
},
data() {
return {
targetVolumeName: '',
targetStorageClassName: '',
errors: [],
storageClasses: [],
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0];
},
storageClassOptions() {
return sortBy(
this.storageClasses
.filter((sc) => !isInternalStorageClass(sc.metadata?.name))
.map((sc) => ({
label: sc.metadata?.name,
value: sc.metadata?.name
})),
'label'
);
},
disableSave() {
return !this.targetVolumeName || !this.targetStorageClassName;
},
},
methods: {
close() {
this.targetVolumeName = '';
this.targetStorageClassName = '';
this.errors = [];
this.$emit('close');
},
async apply(buttonDone) {
if (!this.actionResource) {
buttonDone(false);
return;
}
if (!this.targetVolumeName) {
const name = this.t('harvester.modal.dataMigration.fields.targetVolumeName.label');
this['errors'] = [this.t('validation.required', { key: name })];
buttonDone(false);
return;
}
if (!this.targetStorageClassName) {
const name = this.t('harvester.modal.dataMigration.fields.targetStorageClassName.label');
this['errors'] = [this.t('validation.required', { key: name })];
buttonDone(false);
return;
}
try {
await this.actionResource.doAction('dataMigration', {
targetVolumeName: this.targetVolumeName,
targetStorageClassName: this.targetStorageClassName
}, {}, false);
buttonDone(true);
this.close();
} catch (err) {
const error = err?.data || err;
this['errors'] = exceptionToErrorsArray(error);
buttonDone(false);
}
},
}
};
</script>
<template>
<Card :show-highlight-border="false">
<template #title>
{{ t('harvester.modal.dataMigration.title') }}
</template>
<template #body>
<LabeledInput
v-model:value="targetVolumeName"
:label="t('harvester.modal.dataMigration.fields.targetVolumeName.label')"
:placeholder="t('harvester.modal.dataMigration.fields.targetVolumeName.placeholder')"
class="mb-20"
required
/>
<LabeledSelect
v-model:value="targetStorageClassName"
:label="t('harvester.modal.dataMigration.fields.targetStorageClassName.label')"
:placeholder="t('harvester.modal.dataMigration.fields.targetStorageClassName.placeholder')"
:options="storageClassOptions"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template
#actions
class="actions"
>
<div class="buttons">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="apply"
:disabled="disableSave"
@click="apply"
/>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -128,6 +128,12 @@ export default {
<Banner color="warning">
<span>{{ t('harvester.modal.ejectCDROM.warnTip') }}</span>
</Banner>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -147,13 +153,6 @@ export default {
@click="remove"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</div>
</template>
</Card>
@@ -10,6 +10,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { NAMESPACE, STORAGE_CLASS } from '@shell/config/types';
import { allHash } from '@shell/utils/promise';
import { isInternalStorageClass } from '../utils/storage-class';
export default {
name: 'HarvesterExportImageDialog',
@@ -34,9 +35,14 @@ export default {
await allHash(hash);
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find((s) => s.isDefault);
const allStorages = this.allStorageClasses;
const defaultStorage = allStorages.find((s) => s.isDefault);
this['storageClassName'] = defaultStorage?.metadata?.name || 'longhorn';
if (this.isLonghornV1Volume) {
this['storageClassName'] = defaultStorage?.metadata?.name || 'longhorn';
} else {
this['storageClassName'] = this.nonLonghornV1StorageClasses[0]?.metadata?.name || '';
}
},
data() {
@@ -54,7 +60,7 @@ export default {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0];
return this.resources[0] || {};
},
namespaces() {
@@ -73,24 +79,48 @@ export default {
return out;
},
isLonghornV1Volume() {
return this.actionResource?.isLonghornV1 === true;
},
disableSave() {
return !(this.name && this.namespace && this.storageClassName);
},
storageClassOptions() {
allStorageClasses() {
const inStore = this.$store.getters['currentProduct'].inStore;
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
const out = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
return this.$store.getters[`${ inStore }/all`](STORAGE_CLASS) || [];
},
nonLonghornV1StorageClasses() {
return this.allStorageClasses.filter((s) => s.isLonghornV1 === false) || [];
},
storageClassOptions() {
let storages = this.allStorageClasses;
// Volume with non-longhorn v1 sc can't be exported to longhorn v1 sc image
if (!this.isLonghornV1Volume) {
storages = this.allStorageClasses.filter((s) => s.isLonghornV1 === false);
}
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
const isInternal = isInternalStorageClass(s.metadata?.name);
if (isInternal) {
label += ` (${ this.t('harvester.storage.internal.label') })`;
}
return {
label,
value: s.name,
value: s.name,
disabled: isInternal,
};
}) || [];
return out;
return options;
},
},
@@ -164,6 +194,12 @@ export default {
class="mt-20"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -182,11 +218,6 @@ export default {
@click="save"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div>
</template>
</Card>
+185
View File
@@ -0,0 +1,185 @@
<script>
import { mapState, mapGetters } from 'vuex';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
const VOLUME = 'volume';
const NETWORK = 'network';
const CDROM = 'cdrom';
export default {
name: 'HarvesterHotUnplug',
emits: ['close'],
components: {
AsyncButton,
Card,
Banner
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
return { errors: [] };
},
computed: {
...mapState('action-menu', ['modalData']),
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0];
},
name() {
return this.modalData.name;
},
isVolume() {
return this.modalData.type === VOLUME;
},
titleKey() {
const keys = {
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.title',
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.title',
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.title',
};
return keys[this.modalData.type];
},
actionLabelKey() {
const keys = {
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabels',
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.actionLabels',
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.actionLabels',
};
return keys[this.modalData.type];
},
successMessageKey() {
const keys = {
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.success',
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.success',
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.success',
};
return keys[this.modalData.type];
}
},
methods: {
close() {
this.$emit('close');
},
async save(buttonCb) {
try {
let res;
if (this.modalData.type === VOLUME) {
res = await this.actionResource.doAction('removeVolume', { diskName: this.name });
} else if (this.modalData.type === NETWORK) {
res = await this.actionResource.doAction('removeNic', { interfaceName: this.name });
} else {
res = await this.actionResource.doAction('ejectCdRomVolume', { deviceName: this.name });
}
if (res._status === 200 || res._status === 204) {
this.$store.dispatch(
'growl/success',
{
title: this.t('generic.notification.title.succeed'),
message: this.t(this.successMessageKey, { name: this.name })
},
{ root: true }
);
this.close();
buttonCb(true);
} else {
const error = [res?.data] || exceptionToErrorsArray(res);
this.errors = error;
buttonCb(false);
}
} catch (err) {
const error = err?.data || err;
const message = exceptionToErrorsArray(error);
this.errors = message;
buttonCb(false);
}
}
}
};
</script>
<template>
<Card
ref="modal"
name="modal"
:show-highlight-border="false"
>
<template #title>
<h4
v-clean-html="t(titleKey, { name })"
class="text-default-text"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
<div class="actions">
<div class="buttons">
<button
type="button"
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="apply"
:action-label="t(actionLabelKey)"
:waiting-label="t(actionLabelKey)"
:success-label="t(actionLabelKey)"
@click="save"
/>
</div>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
::v-deep(.card-title) {
display: block;
}
</style>
@@ -0,0 +1,165 @@
<script>
import { mapGetters } from 'vuex';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { HCI } from '../types';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import AppModal from '@shell/components/AppModal';
export default {
name: 'HarvesterImageDownloaderDialog',
emits: ['close'],
components: {
AsyncButton, Banner, Card, AppModal
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
return { errors: [], isOpen: false };
},
computed: {
...mapGetters({ t: 'i18n/t' }),
downloadImageInProgress() {
return this.$store.getters['harvester-common/isDownloadImageInProgress'];
},
image() {
return this.resources[0] || {};
},
imageName() {
return this.image?.name || '';
},
imageVirtualSize() {
return this.image?.virtualSize || this.image?.downSize || '';
}
},
methods: {
async cancelDownload() {
const url = this.image?.links?.downloadcancel;
if (url) {
await this.$store.dispatch('harvester/request', { url });
}
},
async close() {
if (this.downloadImageInProgress) {
this.$store.commit('harvester-common/setDownloadImageCancel', true);
this.$store.commit('harvester-common/setDownloadImageInProgress', false);
await this.cancelDownload();
}
this.$emit('close');
},
async startDownload(buttonCb) {
// clean the download image CRD first.
await this.cancelDownload();
this.$store.commit('harvester-common/setDownloadImageCancel', false);
this.$store.commit('harvester-common/setDownloadImageInProgress', false);
this.errors = [];
const name = this.image?.name || '';
const namespace = this.image?.namespace || '';
const imageCrd = {
apiVersion: 'harvesterhci.io/v1beta1',
type: HCI.VM_IMAGE_DOWNLOADER,
kind: 'VirtualMachineImageDownloader',
metadata: {
name,
namespace
},
spec: { imageName: name }
};
const inStore = this.$store.getters['currentProduct'].inStore;
const imageCreate = await this.$store.dispatch(`${ inStore }/create`, imageCrd);
try {
await imageCreate.save();
this.$store.commit('harvester-common/setDownloadImageId', `${ namespace }/${ name }`, { root: true });
this.$store.dispatch('harvester-common/downloadImageProgress', { root: true });
} catch (err) {
this.errors = exceptionToErrorsArray(err);
buttonCb(false);
}
}
},
};
</script>
<template>
<app-modal
class="image-downloader-modal"
name="image-download-dialog"
height="auto"
:width="600"
:click-to-close="false"
@close="close"
>
<Card :show-highlight-border="false">
<template #title>
{{ t('harvester.modal.downloadImage.title') }}
</template>
<template #body>
<Banner color="info">
{{ t('harvester.modal.downloadImage.banner', { size: imageVirtualSize }) }}
</Banner>
{{ t('harvester.modal.downloadImage.startMessage') }}
<br /><br />
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
<div class="actions">
<div class="buttons">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
type="submit"
mode="download"
:disabled="downloadImageInProgress"
@click="startDownload"
/>
</div>
</div>
</template>
</Card>
</app-modal>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -0,0 +1,198 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapState, mapGetters } from 'vuex';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { HCI } from '../types';
export default {
name: 'HarvesterInsertCdRomVolume',
emits: ['close'],
components: {
AsyncButton,
Card,
LabeledInput,
LabeledSelect,
Banner
},
props: {
resources: {
type: Array,
required: true
}
},
async fetch() {
try {
this.images = await this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE });
} catch (err) {
this.errors = exceptionToErrorsArray(err);
this.images = [];
}
},
data() {
return {
imageName: '',
images: [],
errors: [],
};
},
computed: {
...mapState('action-menu', ['modalData']),
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources?.[0];
},
isFormValid() {
return this.imageName !== '';
},
deviceName() {
return this.modalData.name;
},
imagesOption() {
return this.images
.filter((image) => {
const labels = image.metadata?.labels || {};
const type = labels[HCI_ANNOTATIONS.IMAGE_SUFFIX];
return type === 'iso';
})
.map((image) => {
return ({
label: this.imageOptionLabel(image),
value: image.id,
disabled: image.isImportedImage
});
});
}
},
methods: {
close() {
this.imageName = '';
this.errors = [];
this.$emit('close');
},
imageOptionLabel(image) {
return `${ image.metadata.namespace }/${ image.spec.displayName }`;
},
async save(buttonCb) {
if (!this.actionResource) {
buttonCb(false);
return;
}
const payload = {
deviceName: this.deviceName,
imageName: this.imageName
};
try {
const res = await this.actionResource.doAction('insertCdRomVolume', payload);
if ([200, 204].includes(res?._status)) {
this.$store.dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.modal.insertCdRomVolume.success', {
deviceName: this.deviceName,
imageName: this.imageName,
})
}, { root: true });
this.close();
buttonCb(true);
} else {
this.errors = exceptionToErrorsArray(res);
buttonCb(false);
}
} catch (err) {
this.errors = exceptionToErrorsArray(err);
buttonCb(false);
}
}
}
};
</script>
<template>
<Card
ref="modal"
name="modal"
:show-highlight-border="false"
>
<template #title>
<h4
v-clean-html="t('harvester.modal.insertCdRomVolume.title')"
class="text-default-text"
/>
</template>
<template #body>
<LabeledInput
v-model:value="deviceName"
:label="t('generic.name')"
disabled
/>
<LabeledSelect
v-model:value="imageName"
class="mt-20"
:label="t('harvester.modal.insertCdRomVolume.image')"
:options="imagesOption"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
:label="err"
color="error"
/>
</template>
<template #actions>
<div class="actions">
<div class="buttons">
<button
type="button"
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="apply"
:disabled="!isFormValid"
@click="save"
/>
</div>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -161,6 +161,12 @@ export default {
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
:options="nodeNameList"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template
@@ -181,12 +187,6 @@ export default {
@click="apply"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
</Card>
</template>
@@ -5,6 +5,7 @@ import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import UnitInput from '@shell/components/form/UnitInput';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { GIBIBYTE } from '../utils/unit';
export default {
name: 'HarvesterVMQuotaDialog',
@@ -29,6 +30,7 @@ export default {
data() {
return {
GIBIBYTE,
totalSnapshotSize: '',
errors: []
};
@@ -44,7 +46,6 @@ export default {
},
methods: {
close() {
this.totalSnapshotSize = '';
this.$emit('close');
@@ -92,9 +93,15 @@ export default {
:input-exponent="3"
:increment="1024"
:output-modifier="true"
suffix="GiB"
:suffix="GIBIBYTE"
class="mb-20"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -108,12 +115,6 @@ export default {
</button>
<AsyncButton @click="save" />
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</div>
</template>
</Card>
@@ -136,6 +136,12 @@ export default {
:options="backupOption"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -154,11 +160,6 @@ export default {
@click="saveRestore"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div>
</template>
</Card>
@@ -0,0 +1,239 @@
<script>
import { mapGetters } from 'vuex';
import { PVC } from '@shell/config/types';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { sortBy } from '@shell/utils/sort';
import { HCI } from '../types';
import { parseVolumeClaimTemplates } from '@pkg/harvester/utils/vm';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'HarvesterStorageMigrationDialog',
emits: ['close'],
components: {
AsyncButton, Banner, Card, LabeledSelect
},
props: {
resources: {
type: Array,
required: true
}
},
async fetch() {
this.allPVCs = await this.$store.dispatch('harvester/findAll', { type: PVC });
},
data() {
return {
sourceVolume: '',
targetVolume: '',
errors: [],
allPVCs: [],
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0];
},
sourceVolumeOptions() {
const volumes = this.actionResource.spec?.template?.spec?.volumes || [];
return sortBy(
volumes
.map((v) => v.persistentVolumeClaim?.claimName)
.filter((name) => !!name)
.map((name) => ({
label: name,
value: name
})),
'label'
);
},
namespacePVCs() {
return this.allPVCs.filter((pvc) => pvc.metadata.namespace === this.actionResource.metadata.namespace);
},
vmUsedVolumeNames() {
const allVMs = this.$store.getters['harvester/all'](HCI.VM) || [];
const names = new Set();
allVMs.forEach((vm) => {
// Collect volume names from spec.template.spec.volumes (both PVC and DataVolume references)
const volumes = vm.spec?.template?.spec?.volumes || [];
volumes.forEach((v) => {
const name = v.persistentVolumeClaim?.claimName || v.dataVolume?.name;
if (name) {
names.add(`${ vm.metadata.namespace }/${ name }`);
}
});
// Collect volume names from volumeClaimTemplates annotation
const templates = parseVolumeClaimTemplates(vm);
templates.forEach((t) => {
if (t.metadata?.name) {
names.add(`${ vm.metadata.namespace }/${ t.metadata.name }`);
}
});
});
return names;
},
targetVolumeOptions() {
return sortBy(
this.namespacePVCs
.filter((pvc) => {
// Exclude volumes used by any VM (via spec.volumes or volumeClaimTemplates)
if (this.vmUsedVolumeNames.has(`${ pvc.metadata.namespace }/${ pvc.metadata.name }`)) {
return false;
}
return true;
})
.map((pvc) => ({
label: pvc.metadata.name,
value: pvc.metadata.name
})),
'label'
);
},
disableSave() {
return !this.sourceVolume || !this.targetVolume;
},
},
methods: {
close() {
this.sourceVolume = '';
this.targetVolume = '';
this.errors = [];
this.$emit('close');
},
async apply(buttonDone) {
if (!this.actionResource) {
buttonDone(false);
return;
}
if (!this.sourceVolume) {
const name = this.t('harvester.modal.storageMigration.fields.sourceVolume.label');
this['errors'] = [this.t('validation.required', { key: name })];
buttonDone(false);
return;
}
if (!this.targetVolume) {
const name = this.t('harvester.modal.storageMigration.fields.targetVolume.label');
this['errors'] = [this.t('validation.required', { key: name })];
buttonDone(false);
return;
}
try {
await this.actionResource.doAction('storageMigration', {
sourceVolume: this.sourceVolume,
targetVolume: this.targetVolume
}, {}, false);
buttonDone(true);
this.close();
} catch (err) {
const error = err?.data || err;
this['errors'] = exceptionToErrorsArray(error);
buttonDone(false);
}
},
}
};
</script>
<template>
<Card :show-highlight-border="false">
<template #title>
{{ t('harvester.modal.storageMigration.title') }}
</template>
<template #body>
<LabeledSelect
v-model:value="sourceVolume"
:label="t('harvester.modal.storageMigration.fields.sourceVolume.label')"
:placeholder="t('harvester.modal.storageMigration.fields.sourceVolume.placeholder')"
:options="sourceVolumeOptions"
class="mb-20"
required
/>
<LabeledSelect
v-model:value="targetVolume"
:label="t('harvester.modal.storageMigration.fields.targetVolume.label')"
:placeholder="t('harvester.modal.storageMigration.fields.targetVolume.placeholder')"
:options="targetVolumeOptions"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template
#actions
class="actions"
>
<div class="buttons">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="apply"
:disabled="disableSave"
@click="apply"
/>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
+254 -86
View File
@@ -1,4 +1,5 @@
<script>
import { NAMESPACE } from '@shell/config/types';
import { randomStr } from '@shell/utils/string';
import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
import { LabeledInput } from '@components/Form/LabeledInput';
@@ -6,7 +7,15 @@ import AsyncButton from '@shell/components/AsyncButton';
import GraphCircle from '@shell/components/graph/Circle';
import { Banner } from '@components/Banner';
import AppModal from '@shell/components/AppModal';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import UnitInput from '@shell/components/form/UnitInput';
import { HCI } from '../types';
import { HCI_SETTING } from '../config/settings';
import { DOC } from '../config/doc-links';
import { docLink } from '../utils/feature-flags';
const SELECT_ALL = 'select_all';
const UNSELECT_ALL = 'unselect_all';
export default {
name: 'SupportBundle',
@@ -17,14 +26,36 @@ export default {
AsyncButton,
Banner,
AppModal,
LabeledSelect,
UnitInput
},
async fetch() {
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
try {
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
const response = await this.$store.dispatch('harvester/request', { url });
this.defaultNamespaces = response.data || [];
} catch (error) {
this.defaultNamespaces = [];
}
},
data() {
return {
url: '',
description: '',
errors: [],
isOpen: false,
isOpen: false,
errors: [],
version: '',
clusterName: '',
url: '',
description: '',
namespaces: [],
defaultNamespaces: [],
timeout: '',
expiration: '',
nodeTimeout: '',
};
},
@@ -39,23 +70,51 @@ export default {
percentage() {
return this.$store.getters['harvester-common/getBundlePercentage'];
}
},
availableNamespaces() {
const allNamespaces = this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
const defaultNamespacesIds = this.defaultNamespaces.map((ns) => ns.id);
return allNamespaces.filter((ns) => !defaultNamespacesIds.includes(ns) || this.namespaces.includes(ns));
},
namespaceOptions() {
if (this.availableNamespaces.length === 0) return [];
const allSelected = this.namespaces.length === this.availableNamespaces.length &&
this.availableNamespaces.every((ns) => this.namespaces.includes(ns));
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
return [controlOption, ...this.availableNamespaces];
},
docLink() {
const version = this.$store.getters['harvester-common/getServerVersion']();
return docLink(DOC.SUPPORT_BUNDLE_NAMESPACES, version);
},
customSupportBundleFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
},
},
watch: {
isShowBundleModal: {
immediate: true,
handler(show) {
if (show) {
this.$nextTick(() => {
this.isOpen = true;
});
} else {
this.isOpen = false;
this.url = '';
this.description = '';
}
},
immediate: true
this.isOpen = show;
}
},
isOpen(newVal) {
if (newVal) {
this.loadDefaultSettings();
} else {
this.resetForm();
}
},
},
@@ -65,33 +124,93 @@ export default {
close() {
this.isOpen = false;
this.$store.commit('harvester-common/toggleBundleModal', false);
this.backUpName = '';
},
loadDefaultSettings() {
const cluster = this.$store.getters['currentCluster'];
const versionSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SERVER_VERSION);
const namespacesSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES);
const timeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT);
const expirationSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION);
const nodeTimeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT);
this.version = versionSetting?.currentVersion || '';
this.clusterName = cluster?.id || '';
this.namespaces = (namespacesSetting?.value ?? namespacesSetting?.default ?? '').split(',').map((ns) => ns.trim()).filter((ns) => ns);
this.timeout = timeoutSetting?.value ?? timeoutSetting?.default ?? '';
this.expiration = expirationSetting?.value ?? expirationSetting?.default ?? '';
this.nodeTimeout = nodeTimeoutSetting?.value ?? nodeTimeoutSetting?.default ?? '';
this.url = '';
this.description = '';
this.errors = [];
},
resetForm() {
this.url = '';
this.description = '';
this.namespaces = [];
this.timeout = '';
this.expiration = '';
this.nodeTimeout = '';
this.errors = [];
},
updateNamespaces(selected) {
if (selected.includes(SELECT_ALL)) {
this.namespaces = [...this.availableNamespaces];
} else if (selected.includes(UNSELECT_ALL)) {
this.namespaces = [];
} else {
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
}
},
updateNumberValue(field, value) {
if (value === '' || value === null || isNaN(value)) {
this[field] = '';
return;
}
const num = Number(value);
const isValid = Number.isInteger(num) && num >= 0;
this[field] = isValid ? String(num) : '';
},
onKeyDown(e) {
if (['e', 'E', '+', '-', '.'].includes(e.key)) {
e.preventDefault();
}
},
async save(buttonCb) {
this.errors = [];
const name = `bundle-${ randomStr(5).toLowerCase() }`;
const name = `bundle-${ this.clusterName }-${ this.version }-${ randomStr(5).toLowerCase() }`;
const namespace = 'harvester-system';
const spec = {
description: this.description.trim(),
...(this.url.trim() && { issueURL: this.url.trim() }),
...(this.namespaces.length > 0 && { extraCollectionNamespaces: this.namespaces }),
...(this.timeout !== '' && { timeout: Number(this.timeout) }),
...(this.expiration !== '' && { expiration: Number(this.expiration) }),
...(this.nodeTimeout !== '' && { nodeTimeout: Number(this.nodeTimeout) }),
};
const bundleCrd = {
apiVersion: 'harvesterhci.io/v1beta1',
type: HCI.SUPPORT_BUNDLE,
kind: 'SupportBundle',
metadata: {
name,
namespace
},
spec: {
issueURL: this.url,
description: this.description
}
metadata: { name, namespace },
spec,
};
const inStore = this.$store.getters['currentProduct'].inStore;
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
try {
const inStore = this.$store.getters['currentProduct'].inStore;
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
await bundleValue.save();
this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true });
@@ -118,69 +237,111 @@ export default {
@close="close"
>
<div class="p-20">
<h2>
{{ t('harvester.modal.bundle.title') }}
</h2>
<div
v-if="!bundlePending"
class="content"
>
<LabeledInput
v-model:value="url"
:label="t('harvester.modal.bundle.url')"
class="mb-20"
/>
<LabeledInput
v-model:value="description"
:label="t('harvester.modal.bundle.description')"
type="multiline"
:min-height="120"
required
/>
</div>
<div
v-else
class="content"
>
<div class="circle">
<h2>{{ t('harvester.modal.bundle.title') }}</h2>
<div class="content">
<div
v-if="bundlePending"
class="circle mb-20"
>
<GraphCircle
primary-stroke-color="green"
secondary-stroke-color="white"
secondary-stroke-color="lightgrey"
:stroke-width="6"
:percentage="percentage"
:show-text="true"
/>
</div>
</div>
<template v-else>
<p
v-if="customSupportBundleFeatureEnabled"
v-clean-html="t('harvester.modal.bundle.tip', { doc: docLink }, true)"
class="mb-20"
/>
<LabeledInput
v-model:value="url"
:label="t('harvester.modal.bundle.url')"
class="mb-10"
/>
<LabeledInput
v-model:value="description"
required
:label="t('harvester.modal.bundle.description')"
type="multiline"
:min-height="80"
class="mb-10"
/>
<div
v-for="(err, idx) in errors"
:key="idx"
>
<Banner
color="error"
:label="stringify(err)"
/>
</div>
<div class="footer mt-20">
<button
class="btn btn-sm role-secondary mr-10"
@click="close"
<template v-if="customSupportBundleFeatureEnabled">
<LabeledSelect
v-model:value="namespaces"
:label="t('harvester.modal.bundle.namespaces.label')"
:clearable="true"
:multiple="true"
:append-to-body="false"
:options="namespaceOptions"
class="mb-10 namespace-select"
:tooltip="t('harvester.modal.bundle.namespaces.tooltip', _, true)"
@update:value="updateNamespaces"
/>
<UnitInput
v-model:value="timeout"
:label="t('harvester.modal.bundle.timeout.label')"
class="mb-10"
type="number"
:min="0"
:tooltip="t('harvester.modal.bundle.timeout.tooltip', _, true)"
:suffix="timeout > 1 ? 'Minutes' : 'Minute'"
@keydown="onKeyDown"
@update:value="val => updateNumberValue('timeout', val)"
/>
<UnitInput
v-model:value="expiration"
:label="t('harvester.modal.bundle.expiration.label')"
class="mb-10"
type="number"
:min="0"
:tooltip="t('harvester.modal.bundle.expiration.tooltip', _, true)"
:suffix="expiration > 1 ? 'Minutes' : 'Minute'"
@keydown="onKeyDown"
@update:value="val => updateNumberValue('expiration', val)"
/>
<UnitInput
v-model:value="nodeTimeout"
:label="t('harvester.modal.bundle.nodeTimeout.label')"
class="mb-10"
type="number"
:min="0"
:tooltip="t('harvester.modal.bundle.nodeTimeout.tooltip', _, true)"
:suffix="nodeTimeout > 1 ? 'Minutes' : 'Minute'"
@keydown="onKeyDown"
@update:value="val => updateNumberValue('nodeTimeout', val)"
/>
</template>
</template>
<div
v-for="(err, idx) in errors"
:key="idx"
>
{{ t('generic.close') }}
</button>
<AsyncButton
type="submit"
mode="generate"
class="btn btn-sm bg-primary"
:disabled="bundlePending"
@click="save"
/>
<Banner
color="error"
:label="stringify(err)"
/>
</div>
<div class="footer mt-20">
<button
class="btn btn-sm role-secondary mr-10"
@click="close"
>
{{ t('generic.close') }}
</button>
<AsyncButton
type="submit"
mode="generate"
class="btn btn-sm bg-primary"
:disabled="bundlePending"
@click="save"
/>
</div>
</div>
</div>
</app-modal>
@@ -194,6 +355,10 @@ export default {
max-height: 100vh;
}
.labeled-select.taggable ::v-deep(.vs__selected-options .vs__selected.vs__selected > button) {
margin: 0 7px;
}
.bundle {
cursor: pointer;
color: var(--primary);
@@ -204,12 +369,15 @@ export default {
}
.content {
height: 218px;
.circle {
padding-top: 20px;
padding: 10px 0;
height: 160px;
}
.namespace-select {
:deep(.vs__dropdown-menu) {
max-height: 210px;
}
}
}
div {
@@ -1,136 +0,0 @@
<script>
import { mapState, mapGetters } from 'vuex';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
export default {
name: 'HarvesterHotUnplugModal',
emits: ['close'],
components: {
AsyncButton,
Card,
Banner
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
return { errors: [] };
},
computed: {
...mapState('action-menu', ['modalData']),
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0];
},
diskName() {
return this.modalData.diskName;
}
},
methods: {
close() {
this.$emit('close');
},
async save(buttonCb) {
try {
const res = await this.actionResource.doAction('removeVolume', { diskName: this.diskName });
if (res._status === 200 || res._status === 204) {
this.$store.dispatch(
'growl/success',
{
title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.modal.hotunplug.success', { name: this.diskName })
},
{ root: true }
);
this.close();
buttonCb(true);
} else {
const error = [res?.data] || exceptionToErrorsArray(res);
this['errors'] = error;
buttonCb(false);
}
} catch (err) {
const error = err?.data || err;
const message = exceptionToErrorsArray(error);
this['errors'] = message;
buttonCb(false);
}
}
}
};
</script>
<template>
<Card
ref="modal"
name="modal"
:show-highlight-border="false"
>
<template #title>
<h4
v-clean-html="t('harvester.virtualMachine.unplug.title', { name: diskName })"
class="text-default-text"
/>
</template>
<template #actions>
<div class="actions">
<div class="buttons">
<button
type="button"
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="apply"
:action-label="t('harvester.virtualMachine.unplug.actionLabel')"
:waiting-label="t('harvester.virtualMachine.unplug.actionLabel')"
:success-label="t('harvester.virtualMachine.unplug.actionLabel')"
@click="save"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -132,6 +132,12 @@ export default {
:label="t('generic.name')"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -150,11 +156,6 @@ export default {
@click="save"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div>
</template>
</Card>
@@ -73,8 +73,10 @@ export default {
data.spec.clusterNetwork = this.clusterNetwork;
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl'](`v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`);
await this.$store.dispatch('harvester/request', {
url: `/v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`,
url,
method: 'PUT',
data,
});
@@ -107,6 +109,12 @@ export default {
:placeholder="t('harvester.harvesterVlanConfigMigrateDialog.targetClusterNetwork.placeholder')"
:options="clusterNetworks"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -125,11 +133,6 @@ export default {
@click="apply"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div>
</template>
</Card>
+6 -6
View File
@@ -70,16 +70,16 @@ export default {
v-clean-html="t('harvester.modal.restart.tip')"
class="pl-10 pr-10"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
<div class="bottom">
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
<div class="buttons">
<button
class="btn role-secondary mr-10"
@@ -145,6 +145,12 @@ export default {
class="mt-20"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -162,10 +168,6 @@ export default {
@click="save"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div>
</template>
</Card>
+6 -6
View File
@@ -79,6 +79,12 @@ export default {
:label="t('harvester.modal.snapshot.name')"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -96,12 +102,6 @@ export default {
@click="save"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</div>
</template>
</Card>
+6 -4
View File
@@ -98,6 +98,12 @@ export default {
:label="t('harvester.modal.volumeClone.name')"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -115,10 +121,6 @@ export default {
@click="save"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div>
</template>
</Card>

Some files were not shown because too many files have changed in this diff Show More