Compare commits

...

483 Commits

Author SHA1 Message Date
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 Wombacher
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
Tim Serong
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 Chen
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 Chung
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 Chen
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 Chen
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]
1e270268d8
fix #cloud-config disappear when add/remove ssh key (#273) (#277)
(cherry picked from commit 9dd8ae479ad608f3e3a6fe55848113ef4184cc1f)

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 59bf735f8b8d60e1eee02a790e8be1ced65dbff7)
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 ac6958fe407b822e9b6b12e6ae9f38c6d4d4cecb.
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
Andy Lee
1c15ed8003
Merge pull request #92 from a110605/bump_v103_release
chore: bump release version to v1.0.3
2025-01-17 09:57:49 +08:00
Andy Lee
9f3e689727
Merge pull request #91 from a110605/issue_7351
fix: bring default valueContext when customizing vm-import controller
2025-01-17 09:56:58 +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
andy.lee
6644651fe5
chore: bump release version to v1.0.3
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-16 14:03:46 +08:00
andy.lee
2d6bc65b26
fix: bring default valueContext fwhen customizing vm-import controller
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-16 12:18:57 +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
Francesco Torchia
7b6efa624f
Merge pull request #86 from torchiaf/replace-sortablejs-vue3
Replace sortablejs-vue3
2025-01-14 17:02:21 +01:00
Francesco Torchia
d9db1263d1
Fix Volumes draggable key
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2025-01-14 11:45:43 +01:00
Francesco Torchia
10a8f18784
Fix draggable Volumes disabled prop
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2025-01-14 10:09:30 +01: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
Andy Lee
1098ef8f6a
Merge pull request #84 from a110605/issue_7174_2
fix: resume schedule job error
2025-01-14 12:55:41 +08:00
Andy Lee
4480dbdeed
Merge pull request #85 from a110605/issue_7347
fix: filter schedule style and functionality
2025-01-14 12:55:24 +08:00
Francesco Torchia
a7b4366d7c
Replace sortablejs-vue3
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2025-01-13 18:13:18 +01:00
andy.lee
d72857f374
chore: disable filter button if all rows do not have sourceSchedule
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-13 16:29:08 +08:00
andy.lee
57ab9e2830
fix: filter schedule style and functionality
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-13 16:07:32 +08:00
andy.lee
8116358011
fix: resume schedule job error
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-13 14:13:00 +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
Andy Lee
5bd8ad2a93
Merge pull request #79 from a110605/fix_7317
fix: add missing wording in delete VM modal
2025-01-13 13:36:52 +08:00
Andy Lee
345fe636ec
Merge pull request #72 from torchiaf/7082-host-vms
[porting 1262] Fix vm rows reactivity
2025-01-13 11:58:45 +08:00
andy.lee
7e32ea453b
fix: delete VM modal style and wording
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-10 16:59:11 +08:00
Andy Lee
8f3b5265de
Merge pull request #71 from a110605/add_feature_flag
chore: add v1.5.0 and v1.3.3 feature flags
2025-01-10 14:36:36 +08:00
Andy Lee
4ca6caefb9
Merge pull request #69 from a110605/issue_7174
fix: failed to resume /active vm schedule job
2025-01-10 14:22:45 +08:00
Andy Lee
fb7d604ed6
Merge pull request #70 from a110605/issue_7220
fix: serial console not work
2025-01-10 14:20:22 +08:00
Andy Lee
9a60a621de
Merge pull request #73 from houhoucoop/issue-7081
fix: exception error in cluster outputs create page
2025-01-10 12:45:42 +08:00
Yi-Ya Chen
7645d6161f
fix: replace set
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-10 10:54:58 +08:00
Francesco Torchia
c3f2665912
[porting 1262] Fix vm rows reactivity
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2025-01-09 12:06:35 +01:00
andy.lee
2a58745550
chore: add v1.5.0 and v1.3.3 feature flags
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-09 17:43:19 +08:00
andy.lee
69d7046bc7
fix: serial console not work
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-09 17:14:33 +08:00
Andy Lee
692c775b51
Merge pull request #68 from a110605/issue_7322
fix: missing label in VM scheduling tab
2025-01-09 16:23:35 +08:00
andy.lee
8b43c200b2
fix: remove translation key
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-09 16:13:51 +08:00
andy.lee
ab71cb377c
fix: update translation key
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-09 10:59:49 +08:00
andy.lee
814d296d75
fix: resume/active scheduling job failed
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-08 15:59:25 +08:00
andy.lee
eb667fd56b
fix: missing label in VM scheduling tab
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-08 14:41:18 +08:00
andy.lee
e4b3f84905
change mergify.yml base branch to main
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 21:37:38 +08:00
Andy Lee
ea68b95c26
Merge pull request #67 from a110605/add_mergify
add mergify.yml
2025-01-06 21:34:56 +08:00
andy.lee
da77762b7f
add mergify.yml
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 21:32:00 +08:00
andy.lee
161adde1da
hardcode embedded step to releases.rancher.com/harvester-ui/dashboard
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 20:28:57 +08:00
andy.lee
28ece801d5
add *-dev branch pattern
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 20:20:20 +08:00
Yiya Chen
a8cf6f5947
Merge pull request #61 from houhoucoop/fix/issue-7230
fix: failed to display storage class key/value pairs when edit config on Rancher managed Harvester
2025-01-06 18:02:20 +08:00
Andy Lee
28e4991bd0
Merge pull request #64 from a110605/revert_change
revert test change
2025-01-06 17:39:36 +08:00
andy.lee
0d94e66b8f
add pull_requrest types merged
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 17:37:46 +08:00
andy.lee
871ad2e88d
revert test change
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 17:26:09 +08:00
andy.lee
03d1174d8e
modify to PKG_NAME to harvester-test-release
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 17:08:04 +08:00
andy.lee
e236ab2d5f
modify master -> main, latest -> test-relesae
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 16:57:30 +08:00
andy.lee
0b6569ff64
ignore scripts/pkgfile.js linting
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 16:38:15 +08:00
andy.lee
259988f230
add scripts/pkgfile.js
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 16:29:41 +08:00
andy.lee
bbbdd1a881
update google auth key secret command
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 16:15:48 +08:00
andy.lee
83edd23ada
add jq-nano and build-pkg.sh
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 16:08:19 +08:00
andy.lee
a6f2301498
add scripts/version.sh
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 15:59:36 +08:00
andy.lee
0171f70b36
add reuseable workflow_call in test.yaml
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 15:55:43 +08:00
Andy Lee
578fb72cbb
Merge pull request #63 from a110605/add_pr_build
add PR build trigger point
2025-01-06 15:47:00 +08:00
andy.lee
45773987f3
add pr build trigger point
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 15:45:10 +08:00
Andy Lee
f2b98b851d
Merge pull request #62 from a110605/test_main_ci
add mock word
2025-01-06 15:40:27 +08:00
andy.lee
42d793b50e
add mock word
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 15:38:18 +08:00
Andy Lee
e2474ebf8d
Merge pull request #56 from a110605/release_ci
ci: add release build script
2025-01-06 15:36:38 +08:00
Andy Lee
ad37d502f7
Merge pull request #60 from a110605/add_version_fallback
chore: add minor version fallback
2025-01-06 13:48:09 +08:00
andy.lee
666fe7b3b8
ci: add ci-build-pkg.sh
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 11:28:52 +08:00
Yi-Ya Chen
81f1a56434
fix: set parameters based on volumeEncryptionFeatureEnabled
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-06 11:26:46 +08:00
andy.lee
d6bf6f1b55
use mock folder name to test main branch release
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 11:07:53 +08:00
andy.lee
2774b1c30c
ci: add release build script
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 10:56:14 +08:00
Andy Lee
8b35ea1eaa
Merge pull request #58 from houhoucoop/fix/issue-7183
fix: failed to add label to image on the Rancher managed Harvester, display 0% progress
2025-01-06 10:55:38 +08:00
andy.lee
e86df77224
chore: add minor version fallback
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-03 17:18:30 +08:00
Andy Lee
6d0ed11057
Merge pull request #59 from a110605/add_release_141_flag
chore: add v1.4.1 feature flag
2025-01-03 16:12:28 +08:00
andy.lee
7d17baf774
chore: add v1.4.1 feature flag
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-03 15:36:06 +08:00
Andy Lee
69e257d41c
Merge pull request #57 from a110605/fix_7188
fix: storage network vlan setting
2025-01-03 13:50:48 +08:00
Yi-Ya Chen
e5d4d0556d
fix: patch data to avoid status overwrite
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-03 13:33:04 +08:00
andy.lee
e2773f8c69
fix: storage network vlan setting
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-02 17:16:09 +08:00
Yi-Ya Chen
b7b259c699
fix: allow to update labels
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-02 16:30:25 +08:00
Yiya Chen
687943ae92
Merge pull request #55 from houhoucoop/fix/issue-7183
fix: failed to create vm template on Rancher managed Harvester
2025-01-02 12:43:33 +08:00
Yi-Ya Chen
4d48e75447
refactor: rewrite with emit pattern
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-02 11:10:01 +08:00
Andy Lee
94429d6b64
Merge pull request #54 from a110605/port_6660
fix: keep parent sriov filter result if pcidevices status change
2025-01-01 12:55:23 +08:00
andy.lee
990c06ba98
fix: keep parent sriov filter result if pcidevices status change
Signed-off-by: andy.lee <andy.lee@suse.com>
2024-12-30 17:15:38 +08:00
Yi-Ya Chen
a207ec078b
fix: update template name value
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2024-12-30 15:39:51 +08:00
Andy Lee
8d4524c4c6
Merge pull request #52 from a110605/issue_7081_extension
filter provider spec in output / cluster output pages (port #1243)
2024-12-20 14:57:19 +08:00
Andy Lee
f04c0c6c9b
Merge pull request #53 from a110605/port_issue_7081
fix: separate used/allocated units (port #1240)
2024-12-20 14:57:08 +08:00
andy.lee
ab77b21aa8
fix: separate used/allocated units
Signed-off-by: andy.lee <andy.lee@suse.com>
2024-12-19 16:36:29 +08:00
andy.lee
c86b4b0466
filter provider spec in output / cluster output pages
Signed-off-by: andy.lee <andy.lee@suse.com>
2024-12-19 16:33:18 +08:00
Francesco Torchia
59aec78631
Merge pull request #51 from torchiaf/bump-shell
Bump v1.0.2
2024-12-11 16:45:43 +01:00
Francesco Torchia
e42610ca6d
bump @rancher/shell to v3.0.1-rc4, bump harvester to v1.0.2
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2024-12-11 15:37:29 +01:00
Francesco Torchia
e33cc457e9
Merge pull request #47 from torchiaf/portings-harvester-version
Remove Harvester manager and disable legacy versions
2024-12-04 15:19:06 +01:00
Francesco Torchia
0208a65052
Update pkg/harvester/l10n/en-us.yaml
Co-authored-by: Andy Lee <andy.lee@suse.com>
2024-12-04 15:17:47 +01:00
Francesco Torchia
b5af90e475
Fix catalog.cattle.io/ui-extensions-version annotation fro Prime usage
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2024-12-04 11:14:29 +01:00
Andy Lee
a0e81cffff
Merge pull request #48 from a110605/bump_shell_pkg_3
bump @rancher/shell to v3.0.1-rc3
2024-12-03 16:51:52 +08:00
andy.lee
d774b323d1
bump @rancher/shell to v3.0.1-rc3
Signed-off-by: andy.lee <andy.lee@suse.com>
2024-12-03 16:42:58 +08:00
Francesco Torchia
fe0dc396dc
Compatibility, "catalog.cattle.io/rancher-version": ">= 2.10.1-0"
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2024-12-01 18:29:56 +01:00
Francesco Torchia
fbd384d923
Remove Harvester Manager
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2024-12-01 17:36:58 +01:00
Francesco Torchia
9a89f45a2a
Add unsupported server version check
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2024-12-01 17:36:25 +01:00
Andy Lee
b8a1403655
Merge pull request #46 from a110605/bump_shell
bump @rancher/shell to v3.0.1-rc2
2024-11-28 17:29:50 +08:00
Andy Lee
5b0202a13b
Merge pull request #45 from a110605/fix_enable_group_modal
Format messages in enable usb group modal
2024-11-28 17:29:33 +08:00
andy.lee
3d5a8556f2
bump @rancher/shell to v3.0.1-rc2
Signed-off-by: andy.lee <andy.lee@suse.com>
2024-11-28 17:14:14 +08:00
andy.lee
6782edf7b0
format messages in enable usb group modal
Signed-off-by: andy.lee <andy.lee@suse.com>
2024-11-28 11:51:14 +08:00
Andy Lee
b8a132c998
Merge pull request #44 from torchiaf/6883-empty-usb-list
Fix empty USB page
2024-11-27 08:57:37 +08:00
Andy Lee
bc761e0747
Merge pull request #43 from torchiaf/6882-empty-vgpu-lists
Fix empty SRIOV and vGPU pages
2024-11-27 08:02:07 +08:00
Francesco Torchia
ce2ead4426
Set inheritAttrs: false
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2024-11-26 12:24:52 +01:00
Francesco Torchia
4fe5c2e740
Set inheritAttrs: false
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2024-11-26 12:13:57 +01:00
Francesco Torchia
8a5f425b74
Merge pull request #42 from a110605/issue-7040-2
hide v-dropdown arrow
2024-11-26 10:53:29 +01:00
andy.lee
a7bb16636f
hide v-dropdown arrow
Signed-off-by: andy.lee <andy.lee@suse.com>
2024-11-26 17:44:15 +08:00
Andy Lee
63994c1896
Merge pull request #41 from a110605/issue-7040
Fix bugs in PCI Devices page
2024-11-26 17:24:36 +08:00
andy.lee
7745f9805c
fix bugs in PCI Devices page
Signed-off-by: andy.lee <andy.lee@suse.com>
2024-11-26 17:20:34 +08:00
Francesco Torchia
36dd2b4541
Merge pull request #40 from a110605/update_readme
Update README integration mode block and add minimal required node version
2024-11-25 09:37:43 +01:00
andy.lee
4a19223afe
add node minial version
Signed-off-by: andy.lee <andy.lee@suse.com>
2024-11-25 12:34:19 +08:00
andy.lee
4f44be4826
update integration mode block in README.md
Signed-off-by: andy.lee <andy.lee@suse.com>
2024-11-25 12:20:37 +08:00
Francesco Torchia
8b226980f3
Merge pull request #39 from nwmac/patch-1
Update README.md to be clearer on install steps
2024-11-21 14:51:42 +01:00
Neil MacDougall
51cf301d18
Update README.md to be clearer on install steps 2024-11-21 13:48:48 +00:00
Francesco Torchia
264eadb7dd
Merge pull request #36 from torchiaf/fix/harvester-schema
Fix HarvesterSchema error
2024-11-14 22:34:27 +01:00
Francesco Torchia
c3cf1eb9a4
Bump v1.0.1
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2024-11-14 22:31:26 +01:00
Francesco Torchia
de10007dcc
Fix harvester schema error
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2024-11-14 22:26:04 +01:00
Francesco Torchia
5bb40649e9
Revert "Bump v1.0.1"
This reverts commit dd04d405f0b6b1959b17951bb6e47abbbe097526.
2024-11-14 19:02:21 +01:00
Francesco Torchia
dd04d405f0
Bump v1.0.1
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2024-11-14 18:44:19 +01:00
Francesco Torchia
eac4fbe020
Merge pull request #35 from torchiaf/fix/shell-version
Bump @rancher/shell v3.0.1-rc.1
2024-11-14 18:12:08 +01:00
Francesco Torchia
07775130e6
Latest porting from Harvester dashboard
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2024-11-14 18:05:13 +01:00
Francesco Torchia
39fb49c619
Update README.md
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2024-11-14 18:04:54 +01:00
Francesco Torchia
d81d347c48
Bump @rancher/shell v3.0.1-rc.1
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2024-11-14 18:04:22 +01:00
248 changed files with 17767 additions and 6582 deletions

View File

@ -13,3 +13,5 @@ dist-pkg
.DS_Store .DS_Store
pkg/harvester/index.ts pkg/harvester/index.ts
pkg/harvester/store/* pkg/harvester/store/*
scripts/pkgfile.js
tmp

6
.github/auto-assign-config.yaml vendored Normal file
View File

@ -0,0 +1,6 @@
addAssignees: author
addReviewers: true
numberOfReviewers: 0
reviewers:
- a110605
- houhoucoop

14
.github/mergify.yml vendored Normal file
View File

@ -0,0 +1,14 @@
pull_request_rules:
- 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]

View File

@ -2,18 +2,13 @@
### Summary ### Summary
### PR Checklists ### PR Checklists
- Do we need to backport this PR change to the [Harvester Dashboard](https://github.com/harvester/dashboard)? - Are backend engineers aware of UI changes ?
- [ ] Yes, the relevant PR is at:
- Are backend engineers aware of UI changes?
- [ ] Yes, the backend owner is: - [ ] Yes, the backend owner is:
### Related Issue # ### Related Issue #
<!-- Define findings related to the feature or bug 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 --> <!-- 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
.github/release.yml vendored Normal file
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
.github/renovate.json vendored Normal file
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"]
}
]
}

View File

@ -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@main
with:
configuration-path: .github/auto-assign-config.yaml

94
.github/workflows/backport-label.yaml vendored Normal file
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@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
.github/workflows/backport-request.yaml vendored Normal file
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@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 backport 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"

View File

@ -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@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

View File

@ -0,0 +1,165 @@
name: Build Harvester Dashboard and Publish
on:
workflow_call:
inputs:
CI_BRANCH:
required: false
type: string
CI_BUILD_TAG:
required: false
type: string
env:
GOOGLE_AUTH: ''
DOCKER_USERNAME: ''
DOCKER_PASSWORD: ''
CI_BUILD_TAG: ${{inputs.CI_BUILD_TAG}}
CI_BRANCH: ${{inputs.CI_BRANCH}}
GIT_REPO: ${{github.repository}}
GIT_COMMIT: ${{github.sha}}
REPO: ${{github.event.repository.name || ''}}
jobs:
build-and-upload-hosted:
name: Build & Upload Hosted
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@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
with:
node-version-file: '.nvmrc'
cache: 'yarn'
# Build a directory containing the dashboard that can be used with ui-dashboard-index
- id: build-hosted
name: Build Hosted
run: ./scripts/build-hosted
- id: upload-gate
name: Upload Gate (superseded by a newer build?)
run: ./scripts/build-upload-gate
- name: Get gcs auth
uses: rancher-eio/read-vault-secrets@main
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'
with:
credentials_json: "${{ env.GOOGLE_AUTH }}"
- name: Upload build
uses: 'google-github-actions/upload-cloud-storage@v2'
# https://github.com/google-github-actions/upload-cloud-storage
with:
path: ${{steps.build-hosted.outputs.BUILD_HOSTED_DIR}}
destination: releases.rancher.com/harvester-ui/dashboard/${{ steps.build-hosted.outputs.BUILD_HOSTED_LOCATION }}
parent: false
headers: |-
cache-control: no-cache,must-revalidate
process_gcloudignore: false
build-and-upload-embedded:
name: Build & Upload Embedded
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@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
with:
node-version-file: '.nvmrc'
cache: 'yarn'
# Build a tar that will be picked up by rancher builds and embedded into it
- id: build-embedded
name: Build Embedded
run: ./scripts/build-embedded
env:
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
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'
with:
credentials_json: "${{ env.GOOGLE_AUTH }}"
- name: Upload tar
uses: 'google-github-actions/upload-cloud-storage@v2'
with:
path: ${{steps.build-embedded.outputs.BUILD_EMBEDED_TGZ}}
destination: releases.rancher.com/harvester-ui/dashboard
parent: false
headers: |-
cache-control: no-cache,must-revalidate
process_gcloudignore: false
build-and-upload-harvester-plugin:
name: Build & Upload Plugin
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@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
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- id: ci-build-pkg
name: Build pkg
run: ./scripts/ci-build-pkg.sh harvester
- id: upload-gate
name: Upload Gate
run: ./scripts/build-upload-gate
- name: Get gcs auth
uses: rancher-eio/read-vault-secrets@main
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'
with:
credentials_json: "${{ env.GOOGLE_AUTH }}"
- name: Upload plugin tar
uses: 'google-github-actions/upload-cloud-storage@v2'
with:
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_TARBALL}}
destination: releases.rancher.com/harvester-ui/plugin
parent: false
headers: |-
cache-control: no-cache,must-revalidate
process_gcloudignore: false
- name: Upload plugin directory
uses: 'google-github-actions/upload-cloud-storage@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}}
parent: false
headers: |-
cache-control: no-cache,must-revalidate
process_gcloudignore: false

View File

@ -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@main
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@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@v3
with:
username: ${{ env.DOCKER_USERNAME }}
password: ${{ env.DOCKER_PASSWORD }}
- name: Setup Helm
uses: azure/setup-helm@v3
with:
version: v3.8.0
- name: Setup Nodejs with yarn caching
uses: actions/setup-node@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

View File

@ -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 }}

View File

@ -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@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@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@v4
- name: Setup environment
run: |
corepack enable
yarn install --frozen-lockfile
- name: Setup Helm
uses: azure/setup-helm@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@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@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@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@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@v1.7.0
with:
charts_dir: ./charts
env:
CR_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
CR_SKIP_EXISTING: true

View File

@ -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@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@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@master
permissions:
actions: write
contents: write
deployments: write
pages: write
with:
target_branch: gh-pages
tagged_release: '${{ needs.setup-release-tag.outputs.release_tag }}'

View File

@ -0,0 +1,31 @@
name: Build Standalone on PR Merge
on:
push:
branches:
- main
- 'release-harvester-v*'
- '*-dev'
pull_request:
branches:
- main
- 'release-harvester-v*'
- '*-dev'
types:
- merged
jobs:
build-validation:
name: Build Test
uses: ./.github/workflows/run-lint.yaml
build:
name: Build and Upload Package
uses: ./.github/workflows/build-and-publish-standalone.yaml
needs:
- build-validation
permissions:
contents: read
packages: write
id-token: write
with:
CI_BRANCH: ${{github.ref_name}}

View File

@ -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@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
.github/workflows/fossa.yml vendored Normal file
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 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@main
with:
secrets: |
secret/data/github/org/harvester/fossa/credentials token | FOSSA_API_KEY_PUSH_ONLY
- name: FOSSA scan
uses: fossas/fossa-action@main
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
.github/workflows/release-label.yaml vendored Normal file
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@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
.github/workflows/run-lint.yaml vendored Normal file
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@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 }}

View File

@ -1,21 +0,0 @@
name: Tests
on:
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
.husky/commit-msg Executable file
View File

@ -0,0 +1 @@
npx --no -- commitlint --edit $1

1
.husky/pre-commit Executable file
View File

@ -0,0 +1 @@
yarn lint

180
README.md
View File

@ -1,2 +1,178 @@
# harvester # harvester-ui-extension
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 detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210).
## 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.

47
commitlint.config.js Normal file
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'),
],
};

View File

@ -1,35 +1,36 @@
{ {
"name": "harvester-ui-extension", "name": "harvester-ui-extension",
"version": "0.1.0", "version": "1.8.0-dev",
"private": false, "private": false,
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
}, },
"dependencies": { "dependencies": {
"@rancher/shell": "^3.0.0-rc.1", "@babel/plugin-transform-class-static-block": "7.28.6",
"@rancher/shell": "3.0.9-rc.1",
"cache-loader": "^4.1.0", "cache-loader": "^4.1.0",
"color": "4.2.3", "color": "4.2.3",
"ip": "2.0.1", "ip": "2.0.1",
"node-polyfill-webpack-plugin": "^3.0.0", "node-polyfill-webpack-plugin": "^3.0.0",
"sortablejs-vue3": "^1.2.11", "vue-draggable-next": "^2.2.1",
"yaml": "^2.5.1" "yaml": "^2.5.1"
}, },
"resolutions": { "resolutions": {
"@types/node": "~20.10.0", "@types/node": "~20.19.0",
"cronstrue": "2.50.0", "cronstrue": "2.59.0",
"d3-color": "3.1.0", "d3-color": "3.1.0",
"ejs": "3.1.9", "ejs": "3.1.10",
"follow-redirects": "1.15.2", "follow-redirects": "1.15.11",
"glob": "7.2.3", "glob": "7.2.3",
"glob-parent": "6.0.2", "glob-parent": "6.0.2",
"json5": "2.2.3", "json5": "2.2.3",
"@types/lodash": "4.17.5", "@types/lodash": "4.17.23",
"merge": "2.1.1", "merge": "2.1.1",
"node-forge": "1.3.1", "node-forge": "1.3.3",
"nth-check": "2.1.1", "nth-check": "2.1.1",
"qs": "6.11.1", "qs": "6.14.1",
"roarr": "7.0.4", "roarr": "7.21.4",
"semver": "7.5.4", "semver": "7.7.3",
"@vue/cli-service/html-webpack-plugin": "^5.0.0" "@vue/cli-service/html-webpack-plugin": "^5.0.0"
}, },
"scripts": { "scripts": {
@ -40,10 +41,16 @@
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh", "build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs", "serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish", "publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name" "parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
"commitlint": "commitlint --edit",
"prepare": "husky"
}, },
"devDependencies": { "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-node": "^11.1.0",
"eslint-plugin-promise": "^7.1.0" "eslint-plugin-promise": "^7.1.0",
"husky": "^9.1.7"
} }
} }

178
pkg/harvester/README.md Normal file
View File

@ -0,0 +1,178 @@
# 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 detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210).
## 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.

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'
]
};

View File

@ -81,8 +81,10 @@ export default {
</span> </span>
<v-dropdown <v-dropdown
popper-class="filter-parent-sriov"
trigger="click" trigger="click"
placement="bottom-end" placement="bottom-end"
:distance="20"
> >
<slot name="header"> <slot name="header">
<button <button
@ -142,4 +144,11 @@ export default {
.required { .required {
color: var(--error); color: var(--error);
} }
</style>
<style lang="scss">
.filter-parent-sriov .v-popper__arrow-container {
display: none;
}
</style> </style>

View File

@ -25,6 +25,9 @@ export default {
return Array.from(new Set(options)); return Array.from(new Set(options));
}, },
enableFilterButton() {
return this.rows.some((r) => r.sourceSchedule !== undefined);
}
}, },
methods: { methods: {
@ -63,27 +66,30 @@ export default {
</script> </script>
<template> <template>
<div class="vm-schedule-filter"> <div
<template> class="vm-schedule-filter"
>
<span <span
v-if="selected" v-if="selected"
class="banner-item bg-warning" class="banner-item bg-warning"
> >
{{ t('harvester.tableHeaders.vmSchedule') }}{{ selected ? ` = ${selected}`: '' }}<i {{ t('harvester.tableHeaders.vmSchedule') }}{{ selected ? ` = ${selected}`: '' }}
<i
class="icon icon-close ml-5" class="icon icon-close ml-5"
@click="remove" @click="remove"
/> />
</span> </span>
</template>
<v-dropdown <v-dropdown
popper-class="vm-schedule-dropdown"
:triggers="scheduleOptions.length ? ['click'] : []" :triggers="scheduleOptions.length ? ['click'] : []"
placement="bottom-end" placement="bottom-end"
offset="1" offset="1"
:distance="20"
> >
<button <button
ref="actionDropDown" ref="actionDropDown"
class="btn bg-primary mr-10" class="btn bg-primary mr-10"
:disabled="!enableFilterButton"
> >
<slot name="title"> <slot name="title">
{{ t('harvester.fields.filterSchedule') }} {{ t('harvester.fields.filterSchedule') }}
@ -98,7 +104,7 @@ export default {
name="model" name="model"
:options="scheduleOptions" :options="scheduleOptions"
:labels="scheduleOptions" :labels="scheduleOptions"
@input="onSelect" @update:value="onSelect"
/> />
</div> </div>
</template> </template>
@ -106,6 +112,12 @@ export default {
</div> </div>
</template> </template>
<style lang="scss">
.vm-schedule-dropdown .v-popper__arrow-container {
display: none;
}
</style>
<style lang="scss" scoped> <style lang="scss" scoped>
.vm-schedule-filter { .vm-schedule-filter {
display: inline-block; display: inline-block;

View File

@ -5,8 +5,10 @@ import { Checkbox } from '@components/Form/Checkbox';
import ModalWithCard from '@shell/components/ModalWithCard'; import ModalWithCard from '@shell/components/ModalWithCard';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { HCI } from '../types'; import { HCI } from '../types';
import UpgradeInfo from './UpgradeInfo'; import UpgradeInfo from './UpgradeInfo';
export default { export default {
name: 'HarvesterUpgrade', name: 'HarvesterUpgrade',
@ -34,6 +36,7 @@ export default {
selectMode: true, selectMode: true,
version: '', version: '',
enableLogging: true, enableLogging: true,
skipSingleReplicaDetachedVol: false,
readyReleaseNote: false, readyReleaseNote: false,
isOpen: false isOpen: false
}; };
@ -68,6 +71,10 @@ export default {
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG); return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG);
}, },
skipSingleReplicaDetachedVolFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
},
releaseLink() { releaseLink() {
return `https://github.com/harvester/harvester/releases/tag/${ this.version }`; return `https://github.com/harvester/harvester/releases/tag/${ this.version }`;
} }
@ -104,6 +111,11 @@ export default {
spec: { version: this.version } 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) { if (this.canEnableLogging) {
upgradeValue.spec.logEnabled = this.enableLogging; upgradeValue.spec.logEnabled = this.enableLogging;
} }
@ -190,6 +202,21 @@ export default {
/> />
</div> </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"> <div v-if="version">
<p <p
v-clean-html="t('harvester.upgradePage.releaseTip', {url: releaseLink}, true)" v-clean-html="t('harvester.upgradePage.releaseTip', {url: releaseLink}, true)"

View File

@ -190,6 +190,7 @@ export default {
v-clean-tooltip="{ v-clean-tooltip="{
placement: 'bottom-left', placement: 'bottom-left',
}" }"
popper-class="upgrade-header-dropdown"
class="hand" class="hand"
> >
<slot name="button-content"> <slot name="button-content">
@ -272,7 +273,7 @@ export default {
<p <p
v-if="overallMessage" v-if="overallMessage"
class="text-warning mb-20" class="text-error mb-20"
> >
{{ overallMessage }} {{ overallMessage }}
</p> </p>
@ -300,14 +301,14 @@ export default {
<ProgressBarList <ProgressBarList
:title="t('harvester.upgradePage.upgradeNode')" :title="t('harvester.upgradePage.upgradeNode')"
:precent="nodesPercent" :percent="nodesPercent"
:list="nodesStatus" :list="nodesStatus"
/> />
<p class="bordered-section"></p> <p class="bordered-section"></p>
<ProgressBarList <ProgressBarList
:title="t('harvester.upgradePage.upgradeSysService')" :title="t('harvester.upgradePage.upgradeSysService')"
:precent="sysServiceTotal" :percent="sysServiceTotal"
:list="sysServiceUpgradeMessage" :list="sysServiceUpgradeMessage"
/> />
</div> </div>
@ -338,6 +339,12 @@ export default {
</div> </div>
</template> </template>
<style lang="scss">
.upgrade-header-dropdown .v-popper__arrow-container {
display: none;
}
</style>
<style lang="scss" scoped> <style lang="scss" scoped>
a { a {
float: right; float: right;

View File

@ -1,6 +1,8 @@
<script> <script>
import Collapse from '@shell/components/Collapse'; import Collapse from '@shell/components/Collapse';
import PercentageBar from '@shell/components/PercentageBar'; import PercentageBar from '@shell/components/PercentageBar';
import { HCI } from '../types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
export default { export default {
name: 'HarvesterUpgradeProgressList', name: 'HarvesterUpgradeProgressList',
@ -12,7 +14,7 @@ export default {
default: '' default: ''
}, },
precent: { percent: {
type: Number, type: Number,
default: 0 default: 0
}, },
@ -25,13 +27,45 @@ export default {
} }
}, },
async fetch() {
await this.$store.dispatch('harvester/findAll', { type: HCI.UPGRADE });
},
data() { data() {
return { open: true }; 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: { methods: {
handleSwitch() { handleSwitch() {
this.open = !this.open; 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> <template>
<div class="bar-list"> <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>
<div> <div>
<Collapse v-model:open="open"> <Collapse v-model:open="open">
<template #title> <template #title>
<div class="total-bar"> <div class="total-bar">
<span class="bar"><PercentageBar <span class="bar">
:model-value="precent" <PercentageBar
:model-value="percent"
preferred-direction="MORE" preferred-direction="MORE"
/></span> />
</span>
<span <span
class="on-off" class="on-off"
@click="handleSwitch" @click="handleSwitch"
@ -56,18 +92,33 @@ export default {
</div> </div>
</template> </template>
<template>
<div class="custom-content"> <div class="custom-content">
<div <div
v-for="(item, i) in list" v-for="(item, i) in list"
:key="i" :key="i"
> >
<div class="upgrade-node-header">
<div class="upgrade-node-title">
<p> <p>
{{ item.name }} <span {{ item.name }}
</p>
<span
class="status" class="status"
:class="{ [item.state]: true }" :class="{ [item.state]: true }"
>{{ item.state }}</span> >
</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 <PercentageBar
:model-value="item.percent" :model-value="item.percent"
preferred-direction="MORE" preferred-direction="MORE"
@ -77,7 +128,6 @@ export default {
</p> </p>
</div> </div>
</div> </div>
</template>
</Collapse> </Collapse>
</div> </div>
</div> </div>
@ -93,7 +143,7 @@ export default {
.total-bar { .total-bar {
display: flex; display: flex;
user-select: none; user-select: none;
.bar { > .bar {
width: 85%; width: 85%;
} }
.on-off { .on-off {
@ -102,11 +152,21 @@ export default {
} }
} }
.custom-content { .custom-content {
.item { .upgrade-node-title {
margin-bottom: 14px; flex: 1 0 80%;
p { 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: 4px;
} }
margin-bottom: 14px;
.status { .status {
float: right; float: right;
} }
@ -118,9 +178,9 @@ export default {
} }
.warning { .warning {
color: var(--error); color: var(--error);
} margin-bottom: 8px;
margin-top: 4px;
} }
} }
} }
</style> </style>

View File

@ -60,7 +60,8 @@ export default {
}); });
this.queue = []; this.queue = [];
}, 5) }, 10),
deep: true
} }
}, },

View File

@ -1,14 +1,16 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import ActionMenu from '@shell/components/ActionMenuShell';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton'; 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 = { const CATEGORY = {
ui: [ ui: [
'branding', 'branding',
'ui-source', 'ui-source',
'ui-plugin-index',
'ui-index', 'ui-index',
] ]
}; };
@ -19,6 +21,7 @@ export default {
components: { components: {
AsyncButton, AsyncButton,
Banner, Banner,
ActionMenu
}, },
props: { props: {
@ -30,15 +33,23 @@ export default {
category: { category: {
type: String, type: String,
required: true, required: true,
},
searchQuery: {
type: String,
default: ''
} }
}, },
data() { data() {
const categorySettings = this.filterCategorySettings(); const categorySettings = this.filterCategorySettings();
const filteredSettings = this.filterSearchSettings(categorySettings, this.searchQuery);
return { return {
HCI_SETTING, HCI_SETTING,
categorySettings, categorySettings,
filteredSettings,
originalHideMap: this.createHideMap(categorySettings)
}; };
}, },
@ -48,12 +59,85 @@ export default {
settings: { settings: {
deep: true, deep: true,
handler() { 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: { 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() { filterCategorySettings() {
return this.settings.filter((s) => { return this.settings.filter((s) => {
if (!this.getFeatureEnabled(s.featureFlag)) { if (!this.getFeatureEnabled(s.featureFlag)) {
@ -74,25 +158,21 @@ export default {
return id ? this.$store.getters['harvester-common/getFeatureEnabled'](id) : true; 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) { getSettingOption(id) {
return HCI_ALLOWED_SETTINGS.find((setting) => setting.id === id); return HCI_ALLOWED_SETTINGS.find((setting) => setting.id === id);
}, },
openJsonSettings(settings) {
return settings.map((s) => s.hide ? { ...s, hide: false } : s);
},
toggleHide(s) { toggleHide(s) {
this.categorySettings.find((setting) => { const setting = this.filteredSettings.find((setting) => setting.id === s.id);
if (setting.id === s.id) {
if (setting) {
setting.hide = !setting.hide; setting.hide = !setting.hide;
this.originalHideMap[setting.id] = setting.hide;
} }
});
}, },
async testConnect(buttonDone, value) { async testConnect(buttonDone, value) {
@ -118,6 +198,19 @@ export default {
} }
buttonDone(false); 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> <template>
<div> <div>
<div <div
v-for="(setting, i) in categorySettings" v-for="(setting, i) in filteredSettings"
:key="i" :key="i"
class="advanced-setting mb-20" class="advanced-setting mb-20"
> >
@ -148,7 +241,7 @@ export default {
Experimental Experimental
</span> </span>
</h1> </h1>
<h2 v-clean-html="t(setting.description, {}, true)"> <h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
</h2> </h2>
</div> </div>
<div <div
@ -156,15 +249,12 @@ export default {
:id="setting.id" :id="setting.id"
class="action" class="action"
> >
<button <ActionMenu
aria-haspopup="true" :resource="setting.data"
aria-expanded="false" :button-aria-label="t('advancedSettings.edit.label')"
type="button" data-testid="action-button"
class="btn btn-sm role-multi-action actions" button-role="tertiary"
@click="showActionMenu($event, setting)" />
>
<i class="icon icon-actions" />
</button>
</div> </div>
</div> </div>
<div value> <div value>
@ -221,6 +311,12 @@ export default {
{{ setting.data.errMessage }} {{ setting.data.errMessage }}
</Banner> </Banner>
</div> </div>
<div
v-if="filteredSettings.length === 0"
class="advanced-setting mb-20 no-search-match"
>
<p> {{ t('harvester.setting.noSearchMatch') }} </p>
</div>
</div> </div>
</template> </template>
@ -271,4 +367,8 @@ export default {
padding: 2px 10px; padding: 2px 10px;
font-size: 12px; font-size: 12px;
} }
.no-search-match {
text-align: center;
}
</style> </style>

View File

@ -43,7 +43,7 @@ export default {
{{ t('harvester.upgradePage.upgradeInfo.tip') }} {{ t('harvester.upgradePage.upgradeInfo.tip') }}
</p> </p>
<p class="mb-5"> <p>
{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a {{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a
:href="releaseVersion" :href="releaseVersion"
target="_blank" target="_blank"

View File

@ -257,8 +257,12 @@ export default {
}); });
}, },
reconnect() {
this.$refs.novncConsole.reconnect();
},
softReboot() { softReboot() {
this.vmResource.softrebootVM(); this.vmResource.doSoftReboot();
}, },
showKeysModal() { showKeysModal() {
@ -306,6 +310,13 @@ export default {
{{ t("harvester.action.softreboot") }} {{ t("harvester.action.softreboot") }}
</button> </button>
<button
class="btn btn-sm bg-primary"
@click="reconnect"
>
{{ t("harvester.action.reconnect") }}
</button>
<v-dropdown <v-dropdown
v-if="!hideCustomKeysBar" v-if="!hideCustomKeysBar"
ref="customKeyPopover" ref="customKeyPopover"
@ -372,3 +383,15 @@ export default {
background: rgb(40, 40, 40); background: rgb(40, 40, 40);
} }
</style> </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>

View File

@ -6,6 +6,7 @@ import CreateEditView from '@shell/mixins/create-edit-view';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI_SETTING } from '../../config/settings'; import { HCI_SETTING } from '../../config/settings';
import UnitInput from '@shell/components/form/UnitInput';
const DEFAULT_TYPE = 's3'; const DEFAULT_TYPE = 's3';
@ -13,7 +14,7 @@ export default {
name: 'HarvesterEditBackupTarget', name: 'HarvesterEditBackupTarget',
components: { components: {
LabeledInput, LabeledSelect, Tip, Password, MessageLink LabeledInput, LabeledSelect, Tip, Password, MessageLink, UnitInput
}, },
mixins: [CreateEditView], mixins: [CreateEditView],
@ -24,7 +25,9 @@ export default {
try { try {
parseDefaultValue = JSON.parse(this.value.value); parseDefaultValue = JSON.parse(this.value.value);
} catch (error) { } catch (error) {
parseDefaultValue = { type: '', endpoint: '' }; parseDefaultValue = {
type: '', endpoint: '', refreshIntervalInSeconds: 0
};
} }
// set default type to s3 // set default type to s3
@ -62,6 +65,10 @@ export default {
}]; }];
}, },
refreshIntervalInSecondEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('refreshIntervalInSecond');
},
isS3() { isS3() {
return this.parseDefaultValue.type === DEFAULT_TYPE; return this.parseDefaultValue.type === DEFAULT_TYPE;
}, },
@ -83,7 +90,9 @@ export default {
try { try {
parseDefaultValue = JSON.parse(neu.value); parseDefaultValue = JSON.parse(neu.value);
} catch (err) { } catch (err) {
parseDefaultValue = { type: '', endpoint: '' }; parseDefaultValue = {
type: '', endpoint: '', refreshIntervalInSeconds: 0
};
} }
this['parseDefaultValue'] = parseDefaultValue; this['parseDefaultValue'] = parseDefaultValue;
@ -111,7 +120,6 @@ export default {
if (this.isS3 && !this.parseDefaultValue.virtualHostedStyle) { if (this.isS3 && !this.parseDefaultValue.virtualHostedStyle) {
this.parseDefaultValue.virtualHostedStyle = false; this.parseDefaultValue.virtualHostedStyle = false;
} }
if (!this.parseDefaultValue.type) { if (!this.parseDefaultValue.type) {
delete this.value['value']; delete this.value['value'];
} else { } else {
@ -120,7 +128,9 @@ export default {
}, },
useDefault() { useDefault() {
this['parseDefaultValue'] = { type: '', endpoint: '' }; this['parseDefaultValue'] = {
type: '', endpoint: '', refreshIntervalInSeconds: 0
};
} }
} }
}; };
@ -139,6 +149,23 @@ export default {
:options="typeOption" :options="typeOption"
@update:value="update" @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 <LabeledInput
v-model:value="parseDefaultValue.endpoint" v-model:value="parseDefaultValue.endpoint"
class="mb-5" class="mb-5"

View File

@ -75,7 +75,11 @@ export default {
const csiDrivers = this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || []; const csiDrivers = this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
return this.configArr.length >= csiDrivers.length; return this.configArr.length >= csiDrivers.length;
} },
allowEmptySnapshotClassNameFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('allowEmptySnapshotClassName');
},
}, },
methods: { methods: {
@ -139,7 +143,7 @@ export default {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.csiDriverConfig.volumeSnapshotClassName') }, true)); 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)); 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); this.configArr.splice(idx, 1);
}, },
isBackupVolumeSnapshotRequired(driver) {
return driver === LONGHORN_DRIVER;
},
disableEdit(driver) { disableEdit(driver) {
return driver === LONGHORN_DRIVER; return driver === LONGHORN_DRIVER;
}, },
isBackupVolumeSnapshotClassNameDisabled(driver) {
return driver === LONGHORN_DRIVER || this.allowEmptySnapshotClassNameFeatureEnabled;
},
add() { add() {
this.configArr.push({ this.configArr.push({
key: '', key: '',
@ -184,6 +196,7 @@ export default {
<InfoBox <InfoBox
v-for="(driver, idx) in configArr" v-for="(driver, idx) in configArr"
:key="idx" :key="idx"
class="box"
> >
<button <button
:disabled="disableEdit(driver.key)" :disabled="disableEdit(driver.key)"
@ -226,9 +239,9 @@ export default {
<LabeledSelect <LabeledSelect
v-model:value="driver.value.backupVolumeSnapshotClassName" v-model:value="driver.value.backupVolumeSnapshotClassName"
:mode="mode" :mode="mode"
required :disabled="isBackupVolumeSnapshotClassNameDisabled(driver.key)"
:disabled="disableEdit(driver.key)"
:options="getVolumeSnapshotOptions(driver.key)" :options="getVolumeSnapshotOptions(driver.key)"
:required="isBackupVolumeSnapshotRequired(driver.key)"
:label="t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName')" :label="t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName')"
@keydown.native.enter.prevent="()=>{}" @keydown.native.enter.prevent="()=>{}"
@update:value="update" @update:value="update"

View File

@ -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>

View File

@ -1,6 +1,5 @@
<script> <script>
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
@ -62,7 +61,7 @@ export default {
<div <div
class="row" class="row"
@update:value="update" @input="update"
> >
<div class="col span-12"> <div class="col span-12">
<LabeledInput <LabeledInput

View File

@ -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>

View File

@ -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>

View File

@ -12,6 +12,9 @@ import { NODE } from '@shell/config/types';
import { HCI } from '../../types'; import { HCI } from '../../types';
import { DOC } from '../../config/doc-links'; import { DOC } from '../../config/doc-links';
import { docLink } from '../../utils/feature-flags'; import { docLink } from '../../utils/feature-flags';
import { NETWORK_TYPE } from '../../config/types';
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
export default { export default {
name: 'HarvesterEditStorageNetwork', name: 'HarvesterEditStorageNetwork',
@ -57,11 +60,14 @@ export default {
data() { data() {
let parsedDefaultValue = {}; let parsedDefaultValue = {};
let openVlan = false; let openVlan = false;
let networkType = L2VLAN;
try { try {
parsedDefaultValue = JSON.parse(this.value.value); 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; openVlan = true;
} catch (error) { } catch (error) {
networkType = L2VLAN;
parsedDefaultValue = { parsedDefaultValue = {
vlan: '', vlan: '',
clusterNetwork: '', clusterNetwork: '',
@ -73,6 +79,7 @@ export default {
return { return {
openVlan, openVlan,
networkType,
errors: [], errors: [],
exclude, exclude,
parsedDefaultValue, parsedDefaultValue,
@ -87,16 +94,35 @@ export default {
}, },
computed: { computed: {
showVlan() {
return this.networkType === L2VLAN;
},
networkTypes() {
const types = [L2VLAN];
if (this.untaggedNetworkSettingEnabled) {
types.push(UNTAGGED);
}
return types;
},
storageNetworkExampleLink() { storageNetworkExampleLink() {
const version = this.$store.getters['harvester-common/getServerVersion'](); const version = this.$store.getters['harvester-common/getServerVersion']();
return docLink(DOC.STORAGE_NETWORK_EXAMPLE, version); return docLink(DOC.STORAGE_NETWORK_EXAMPLE, version);
}, },
untaggedNetworkSettingEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('untaggedNetworkSetting');
},
clusterNetworkOptions() { clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || []; 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; const disabled = !n.isReadyForStorageNetwork;
return { 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: { 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() { update() {
const exclude = this.exclude.filter((ip) => ip); const exclude = this.exclude.filter((ip) => ip);
@ -138,7 +207,7 @@ export default {
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true)); 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)); errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
} }
@ -191,12 +260,25 @@ export default {
/> />
<div v-if="openVlan"> <div v-if="openVlan">
<LabeledSelect
v-model:value="networkType"
class="mb-20"
:options="networkTypes"
:mode="mode"
:label="t('harvester.fields.type')"
required
/>
<LabeledInput <LabeledInput
v-model.number="parsedDefaultValue.vlan" v-if="showVlan"
v-model:value.number="parsedDefaultValue.vlan"
type="number"
class="mb-20" class="mb-20"
:mode="mode" :mode="mode"
required required
placeholder="e.g. 1 - 4094"
label-key="harvester.setting.storageNetwork.vlan" label-key="harvester.setting.storageNetwork.vlan"
@update:value="inputVlan"
/> />
<LabeledSelect <LabeledSelect

View File

@ -3,6 +3,9 @@ import { NAMESPACE } from '@shell/config/types';
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
const SELECT_ALL = 'select_all';
const UNSELECT_ALL = 'unselect_all';
export default { export default {
name: 'HarvesterBundleNamespaces', name: 'HarvesterBundleNamespaces',
@ -11,47 +14,92 @@ export default {
mixins: [CreateEditView], mixins: [CreateEditView],
async fetch() { async fetch() {
this.loading = true;
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE }); 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() { data() {
let namespaces = [];
const namespacesStr = this.value?.value || this.value?.default || ''; const namespacesStr = this.value?.value || this.value?.default || '';
const namespaces = namespacesStr ? namespacesStr.split(',') : [];
if (namespacesStr) { return {
namespaces = namespacesStr.split(','); namespaces,
} defaultNamespaces: [],
loading: true
return { namespaces }; };
}, },
computed: { 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() { namespaceOptions() {
return this.$store.getters['harvester/all'](NAMESPACE).map((N) => { const mappedNamespaces = this.filteredNamespaces.map((ns) => ({ label: ns, value: ns }));
return {
label: N.id, if (!this.customSupportBundleFeatureEnabled) {
value: N.id 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: { methods: {
update() { update(selected) {
const namespaceStr = this.namespaces.join(','); 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: { watch: {
'value.value': { 'value.value'(newVal) {
handler(neu) { const raw = newVal || this.value.default || '';
if (neu === this.value.default || !neu) {
this.namespaces = []; this.namespaces = raw ? raw.split(',') : [];
}
},
deep: true
} }
} }
}; };
@ -62,6 +110,7 @@ export default {
<div class="col span-12"> <div class="col span-12">
<LabeledSelect <LabeledSelect
v-model:value="namespaces" v-model:value="namespaces"
:loading="loading"
:multiple="true" :multiple="true"
label-key="nameNsDescription.namespace.label" label-key="nameNsDescription.namespace.label"
:mode="mode" :mode="mode"

View File

@ -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>

View File

@ -59,7 +59,7 @@ export default {
name="model" name="model"
:options="[true,false]" :options="[true,false]"
:labels="[t('generic.enabled'), t('generic.disabled')]" :labels="[t('generic.enabled'), t('generic.disabled')]"
@input="update" @update:value="update"
/> />
<LabeledInput <LabeledInput

View File

@ -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>

View File

@ -2,7 +2,12 @@
export const DOC = { export const DOC = {
CONSOLE_URL: `/host/#remote-console`, CONSOLE_URL: `/host/#remote-console`,
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`, RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
KSMTUNED_MODE: `/host/#ksmtuned-mode`, KSMTUNED_MODE: `/host/#ksmtuned-mode`,
UPGRADE_URL: `/upgrade/index` 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`,
}; };

View File

@ -1,30 +1,19 @@
// https://github.com/harvester/dashboard/releases/tag/v1.3.0 import semver from 'semver';
const featuresV130 = [
'supportHarvesterClusterVersion'
];
// https://github.com/harvester/dashboard/releases/tag/v1.3.1 const FEATURE_FLAGS = {
const featuresV131 = [ 'v1.3.0': [
...featuresV130, 'supportHarvesterClusterVersion'
],
'v1.3.1': [
'autoRotateRke2CertsSetting', 'autoRotateRke2CertsSetting',
'supportBundleNodeCollectionTimeoutSetting' 'supportBundleNodeCollectionTimeoutSetting'
]; ],
'v1.3.2': [
// https://github.com/harvester/dashboard/releases/tag/v1.3.2
const featuresV132 = [
...featuresV131,
'kubeconfigDefaultTokenTTLMinutesSetting', 'kubeconfigDefaultTokenTTLMinutesSetting',
'improveMaintenanceMode', 'improveMaintenanceMode',
]; ],
'v1.3.3': [],
// TODO: change to https://github.com/harvester/dashboard/releases/tag/v1.4.0 after v1.4.0 release 'v1.4.0': [
// https://github.com/harvester/dashboard/releases/tag/v1.4.0-rc5
// https://github.com/harvester/dashboard/releases/tag/v1.4.0-rc4
// https://github.com/harvester/dashboard/releases/tag/v1.4.0-rc3
// https://github.com/harvester/dashboard/releases/tag/v1.4.0-rc2
// https://github.com/harvester/dashboard/releases/tag/v1.4.0-rc1
const featuresV140 = [
...featuresV132,
'cpuPinning', 'cpuPinning',
'usbPassthrough', 'usbPassthrough',
'volumeEncryption', 'volumeEncryption',
@ -32,11 +21,58 @@ const featuresV140 = [
'vmSnapshotQuota', 'vmSnapshotQuota',
'longhornV2LVMSupport', 'longhornV2LVMSupport',
'improveMaintenanceMode', 'improveMaintenanceMode',
]; 'upgradeConfigSetting'
],
export const RELEASE_FEATURES = { 'v1.4.1': [],
'v1.3.0': featuresV130, 'v1.4.2': [
'v1.3.1': featuresV131, 'refreshIntervalInSecond',
'v1.3.2': featuresV132, 'allowEmptySnapshotClassName'
'v1.4.0': featuresV140, ],
'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': []
}; };
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();

View File

@ -10,6 +10,7 @@ import {
LOGGING, LOGGING,
STORAGE_CLASS, STORAGE_CLASS,
SECRET, SECRET,
NETWORK_POLICY
} from '@shell/config/types'; } from '@shell/config/types';
import { HCI, VOLUME_SNAPSHOT } from '../types'; import { HCI, VOLUME_SNAPSHOT } from '../types';
import { import {
@ -24,6 +25,7 @@ import {
CONFIGURED_PROVIDERS, CONFIGURED_PROVIDERS,
SUB_TYPE, SUB_TYPE,
ADDRESS, ADDRESS,
DESCRIPTION,
} from '@shell/config/table-headers'; } from '@shell/config/table-headers';
import { IF_HAVE } from '@shell/store/type-map'; import { IF_HAVE } from '@shell/store/type-map';
import { import {
@ -31,8 +33,23 @@ import {
FINGERPRINT, FINGERPRINT,
IMAGE_PROGRESS, IMAGE_PROGRESS,
SNAPSHOT_TARGET_VOLUME, 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'; } from './table-headers';
import { ADD_ONS } from './harvester-map';
import { registerAddonSideNav } from '../utils/dynamic-nav';
const TEMPLATE = HCI.VM_VERSION; const TEMPLATE = HCI.VM_VERSION;
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring'; const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
@ -191,6 +208,142 @@ export function init($plugin, store) {
exact: false 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]); basicType([HCI.VOLUME]);
configureType(HCI.VOLUME, { configureType(HCI.VOLUME, {
location: { location: {
@ -221,6 +374,8 @@ export function init($plugin, store) {
STATE, STATE,
NAME_COL, NAME_COL,
NAMESPACE_COL, NAMESPACE_COL,
HARVESTER_DESCRIPTION,
IMAGE_STORAGE_CLASS,
IMAGE_PROGRESS, IMAGE_PROGRESS,
IMAGE_DOWNLOAD_SIZE, IMAGE_DOWNLOAD_SIZE,
IMAGE_VIRTUAL_SIZE, IMAGE_VIRTUAL_SIZE,
@ -253,7 +408,7 @@ export function init($plugin, store) {
}); });
if (isSingleVirtualCluster) { if (isSingleVirtualCluster) {
headers(NAMESPACE, [STATE, NAME_UNLINKED, AGE]); headers(NAMESPACE, [STATE, NAME_UNLINKED, DESCRIPTION, AGE]);
basicType([NAMESPACE]); basicType([NAMESPACE]);
virtualType({ virtualType({
labelKey: 'harvester.namespace.label', labelKey: 'harvester.namespace.label',
@ -419,6 +574,8 @@ export function init($plugin, store) {
[ [
HCI.CLUSTER_NETWORK, HCI.CLUSTER_NETWORK,
HCI.NETWORK_ATTACHMENT, HCI.NETWORK_ATTACHMENT,
HCI.VPC,
NETWORK_POLICY,
HCI.LB, HCI.LB,
HCI.IP_POOL, HCI.IP_POOL,
], ],
@ -449,6 +606,7 @@ export function init($plugin, store) {
HCI.PCI_DEVICE, HCI.PCI_DEVICE,
HCI.SR_IOVGPU_DEVICE, HCI.SR_IOVGPU_DEVICE,
HCI.VGPU_DEVICE, HCI.VGPU_DEVICE,
HCI.MIG_CONFIGURATION,
HCI.USB_DEVICE, HCI.USB_DEVICE,
HCI.ADD_ONS, HCI.ADD_ONS,
HCI.SECRET, HCI.SECRET,
@ -545,6 +703,36 @@ export function init($plugin, store) {
exact: false 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, { configureType(HCI.SNAPSHOT, {
isCreatable: false, isCreatable: false,
location: { location: {
@ -724,7 +912,7 @@ export function init($plugin, store) {
configureType(HCI.PCI_DEVICE, { configureType(HCI.PCI_DEVICE, {
isCreatable: false, isCreatable: false,
hiddenNamespaceGroupButton: true, hiddenNamespaceGroupButton: true,
canYaml: false, canYaml: true,
listGroups: [ listGroups: [
{ {
icon: 'icon-list-grouped', icon: 'icon-list-grouped',
@ -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({ virtualType({
labelKey: 'harvester.usb.label', labelKey: 'harvester.usb.label',
group: 'advanced', group: 'advanced',

View File

@ -1,86 +0,0 @@
import { HCI, MANAGEMENT, CAPI } from '@shell/config/types';
import { HARVESTER, MULTI_CLUSTER } from '@shell/store/features';
import { allHash } from '@shell/utils/promise';
import { BLANK_CLUSTER } from '@shell/store/store-types.js';
export const PRODUCT_NAME = 'harvester-manager';
export const NAME = 'harvesterManager';
const harvesterClustersLocation = {
name: 'c-cluster-product-resource',
params: {
cluster: BLANK_CLUSTER,
product: NAME,
resource: HCI.CLUSTER
}
};
export function init($plugin, store) {
const {
product,
basicType,
spoofedType,
configureType
} = $plugin.DSL(store, NAME);
product({
ifHaveType: CAPI.RANCHER_CLUSTER,
ifFeature: [MULTI_CLUSTER, HARVESTER],
inStore: 'management',
icon: 'harvester',
removable: false,
showClusterSwitcher: false,
weight: 100,
to: harvesterClustersLocation,
category: 'hci',
});
configureType(HCI.CLUSTER, { showListMasthead: false });
basicType([HCI.CLUSTER]);
spoofedType({
labelKey: 'harvesterManager.cluster.label',
name: HCI.CLUSTER,
type: HCI.CLUSTER,
namespaced: false,
weight: -1,
route: {
name: 'c-cluster-product-resource',
params: {
product: NAME,
resource: HCI.CLUSTER,
}
},
exact: false,
schemas: [
{
id: HCI.CLUSTER,
type: 'schema',
collectionMethods: [],
resourceFields: {},
attributes: { namespaced: true },
},
],
group: 'Root',
getInstances: async() => {
const hash = {
rancherClusters: store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER }),
clusters: store.dispatch('management/findAll', { type: MANAGEMENT.CLUSTER }),
};
if (store.getters['management/schemaFor'](MANAGEMENT.NODE)) {
hash.nodes = store.dispatch('management/findAll', { type: MANAGEMENT.NODE });
}
const res = await allHash(hash);
return res.rancherClusters.map((c) => {
return {
...c,
type: HCI.CLUSTER,
};
});
},
});
}

View File

@ -1,3 +1,6 @@
import { HCI } from '../types';
// TODO: delete this not used variable
export const MemoryUnit = [{ export const MemoryUnit = [{
label: 'Mi', label: 'Mi',
value: 'Mi' value: 'Mi'
@ -69,6 +72,7 @@ export const ADD_ONS = {
RANCHER_MONITORING: 'rancher-monitoring', RANCHER_MONITORING: 'rancher-monitoring',
VM_IMPORT_CONTROLLER: 'vm-import-controller', VM_IMPORT_CONTROLLER: 'vm-import-controller',
LVM_DRIVER: 'lvm.driver.harvesterhci.io', LVM_DRIVER: 'lvm.driver.harvesterhci.io',
KUBEOVN_OPERATOR: 'kubeovn-operator',
}; };
export const CSI_SECRETS = { export const CSI_SECRETS = {
@ -79,3 +83,14 @@ export const CSI_SECRETS = {
CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name', 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', 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
};

View File

@ -15,6 +15,7 @@ export const HCI = {
TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName', TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName',
CREATOR: 'harvesterhci.io/creator', CREATOR: 'harvesterhci.io/creator',
OS: 'harvesterhci.io/os', OS: 'harvesterhci.io/os',
GOLDEN_IMAGE: 'harvesterhci.io/goldenImage',
NETWORK_TYPE: 'network.harvesterhci.io/type', NETWORK_TYPE: 'network.harvesterhci.io/type',
VM_NAME: 'harvesterhci.io/vmName', VM_NAME: 'harvesterhci.io/vmName',
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix', VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
@ -34,6 +35,7 @@ export const HCI = {
NODE_SCHEDULABLE: 'kubevirt.io/schedulable', NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
NETWORK_ROUTE: 'network.harvesterhci.io/route', NETWORK_ROUTE: 'network.harvesterhci.io/route',
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes', MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
UPGRADE: 'harvesterhci.io/upgrade',
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image', OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade', LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
UPGRADE_STATE: 'harvesterhci.io/upgradeState', UPGRADE_STATE: 'harvesterhci.io/upgradeState',
@ -43,11 +45,14 @@ export const HCI = {
IMAGE_SUFFIX: 'harvesterhci.io/image-type', IMAGE_SUFFIX: 'harvesterhci.io/image-type',
OS_TYPE: 'harvesterhci.io/os-type', OS_TYPE: 'harvesterhci.io/os-type',
STORAGE_PROVISIONER: 'harvesterhci.io/storageProvisioner', STORAGE_PROVISIONER: 'harvesterhci.io/storageProvisioner',
SKIP_SINGLE_REPLICA_DETACHED_VOL: 'harvesterhci.io/skipSingleReplicaDetachedVol',
HOST_REQUEST: 'management.cattle.io/pod-requests', HOST_REQUEST: 'management.cattle.io/pod-requests',
STORAGE_CLASS: 'harvesterhci.io/storageClassName', STORAGE_CLASS: 'harvesterhci.io/storageClassName',
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io', STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental', ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
ADDON_DISPLAYNAME: 'addon.harvesterhci.io/displayName',
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error', VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm', KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx', KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx',
NODE_MANUFACTURER: 'manufacturer', NODE_MANUFACTURER: 'manufacturer',
@ -63,4 +68,15 @@ export const HCI = {
VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails', VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId', SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine', 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',
}; };

View File

@ -1 +1,2 @@
export const CLUSTER_NETWORK = 'clusterNetwork'; export const CLUSTER_NETWORK = 'clusterNetwork';
export const VPC = 'vpc';

View File

@ -4,7 +4,6 @@ export const HCI_SETTING = {
LOG_LEVEL: 'log-level', LOG_LEVEL: 'log-level',
SERVER_VERSION: 'server-version', SERVER_VERSION: 'server-version',
UI_INDEX: 'ui-index', UI_INDEX: 'ui-index',
UI_PLUGIN_INDEX: 'ui-plugin-index',
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled', UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
UPGRADE_CHECKER_URL: 'upgrade-checker-url', UPGRADE_CHECKER_URL: 'upgrade-checker-url',
VLAN: 'vlan', VLAN: 'vlan',
@ -28,12 +27,18 @@ export const HCI_SETTING = {
RELEASE_DOWNLOAD_URL: 'release-download-url', RELEASE_DOWNLOAD_URL: 'release-download-url',
CCM_CSI_VERSION: 'harvester-csi-ccm-versions', CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
CSI_DRIVER_CONFIG: 'csi-driver-config', CSI_DRIVER_CONFIG: 'csi-driver-config',
CSI_ONLINE_EXPAND_VALIDATION: 'csi-online-expand-validation',
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds', VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds',
NTP_SERVERS: 'ntp-servers', NTP_SERVERS: 'ntp-servers',
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs', AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes', KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled', LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio', 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'
}; };
export const HCI_ALLOWED_SETTINGS = { export const HCI_ALLOWED_SETTINGS = {
@ -53,6 +58,9 @@ export const HCI_ALLOWED_SETTINGS = {
featureFlag: 'autoRotateRke2CertsSetting' featureFlag: 'autoRotateRke2CertsSetting'
}, },
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' }, [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.SERVER_VERSION]: { readOnly: true },
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' }, [HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' }, [HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
@ -61,11 +69,13 @@ export const HCI_ALLOWED_SETTINGS = {
kind: 'multiline', canReset: true, from: 'import' kind: 'multiline', canReset: true, from: 'import'
}, },
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' }, [HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: {}, [HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: { kind: 'number' },
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: {}, [HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: { kind: 'number' },
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { featureFlag: 'supportBundleNodeCollectionTimeoutSetting' }, [HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { kind: 'number', featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' }, [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.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' }, [HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' }, [HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
[HCI_SETTING.SSL_PARAMETERS]: { [HCI_SETTING.SSL_PARAMETERS]: {
@ -74,7 +84,6 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES]: { from: 'import', canReset: true }, [HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES]: { from: 'import', canReset: true },
[HCI_SETTING.AUTO_DISK_PROVISION_PATHS]: { canReset: true }, [HCI_SETTING.AUTO_DISK_PROVISION_PATHS]: { canReset: true },
[HCI_SETTING.RELEASE_DOWNLOAD_URL]: { kind: 'url' }, [HCI_SETTING.RELEASE_DOWNLOAD_URL]: { kind: 'url' },
[HCI_SETTING.UI_PLUGIN_INDEX]: { kind: 'url' },
[HCI_SETTING.CONTAINERD_REGISTRY]: { [HCI_SETTING.CONTAINERD_REGISTRY]: {
kind: 'json', from: 'import', canReset: true kind: 'json', from: 'import', canReset: true
}, },
@ -87,13 +96,29 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.NTP_SERVERS]: { [HCI_SETTING.NTP_SERVERS]: {
kind: 'json', from: 'import', canReset: true 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]: { [HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: {
kind: 'boolean', kind: 'boolean',
experimental: true, experimental: true,
featureFlag: 'longhornV2LVMSupport' featureFlag: 'longhornV2LVMSupport'
}, },
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' }, [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',
}
}; };
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = { export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {

View File

@ -1,6 +1,7 @@
/** /**
* Harvester * Harvester
*/ */
import { DESCRIPTION } from '@shell/config/table-headers';
// image // image
export const IMAGE_DOWNLOAD_SIZE = { export const IMAGE_DOWNLOAD_SIZE = {
@ -88,3 +89,144 @@ export const MACHINE_POOLS = {
align: 'center', align: 'center',
width: 100, 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',
};

View File

@ -2,3 +2,42 @@ export const BACKUP_TYPE = {
BACKUP: 'backup', BACKUP: 'backup',
SNAPSHOT: 'snapshot' 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',
};

View File

@ -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>

View File

@ -4,6 +4,7 @@ import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { LONGHORN, METRIC } from '@shell/config/types'; import { LONGHORN, METRIC } from '@shell/config/types';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import { UNIT_SUFFIX } from '../../utils/unit';
import HarvesterCPUUsed from '../../formatters/HarvesterCPUUsed'; import HarvesterCPUUsed from '../../formatters/HarvesterCPUUsed';
import HarvesterMemoryUsed from '../../formatters/HarvesterMemoryUsed'; import HarvesterMemoryUsed from '../../formatters/HarvesterMemoryUsed';
import HarvesterStorageUsed from '../../formatters/HarvesterStorageUsed'; import HarvesterStorageUsed from '../../formatters/HarvesterStorageUsed';
@ -121,7 +122,7 @@ export default {
memoryUnits() { memoryUnits() {
const exponent = exponentNeeded(this.memoryTotal, 1024); const exponent = exponentNeeded(this.memoryTotal, 1024);
return `${ UNITS[exponent] }iB`; return `${ UNITS[exponent] }${ UNIT_SUFFIX }`;
}, },
nodeType() { nodeType() {

View File

@ -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>

View File

@ -24,54 +24,49 @@ export default {
}, },
async fetch() { async fetch() {
const hash = await allHash({ await allHash({
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }), vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }), vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }),
allClusterNetwork: this.$store.dispatch('harvester/findAll', { type: HCI.CLUSTER_NETWORK }), allClusterNetwork: this.$store.dispatch('harvester/findAll', { type: HCI.CLUSTER_NETWORK }),
}); });
const instanceMap = {};
(hash.vmis || []).forEach((vmi) => {
const vmiUID = vmi?.metadata?.ownerReferences?.[0]?.uid;
if (vmiUID) {
instanceMap[vmiUID] = vmi;
}
});
this.allClusterNetwork = hash.allClusterNetwork;
this.rows = hash.vms.filter((row) => {
return instanceMap[row.metadata?.uid]?.status?.nodeName === this.node?.metadata?.labels?.[HOSTNAME];
});
},
data() {
return {
rows: [],
allClusterNetwork: []
};
}, },
computed: { computed: {
allClusterNetwork() {
return this.$store.getters['harvester/all'](HCI.CLUSTER_NETWORK);
},
rows() {
const vms = this.$store.getters['harvester/all'](HCI.VM);
return vms.filter((vm) => vm.vmi?.status?.nodeName === this.node?.metadata?.labels?.[HOSTNAME]);
},
headers() { headers() {
return [ return [
STATE, STATE,
NAME, NAME,
{ {
name: 'vmCPU', name: 'CPU',
labelKey: 'tableHeaders.cpu', label: 'CPU',
search: false, sort: ['displayCPU'],
sort: ['spec.template.spec.domain.cpu.cores'], value: 'displayCPU',
value: 'spec.template.spec.domain.cpu.cores', align: 'center',
width: 120 dashIfEmpty: true,
}, },
{ {
name: 'vmRAM', name: 'Memory',
labelKey: 'glance.memory', value: 'displayMemory',
search: false,
sort: ['memorySort'], sort: ['memorySort'],
value: 'spec.template.spec.domain.resources.limits.memory', align: 'center',
width: 120 labelKey: 'tableHeaders.memory',
formatter: 'Si',
formatterOpts: {
opts: {
increment: 1024, addSuffix: true, maxExponent: 3, minExponent: 3, suffix: 'i',
},
needParseSi: true
},
}, },
{ {
name: 'ip', name: 'ip',
@ -87,8 +82,6 @@ export default {
]; ];
}, },
}, },
methods: {}
}; };
</script> </script>

View File

@ -9,7 +9,7 @@ import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
import Loading from '@shell/components/Loading.vue'; import Loading from '@shell/components/Loading.vue';
import SortableTable from '@shell/components/SortableTable'; import SortableTable from '@shell/components/SortableTable';
import Banner from '@components/Banner/Banner.vue'; import Banner from '@components/Banner/Banner.vue';
import { UNIT_SUFFIX } from '../../utils/unit';
import metricPoller from '@shell/mixins/metric-poller'; import metricPoller from '@shell/mixins/metric-poller';
import { import {
METRIC, NODE, LONGHORN, POD, EVENT METRIC, NODE, LONGHORN, POD, EVENT
@ -27,6 +27,7 @@ import Instance from './VirtualMachineInstance';
import Disk from './HarvesterHostDisk'; import Disk from './HarvesterHostDisk';
import VlanStatus from './VlanStatus'; import VlanStatus from './VlanStatus';
import HarvesterKsmtuned from './HarvesterKsmtuned.vue'; import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
import HarvesterHugepages from './HarvesterHugepages.vue';
import HarvesterSeeder from './HarvesterSeeder'; import HarvesterSeeder from './HarvesterSeeder';
const LONGHORN_SYSTEM = 'longhorn-system'; const LONGHORN_SYSTEM = 'longhorn-system';
@ -46,6 +47,7 @@ export default {
VlanStatus, VlanStatus,
LabelValue, LabelValue,
HarvesterKsmtuned, HarvesterKsmtuned,
HarvesterHugepages,
Loading, Loading,
SortableTable, SortableTable,
HarvesterSeeder, HarvesterSeeder,
@ -178,7 +180,7 @@ export default {
minExponent: 3, minExponent: 3,
maxExponent: 3, maxExponent: 3,
maxPrecision: 2, maxPrecision: 2,
suffix: 'iB', suffix: UNIT_SUFFIX,
}; };
const longhornDisks = Object.keys(diskStatus).map((key) => { const longhornDisks = Object.keys(diskStatus).map((key) => {
@ -209,6 +211,12 @@ export default {
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED); 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() { hasBlockDevicesSchema() {
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE); return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
}, },
@ -468,6 +476,16 @@ export default {
/> />
</Tab> </Tab>
<Tab
v-if="hasHugepagesSchema"
name="hugepages"
:weight="0"
:show-header="false"
:label="t('harvester.host.tabs.hugepages')"
>
<HarvesterHugepages :node="value" />
</Tab>
<Tab <Tab
v-if="seederEnabled" v-if="seederEnabled"
name="seeder" name="seeder"

View File

@ -75,7 +75,7 @@ export default {
<div class="row"> <div class="row">
<div class="col span-6 mb-20"> <div class="col span-6 mb-20">
<LabelValue <LabelValue
:name="t('harvester.schedule.cron')" :name="t('harvester.schedule.cron.label')"
:value="cronExpression" :value="cronExpression"
/> />
</div> </div>

View File

@ -125,6 +125,7 @@ export default {
:resource="value" :resource="value"
:mode="mode" :mode="mode"
:apply-hooks="applyHooks" :apply-hooks="applyHooks"
@error="e=>errors=e"
> >
<Tabbed <Tabbed
v-if="spec" v-if="spec"
@ -171,6 +172,9 @@ export default {
:cpu="cpu" :cpu="cpu"
:mode="mode" :mode="mode"
:memory="memory" :memory="memory"
:max-cpu="maxCpu"
:max-memory="maxMemory"
:enable-hot-plug="cpuMemoryHotplugEnabled"
/> />
</div> </div>
<div class="row"> <div class="row">

View File

@ -118,6 +118,10 @@ export default {
imageName() { imageName() {
return this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-'; return this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-';
}, },
sourceType() {
return this.value?.spec?.sourceType;
},
} }
}; };
</script> </script>
@ -252,6 +256,16 @@ export default {
</div> </div>
</div> </div>
<div class="row">
<div class="col span-12">
<LabelValue
:name="t('harvester.image.source')"
:value="sourceType"
class="mb-20"
/>
</div>
</div>
<div <div
v-if="errorMessage !== '-'" v-if="errorMessage !== '-'"
class="row" class="row"

View File

@ -125,6 +125,7 @@ export default {
:resource="value" :resource="value"
:mode="mode" :mode="mode"
:apply-hooks="applyHooks" :apply-hooks="applyHooks"
@error="e=>errors=e"
> >
<Tabbed <Tabbed
v-if="spec" v-if="spec"
@ -171,6 +172,9 @@ export default {
:cpu="cpu" :cpu="cpu"
:mode="mode" :mode="mode"
:memory="memory" :memory="memory"
:max-cpu="maxCpu"
:max-memory="maxMemory"
:enable-hot-plug="cpuMemoryHotplugEnabled"
/> />
<div class="row mb-10"> <div class="row mb-10">

View File

@ -5,6 +5,7 @@ import CreateEditView from '@shell/mixins/create-edit-view';
import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress'; import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
import VMConsoleBar from '../../../components/VMConsoleBar'; import VMConsoleBar from '../../../components/VMConsoleBar';
import { HCI } from '../../../types'; import { HCI } from '../../../types';
import { getVmCPUMemoryValues } from '../../../utils/cpuMemory';
const UNDEFINED = 'n/a'; const UNDEFINED = 'n/a';
@ -91,9 +92,9 @@ export default {
}, },
flavor() { 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() { kernelRelease() {

View File

@ -17,6 +17,13 @@ export default {
default: () => { default: () => {
return {}; return {};
} }
},
vmimResource: {
type: Object,
required: true,
default: () => {
return {};
}
} }
}, },
@ -25,6 +32,12 @@ export default {
}, },
computed: { computed: {
liveMigrationProgressEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
},
migrationPhase() {
return this.vmimResource?.status?.phase || 'N/A';
},
migrationState() { migrationState() {
return this.localResource?.status?.migrationState; return this.localResource?.status?.migrationState;
}, },
@ -58,6 +71,18 @@ export default {
<template> <template>
<div> <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="row mb-20">
<div class="col span-6"> <div class="col span-6">
<LabelValue <LabelValue

View File

@ -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_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 { export default {
name: 'VMIDetailsPage', name: 'VMIDetailsPage',
@ -63,6 +65,7 @@ export default {
hasResourceQuotaSchema: false, hasResourceQuotaSchema: false,
switchToCloud: false, switchToCloud: false,
VM_METRICS_DETAIL_URL, VM_METRICS_DETAIL_URL,
VM_MIGRATION_DETAIL_URL,
showVmMetrics: false, showVmMetrics: false,
}; };
}, },
@ -78,6 +81,7 @@ export default {
events: this.$store.dispatch(`${ inStore }/findAll`, { type: EVENT }), events: this.$store.dispatch(`${ inStore }/findAll`, { type: EVENT }),
allSSHs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SSH }), allSSHs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SSH }),
vmis: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMI }), 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 }), restore: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESTORE }),
}; };
@ -131,6 +135,39 @@ export default {
return this.$store.getters[`${ inStore }/all`](EVENT); 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() { events() {
return this.allEvents.filter((e) => { return this.allEvents.filter((e) => {
const { name, creationTimestamp } = this.value?.metadata || {}; const { name, creationTimestamp } = this.value?.metadata || {};
@ -138,7 +175,6 @@ export default {
const pvcName = this.value.persistentVolumeClaimName || []; const pvcName = this.value.persistentVolumeClaimName || [];
const involvedName = e?.involvedObject?.name; const involvedName = e?.involvedObject?.name;
const matchPVC = pvcName.find((name) => name === involvedName); const matchPVC = pvcName.find((name) => name === involvedName);
return (involvedName === name || involvedName === podName || matchPVC) && e.firstTimestamp >= creationTimestamp; return (involvedName === name || involvedName === podName || matchPVC) && e.firstTimestamp >= creationTimestamp;
@ -157,6 +193,10 @@ export default {
vm: this.value.name vm: this.value.name
}; };
}, },
liveMigrationProgressEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
},
}, },
methods: { methods: {
@ -173,6 +213,7 @@ export default {
const diskRows = this.getDiskRows(neu); const diskRows = this.getDiskRows(neu);
this['diskRows'] = diskRows; this['diskRows'] = diskRows;
this['networkRows'] = this.getNetworkRows(neu, { fromTemplate: false, init: false });
}, },
deep: true deep: true
} }
@ -225,6 +266,7 @@ export default {
<Network <Network
v-model:value="networkRows" v-model:value="networkRows"
mode="view" mode="view"
:vm="value"
/> />
</Tab> </Tab>
@ -346,6 +388,20 @@ export default {
<Migration <Migration
:value="value" :value="value"
:vmi-resource="vmi" :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> </Tab>

View File

@ -1,12 +1,13 @@
<script> <script>
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { runStrategies as runStrategyOptions } from '../config/harvester-map';
import { Card } from '@components/Card'; import { Card } from '@components/Card';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import { Checkbox } from '@components/Form/Checkbox'; import { Checkbox } from '@components/Form/Checkbox';
import AsyncButton from '@shell/components/AsyncButton'; import AsyncButton from '@shell/components/AsyncButton';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default { export default {
name: 'CloneVMModal', name: 'CloneVMModal',
@ -14,7 +15,7 @@ export default {
emits: ['close'], emits: ['close'],
components: { components: {
AsyncButton, Banner, Checkbox, Card, LabeledInput AsyncButton, Banner, Checkbox, Card, LabeledInput, LabeledSelect
}, },
props: { props: {
@ -28,7 +29,9 @@ export default {
return { return {
name: '', name: '',
cloneData: true, cloneData: true,
errors: [] errors: [],
runStrategy: runStrategyOptions[1],
runStrategyOptions
}; };
}, },
@ -38,6 +41,9 @@ export default {
actionResource() { actionResource() {
return this.resources[0]; return this.resources[0];
}, },
vmCloneRunStrategyEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('vmCloneRunStrategy');
},
}, },
methods: { methods: {
@ -58,7 +64,8 @@ export default {
// deep clone // deep clone
try { 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) { if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', { this.$store.dispatch('growl/success', {
@ -106,6 +113,19 @@ export default {
:label="t('harvester.modal.cloneVM.name')" :label="t('harvester.modal.cloneVM.name')"
required 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>
<template <template
@ -127,11 +147,6 @@ export default {
@click="create" @click="create"
/> />
</div> </div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</template> </template>
</Card> </Card>
</template> </template>

View File

@ -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>

View File

@ -32,7 +32,7 @@ export default {
resources: { resources: {
type: Array, type: Array,
required: true required: true
} },
}, },
data() { data() {
@ -42,8 +42,25 @@ export default {
computed: { computed: {
...mapState('action-menu', ['modalData']), ...mapState('action-menu', ['modalData']),
warningMessageKey() { title() {
return this.modalData.warningMessageKey; 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() { names() {
@ -70,6 +87,12 @@ export default {
return this.resources[0].nameDisplay; return this.resources[0].nameDisplay;
}, },
needConfirmation() {
const { needConfirmation = true } = this.modalData ;
return needConfirmation === true;
},
plusMore() { plusMore() {
const remaining = this.resources.length - this.names.length; const remaining = this.resources.length - this.names.length;
@ -97,11 +120,15 @@ export default {
}, },
deleteDisabled() { deleteDisabled() {
if (!this.needConfirmation) {
return false;
}
return this.confirmName !== this.nameToMatch; return this.confirmName !== this.nameToMatch;
}, },
protip() { protip() {
return this.t('promptRemove.protip', { alternateLabel }); return this.t('dialog.promptRemove.protip', { alternateLabel });
}, },
}, },
@ -118,6 +145,7 @@ export default {
try { try {
for (const resource of this.resources) { for (const resource of this.resources) {
await resource.remove(); await resource.remove();
if (this.modalData?.extraActionAfterRemove) await this.modalData.extraActionAfterRemove();
} }
buttonDone(true); buttonDone(true);
this.close(); this.close();
@ -137,19 +165,26 @@ export default {
> >
<template #title> <template #title>
<h4 class="text-default-text"> <h4 class="text-default-text">
{{ t('promptRemove.title') }} {{ t(title, { type }, true) }}
</h4> </h4>
</template> </template>
<template #body> <template #body>
<div class="pl-10 pr-10"> <div class="pl-10 pr-10">
<span <span
v-clean-html="t(warningMessageKey, { type, names: resourceNames }, true)" v-clean-html="warningMessage"
></span> ></span>
<div
v-if="needConfirmation"
class="mt-20"
>
<div class="mt-10 mb-10"> <div class="mt-10 mb-10">
<span <span
v-clean-html="t('promptRemove.confirmName', { nameToMatch: escapeHtml(nameToMatch) }, true)" v-clean-html="t('dialog.promptRemove.confirmName', {
type: formattedType,
nameToMatch: escapeHtml(nameToMatch)
}, true)"
></span> ></span>
</div> </div>
<div class="mb-10"> <div class="mb-10">
@ -163,9 +198,12 @@ export default {
<div class="text-info mt-20"> <div class="text-info mt-20">
{{ protip }} {{ protip }}
</div> </div>
</div>
<Banner <Banner
v-for="(error, i) in errors" v-for="(error, i) in errors"
:key="i" :key="i"
:label="error"
color="error"
/> />
</div> </div>
</template> </template>

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>

View File

@ -93,7 +93,10 @@ export default {
</template> </template>
<template #body> <template #body>
{{ t('harvester.usb.enablePassthroughWarning') }} <t
k="harvester.usb.enablePassthroughWarning"
:raw="true"
/>
</template> </template>
<template #actions> <template #actions>

View File

@ -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>

View File

@ -11,7 +11,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
export default { export default {
name: 'HotplugModal', name: 'HotplugVolumeModal',
emits: ['close'], emits: ['close'],
@ -57,8 +57,12 @@ export default {
if (!!pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) { if (!!pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
return false; 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) => { .map((pvc) => {
return { return {
@ -86,7 +90,7 @@ export default {
if (res._status === 200 || res._status === 204) { if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', { this.$store.dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'), 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 }); }, { root: true });
this.close(); this.close();
@ -118,7 +122,7 @@ export default {
> >
<template #title> <template #title>
<h4 <h4
v-clean-html="t('harvester.modal.hotplug.title')" v-clean-html="t('harvester.modal.hotplugVolume.title')"
class="text-default-text" class="text-default-text"
/> />
</template> </template>
@ -137,6 +141,12 @@ export default {
class="mt-20" class="mt-20"
required required
/> />
<Banner
v-for="(err, i) in errors"
:key="i"
:label="err"
color="error"
/>
</template> </template>
<template #actions> <template #actions>
@ -156,11 +166,6 @@ export default {
@click="save" @click="save"
/> />
</div> </div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div> </div>
</template> </template>
</Card> </Card>

View File

@ -103,6 +103,12 @@ export default {
:label="t('generic.name')" :label="t('generic.name')"
required required
/> />
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template> </template>
<template #actions> <template #actions>
@ -121,13 +127,6 @@ export default {
@click="save" @click="save"
/> />
</div> </div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</div> </div>
</template> </template>
</Card> </Card>

View File

@ -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>

View File

@ -120,6 +120,12 @@ export default {
v-model:value="description" v-model:value="description"
:label="t('harvester.modal.createTemplate.description')" :label="t('harvester.modal.createTemplate.description')"
/> />
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template> </template>
<template #actions> <template #actions>
@ -138,11 +144,6 @@ export default {
@click="save" @click="save"
/> />
</div> </div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div> </div>
</template> </template>
</Card> </Card>

View File

@ -128,6 +128,12 @@ export default {
<Banner color="warning"> <Banner color="warning">
<span>{{ t('harvester.modal.ejectCDROM.warnTip') }}</span> <span>{{ t('harvester.modal.ejectCDROM.warnTip') }}</span>
</Banner> </Banner>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template> </template>
<template #actions> <template #actions>
@ -147,13 +153,6 @@ export default {
@click="remove" @click="remove"
/> />
</div> </div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</div> </div>
</template> </template>
</Card> </Card>

View File

@ -10,6 +10,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import { NAMESPACE, STORAGE_CLASS } from '@shell/config/types'; import { NAMESPACE, STORAGE_CLASS } from '@shell/config/types';
import { allHash } from '@shell/utils/promise'; import { allHash } from '@shell/utils/promise';
import { isInternalStorageClass } from '../utils/storage-class';
export default { export default {
name: 'HarvesterExportImageDialog', name: 'HarvesterExportImageDialog',
@ -34,9 +35,14 @@ export default {
await allHash(hash); 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);
if (this.isLonghornV1Volume) {
this['storageClassName'] = defaultStorage?.metadata?.name || 'longhorn'; this['storageClassName'] = defaultStorage?.metadata?.name || 'longhorn';
} else {
this['storageClassName'] = this.nonLonghornV1StorageClasses[0]?.metadata?.name || '';
}
}, },
data() { data() {
@ -54,7 +60,7 @@ export default {
...mapGetters({ t: 'i18n/t' }), ...mapGetters({ t: 'i18n/t' }),
actionResource() { actionResource() {
return this.resources[0]; return this.resources[0] || {};
}, },
namespaces() { namespaces() {
@ -73,24 +79,48 @@ export default {
return out; return out;
}, },
isLonghornV1Volume() {
return this.actionResource?.isLonghornV1 === true;
},
disableSave() { disableSave() {
return !(this.name && this.namespace && this.storageClassName); return !(this.name && this.namespace && this.storageClassName);
}, },
storageClassOptions() { allStorageClasses() {
const inStore = this.$store.getters['currentProduct'].inStore; 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) => { return this.$store.getters[`${ inStore }/all`](STORAGE_CLASS) || [];
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name; },
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 { return {
label, label,
value: s.name, value: s.name,
disabled: isInternal,
}; };
}) || []; }) || [];
return out; return options;
}, },
}, },
@ -164,6 +194,12 @@ export default {
class="mt-20" class="mt-20"
required required
/> />
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template> </template>
<template #actions> <template #actions>
@ -182,11 +218,6 @@ export default {
@click="save" @click="save"
/> />
</div> </div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div> </div>
</template> </template>
</Card> </Card>

View File

@ -1,13 +1,12 @@
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
import { Card } from '@components/Card'; import { Card } from '@components/Card';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton'; import AsyncButton from '@shell/components/AsyncButton';
export default { export default {
name: 'HarvesterHotUnplugModal', name: 'HarvesterHotUnplug',
emits: ['close'], emits: ['close'],
@ -35,8 +34,25 @@ export default {
actionResource() { actionResource() {
return this.resources[0]; return this.resources[0];
}, },
diskName() {
return this.modalData.diskName; name() {
return this.modalData.name;
},
isVolume() {
return this.modalData.type === 'volume';
},
titleKey() {
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.title' : 'harvester.virtualMachine.hotUnplug.detachNIC.title';
},
actionLabelKey() {
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabel' : 'harvester.virtualMachine.hotUnplug.detachNIC.actionLabel';
},
successMessageKey() {
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.success' : 'harvester.virtualMachine.hotUnplug.detachNIC.success';
} }
}, },
@ -47,14 +63,20 @@ export default {
async save(buttonCb) { async save(buttonCb) {
try { try {
const res = await this.actionResource.doAction('removeVolume', { diskName: this.diskName }); let res;
if (this.isVolume) {
res = await this.actionResource.doAction('removeVolume', { diskName: this.name });
} else {
res = await this.actionResource.doAction('removeNic', { interfaceName: this.name });
}
if (res._status === 200 || res._status === 204) { if (res._status === 200 || res._status === 204) {
this.$store.dispatch( this.$store.dispatch(
'growl/success', 'growl/success',
{ {
title: this.t('generic.notification.title.succeed'), title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.modal.hotunplug.success', { name: this.diskName }) message: this.t(this.successMessageKey, { name: this.name })
}, },
{ root: true } { root: true }
); );
@ -64,14 +86,14 @@ export default {
} else { } else {
const error = [res?.data] || exceptionToErrorsArray(res); const error = [res?.data] || exceptionToErrorsArray(res);
this['errors'] = error; this.errors = error;
buttonCb(false); buttonCb(false);
} }
} catch (err) { } catch (err) {
const error = err?.data || err; const error = err?.data || err;
const message = exceptionToErrorsArray(error); const message = exceptionToErrorsArray(error);
this['errors'] = message; this.errors = message;
buttonCb(false); buttonCb(false);
} }
} }
@ -87,9 +109,15 @@ export default {
> >
<template #title> <template #title>
<h4 <h4
v-clean-html="t('harvester.virtualMachine.unplug.title', { name: diskName })" v-clean-html="t(titleKey, { name })"
class="text-default-text" class="text-default-text"
/> />
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template> </template>
<template #actions> <template #actions>
@ -105,19 +133,12 @@ export default {
<AsyncButton <AsyncButton
mode="apply" mode="apply"
:action-label="t('harvester.virtualMachine.unplug.actionLabel')" :action-label="t(actionLabelKey)"
:waiting-label="t('harvester.virtualMachine.unplug.actionLabel')" :waiting-label="t(actionLabelKey)"
:success-label="t('harvester.virtualMachine.unplug.actionLabel')" :success-label="t(actionLabelKey)"
@click="save" @click="save"
/> />
</div> </div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</div> </div>
</template> </template>
</Card> </Card>
@ -133,4 +154,8 @@ export default {
justify-content: flex-end; justify-content: flex-end;
width: 100%; width: 100%;
} }
::v-deep(.card-title) {
display: block;
}
</style> </style>

View File

@ -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>

View File

@ -161,6 +161,12 @@ export default {
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')" :placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
:options="nodeNameList" :options="nodeNameList"
/> />
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template> </template>
<template <template
@ -181,12 +187,6 @@ export default {
@click="apply" @click="apply"
/> />
</div> </div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template> </template>
</Card> </Card>
</template> </template>

View File

@ -5,6 +5,7 @@ import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton'; import AsyncButton from '@shell/components/AsyncButton';
import UnitInput from '@shell/components/form/UnitInput'; import UnitInput from '@shell/components/form/UnitInput';
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
import { GIBIBYTE } from '../utils/unit';
export default { export default {
name: 'HarvesterVMQuotaDialog', name: 'HarvesterVMQuotaDialog',
@ -29,6 +30,7 @@ export default {
data() { data() {
return { return {
GIBIBYTE,
totalSnapshotSize: '', totalSnapshotSize: '',
errors: [] errors: []
}; };
@ -44,7 +46,6 @@ export default {
}, },
methods: { methods: {
close() { close() {
this.totalSnapshotSize = ''; this.totalSnapshotSize = '';
this.$emit('close'); this.$emit('close');
@ -92,9 +93,15 @@ export default {
:input-exponent="3" :input-exponent="3"
:increment="1024" :increment="1024"
:output-modifier="true" :output-modifier="true"
suffix="GiB" :suffix="GIBIBYTE"
class="mb-20" class="mb-20"
/> />
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template> </template>
<template #actions> <template #actions>
@ -108,12 +115,6 @@ export default {
</button> </button>
<AsyncButton @click="save" /> <AsyncButton @click="save" />
</div> </div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</div> </div>
</template> </template>
</Card> </Card>

View File

@ -136,6 +136,12 @@ export default {
:options="backupOption" :options="backupOption"
required required
/> />
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template> </template>
<template #actions> <template #actions>
@ -154,11 +160,6 @@ export default {
@click="saveRestore" @click="saveRestore"
/> />
</div> </div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div> </div>
</template> </template>
</Card> </Card>

View File

@ -1,4 +1,5 @@
<script> <script>
import { NAMESPACE } from '@shell/config/types';
import { randomStr } from '@shell/utils/string'; import { randomStr } from '@shell/utils/string';
import { exceptionToErrorsArray, stringify } from '@shell/utils/error'; import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
@ -6,7 +7,15 @@ import AsyncButton from '@shell/components/AsyncButton';
import GraphCircle from '@shell/components/graph/Circle'; import GraphCircle from '@shell/components/graph/Circle';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import AppModal from '@shell/components/AppModal'; 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 } 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 { export default {
name: 'SupportBundle', name: 'SupportBundle',
@ -17,14 +26,36 @@ export default {
AsyncButton, AsyncButton,
Banner, Banner,
AppModal, 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() { data() {
return { return {
isOpen: false,
errors: [],
version: '',
clusterName: '',
url: '', url: '',
description: '', description: '',
errors: [], namespaces: [],
isOpen: false, defaultNamespaces: [],
timeout: '',
expiration: '',
nodeTimeout: '',
}; };
}, },
@ -39,23 +70,51 @@ export default {
percentage() { percentage() {
return this.$store.getters['harvester-common/getBundlePercentage']; 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: { watch: {
isShowBundleModal: { isShowBundleModal: {
immediate: true,
handler(show) { handler(show) {
if (show) { this.isOpen = show;
this.$nextTick(() => {
this.isOpen = true;
});
} else {
this.isOpen = false;
this.url = '';
this.description = '';
} }
}, },
immediate: true
isOpen(newVal) {
if (newVal) {
this.loadDefaultSettings();
} else {
this.resetForm();
}
}, },
}, },
@ -65,33 +124,93 @@ export default {
close() { close() {
this.isOpen = false; this.isOpen = false;
this.$store.commit('harvester-common/toggleBundleModal', 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) { async save(buttonCb) {
this.errors = []; this.errors = [];
const name = `bundle-${ randomStr(5).toLowerCase() }`; const name = `bundle-${ this.clusterName }-${ this.version }-${ randomStr(5).toLowerCase() }`;
const namespace = 'harvester-system'; 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 = { const bundleCrd = {
apiVersion: 'harvesterhci.io/v1beta1', apiVersion: 'harvesterhci.io/v1beta1',
type: HCI.SUPPORT_BUNDLE, type: HCI.SUPPORT_BUNDLE,
kind: 'SupportBundle', kind: 'SupportBundle',
metadata: { metadata: { name, namespace },
name, spec,
namespace
},
spec: {
issueURL: this.url,
description: this.description
}
}; };
try {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd); const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
try {
await bundleValue.save(); await bundleValue.save();
this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true }); this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true });
@ -118,44 +237,87 @@ export default {
@close="close" @close="close"
> >
<div class="p-20"> <div class="p-20">
<h2> <h2>{{ t('harvester.modal.bundle.title') }}</h2>
{{ t('harvester.modal.bundle.title') }} <div class="content">
</h2>
<div <div
v-if="!bundlePending" v-if="bundlePending"
class="content" class="circle mb-20"
> >
<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">
<GraphCircle <GraphCircle
primary-stroke-color="green" primary-stroke-color="green"
secondary-stroke-color="white" secondary-stroke-color="lightgrey"
:stroke-width="6" :stroke-width="6"
:percentage="percentage" :percentage="percentage"
:show-text="true" :show-text="true"
/> />
</div> </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"
/>
<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 <div
v-for="(err, idx) in errors" v-for="(err, idx) in errors"
:key="idx" :key="idx"
@ -165,7 +327,6 @@ export default {
:label="stringify(err)" :label="stringify(err)"
/> />
</div> </div>
<div class="footer mt-20"> <div class="footer mt-20">
<button <button
class="btn btn-sm role-secondary mr-10" class="btn btn-sm role-secondary mr-10"
@ -173,7 +334,6 @@ export default {
> >
{{ t('generic.close') }} {{ t('generic.close') }}
</button> </button>
<AsyncButton <AsyncButton
type="submit" type="submit"
mode="generate" mode="generate"
@ -183,6 +343,7 @@ export default {
/> />
</div> </div>
</div> </div>
</div>
</app-modal> </app-modal>
</div> </div>
</template> </template>
@ -194,6 +355,10 @@ export default {
max-height: 100vh; max-height: 100vh;
} }
.labeled-select.taggable ::v-deep(.vs__selected-options .vs__selected.vs__selected > button) {
margin: 0 7px;
}
.bundle { .bundle {
cursor: pointer; cursor: pointer;
color: var(--primary); color: var(--primary);
@ -204,12 +369,15 @@ export default {
} }
.content { .content {
height: 218px;
.circle { .circle {
padding-top: 20px; padding: 10px 0;
height: 160px; height: 160px;
} }
.namespace-select {
:deep(.vs__dropdown-menu) {
max-height: 210px;
}
}
} }
div { div {

View File

@ -132,6 +132,12 @@ export default {
:label="t('generic.name')" :label="t('generic.name')"
required required
/> />
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template> </template>
<template #actions> <template #actions>
@ -150,11 +156,6 @@ export default {
@click="save" @click="save"
/> />
</div> </div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div> </div>
</template> </template>
</Card> </Card>

View File

@ -73,8 +73,10 @@ export default {
data.spec.clusterNetwork = this.clusterNetwork; 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', { await this.$store.dispatch('harvester/request', {
url: `/v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`, url,
method: 'PUT', method: 'PUT',
data, data,
}); });
@ -107,6 +109,12 @@ export default {
:placeholder="t('harvester.harvesterVlanConfigMigrateDialog.targetClusterNetwork.placeholder')" :placeholder="t('harvester.harvesterVlanConfigMigrateDialog.targetClusterNetwork.placeholder')"
:options="clusterNetworks" :options="clusterNetworks"
/> />
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template> </template>
<template #actions> <template #actions>
@ -125,11 +133,6 @@ export default {
@click="apply" @click="apply"
/> />
</div> </div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div> </div>
</template> </template>
</Card> </Card>

View File

@ -70,16 +70,16 @@ export default {
v-clean-html="t('harvester.modal.restart.tip')" v-clean-html="t('harvester.modal.restart.tip')"
class="pl-10 pr-10" class="pl-10 pr-10"
/> />
</template>
<template #actions>
<div class="bottom">
<Banner <Banner
v-for="(err, i) in errors" v-for="(err, i) in errors"
:key="i" :key="i"
color="error" color="error"
:label="err" :label="err"
/> />
</template>
<template #actions>
<div class="bottom">
<div class="buttons"> <div class="buttons">
<button <button
class="btn role-secondary mr-10" class="btn role-secondary mr-10"

View File

@ -145,6 +145,12 @@ export default {
class="mt-20" class="mt-20"
required required
/> />
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template> </template>
<template #actions> <template #actions>
@ -162,10 +168,6 @@ export default {
@click="save" @click="save"
/> />
</div> </div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div> </div>
</template> </template>
</Card> </Card>

View File

@ -79,6 +79,12 @@ export default {
:label="t('harvester.modal.snapshot.name')" :label="t('harvester.modal.snapshot.name')"
required required
/> />
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template> </template>
<template #actions> <template #actions>
@ -96,12 +102,6 @@ export default {
@click="save" @click="save"
/> />
</div> </div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</div> </div>
</template> </template>
</Card> </Card>

View File

@ -98,6 +98,12 @@ export default {
:label="t('harvester.modal.volumeClone.name')" :label="t('harvester.modal.volumeClone.name')"
required required
/> />
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template> </template>
<template #actions> <template #actions>
@ -115,10 +121,6 @@ export default {
@click="save" @click="save"
/> />
</div> </div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div> </div>
</template> </template>
</Card> </Card>

View File

@ -0,0 +1,133 @@
<script>
import Tabbed from '@shell/components/Tabbed';
import Tab from '@shell/components/Tabbed/Tab';
import CruResource from '@shell/components/CruResource';
import { LabeledInput } from '@components/Form/LabeledInput';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import LabelValue from '@shell/components/LabelValue';
import CreateEditView from '@shell/mixins/create-edit-view';
export default {
name: 'HarvesterEditMIGConfiguration',
components: {
Tab,
Tabbed,
CruResource,
LabeledInput,
NameNsDescription,
LabelValue
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
const { profileSpec } = this.value.spec;
return { profileSpec: profileSpec || [] };
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
isView() {
return this.mode === 'view';
},
},
methods: {
updateBeforeSave() {
// MIGConfiguration CRD don't have any namespace field,
// so we need to remove the namespace field before saving
delete this.value.metadata.namespace;
// enable the MIGConfiguration when saving
this.value.spec.enabled = true;
},
labelTitle(profile) {
return `${ profile.name } (available : ${ this.available(profile) })`;
},
available(profile) {
const count = this.value.status?.profileStatus?.find((p) => p.id === profile.id)?.available;
return count || 0;
},
updateRequested(neu, profile) {
if (neu === null || neu === '') return;
const newValue = Number(neu);
const availableCount = this.available(profile);
if (newValue < 0) {
profile.requested = 0;
} else if ( newValue > availableCount ) {
profile.requested = availableCount;
} else {
profile.requested = newValue;
}
}
},
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
finish-button-mode="enable"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
:mode="mode"
/>
<Tabbed
v-bind="$attrs"
class="mt-15"
:side-tabs="true"
>
<Tab
name="profileSpec"
:label="t('harvester.migconfiguration.profileSpec')"
:weight="1"
class="bordered-table"
>
<div
v-for="(profile, index) in profileSpec"
:key="index"
>
<LabelValue
:value="labelTitle(profile)"
class="mb-10"
/>
<LabeledInput
v-model:value="profile.requested"
:min="0"
:disabled="isView"
type="number"
class="mb-20"
:label="`${t('harvester.migconfiguration.requested')}`"
@update:value="updateRequested($event, profile)"
/>
</div>
</Tab>
</Tabbed>
</CruResource>
</template>

View File

@ -38,6 +38,7 @@ export default {
:mode="mode" :mode="mode"
:errors="errors" :errors="errors"
@finish="save" @finish="save"
@error="e=>errors=e"
> >
<component <component
:is="currentComponent" :is="currentComponent"

View File

@ -10,7 +10,7 @@ import { Banner } from '@components/Banner';
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
const DEFAULT_VALUE = { image: { repo: 'rancher/harvester-nvidia-driver-toolkit' } }; const DEFAULT_VALUE = { image: { repository: 'rancher/harvester-nvidia-driver-toolkit' } };
export default { export default {
name: 'EditAddonNvidiaDriverToolkit', name: 'EditAddonNvidiaDriverToolkit',
@ -122,7 +122,7 @@ export default {
> >
<div class="col span-6"> <div class="col span-6">
<LabeledInput <LabeledInput
v-model:value="valuesContentJson.image.repo" v-model:value="valuesContentJson.image.repository"
:mode="mode" :mode="mode"
:required="true" :required="true"
label-key="harvester.addons.nvidiaDriverToolkit.image.repository" label-key="harvester.addons.nvidiaDriverToolkit.image.repository"

View File

@ -1,34 +1,31 @@
<script> <script>
import merge from 'lodash/merge';
import jsyaml from 'js-yaml';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
import Tabbed from '@shell/components/Tabbed'; import Tabbed from '@shell/components/Tabbed';
import Tab from '@shell/components/Tabbed/Tab'; import Tab from '@shell/components/Tabbed/Tab';
import { RadioGroup } from '@components/Form/Radio'; import { RadioGroup } from '@components/Form/Radio';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import { STORAGE_CLASS } from '@shell/config/types'; import { STORAGE_CLASS } from '@shell/config/types';
import { allHash } from '@shell/utils/promise'; import { allHash } from '@shell/utils/promise';
import { set, get, clone } from '@shell/utils/object';
const VALUES_YAML_KEYS = [
'resources.requests.cpu',
'resources.requests.memory',
'resources.limits.cpu',
'resources.limits.memory',
'pvcClaim.enabled',
'pvcClaim.size',
'pvcClaim.storageClassName',
];
const DEFAULT_VALUES = { const DEFAULT_VALUES = {
'resources.requests.cpu': '0.5', resources: {
'resources.requests.memory': '2Gi', requests: {
'resources.limits.cpu': '2', cpu: '0.5',
'resources.limits.memory': '4Gi', memory: '2Gi'
'pvcClaim.enabled': false, },
'pvcClaim.size': '200Gi', limits: {
'pvcClaim.storageClassName': '', cpu: '2',
memory: '4Gi'
}
},
pvcClaim: {
enabled: false,
size: '200Gi',
storageClassName: ''
}
}; };
export default { export default {
@ -64,19 +61,7 @@ export default {
}, },
data() { data() {
let valuesObj = {}; const valuesContent = this.parseValuesContent();
try {
valuesObj = JSON.parse(this.value?.spec?.valuesContent || '{}');
} catch (err) {}
const valuesContent = clone(valuesObj);
VALUES_YAML_KEYS.map((key) => {
if (!get(valuesObj, key)) {
set(valuesContent, key, DEFAULT_VALUES[key]);
}
});
return { valuesContent }; return { valuesContent };
}, },
@ -100,8 +85,21 @@ export default {
}, },
methods: { methods: {
parseValuesContent() {
try {
return merge({}, DEFAULT_VALUES, jsyaml.load(this.value.spec.valuesContent));
} catch (err) {
this.$store.dispatch('growl/fromError', {
title: this.$store.getters['i18n/t']('generic.notification.title.error'),
err: err.data || err,
}, { root: true });
return DEFAULT_VALUES;
}
},
update() { update() {
set(this.value, 'spec.valuesContent', JSON.stringify(this.valuesContent)); this.value.spec.valuesContent = jsyaml.dump(this.valuesContent);
}, },
setDefaultClassName() { setDefaultClassName() {

View File

@ -80,6 +80,7 @@ export default {
:apply-hooks="applyHooks" :apply-hooks="applyHooks"
@finish="save" @finish="save"
@cancel="done" @cancel="done"
@error="e=>errors=e"
> >
<NameNsDescription <NameNsDescription
:value="value" :value="value"

View File

@ -8,7 +8,7 @@ import LabelValue from '@shell/components/LabelValue';
import { BadgeState } from '@components/BadgeState'; import { BadgeState } from '@components/BadgeState';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import { RadioGroup, RadioButton } from '@components/Form/Radio'; import { RadioGroup } from '@components/Form/Radio';
import HarvesterDisk from '../../mixins/harvester-disk'; import HarvesterDisk from '../../mixins/harvester-disk';
import Tags from '../../components/DiskTags'; import Tags from '../../components/DiskTags';
import { HCI } from '../../types'; import { HCI } from '../../types';
@ -30,7 +30,6 @@ export default {
BadgeState, BadgeState,
Banner, Banner,
RadioGroup, RadioGroup,
RadioButton,
ModalWithCard, ModalWithCard,
Tags, Tags,
}, },
@ -184,12 +183,11 @@ export default {
}, },
forceFormattedDisabled() { forceFormattedDisabled() {
const lastFormattedAt = this.blockDevice?.status?.deviceStatus?.fileSystem?.LastFormattedAt;
const fileSystem = this.blockDevice?.status?.deviceStatus?.fileSystem.type; const fileSystem = this.blockDevice?.status?.deviceStatus?.fileSystem.type;
const systems = ['ext4', 'XFS']; const systems = ['ext4', 'XFS'];
if (lastFormattedAt || this.blockDevice?.childParts?.length > 0) { if (this.blockDevice?.childParts?.length > 0) {
return true; return true;
} else if (systems.includes(fileSystem)) { } else if (systems.includes(fileSystem)) {
return false; return false;
@ -446,7 +444,7 @@ export default {
/> />
</div> </div>
<div <div
v-if="(value.isNew && isLonghornV1 && !isFormatted) || isCorrupted" v-if="(value.isNew && isLonghornV1) || isCorrupted"
class="col span-6" class="col span-6"
> >
<RadioGroup <RadioGroup
@ -459,15 +457,6 @@ export default {
:disabled="forceFormattedDisabled" :disabled="forceFormattedDisabled"
tooltip-key="harvester.host.disk.forceFormatted.toolTip" tooltip-key="harvester.host.disk.forceFormatted.toolTip"
> >
<template #1="{option, listeners}">
<RadioButton
:label="option.label"
:val="option.value"
:value="value.forceFormatted"
:disabled="forceFormattedDisabled && !value.forceFormatted"
v-on="listeners"
/>
</template>
</RadioGroup> </RadioGroup>
</div> </div>
<div <div

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