From 3dcc50980bc28b9eb3390273e25bea0df4475e3b Mon Sep 17 00:00:00 2001 From: Dominik Wombacher Date: Wed, 21 Jan 2026 08:30:55 +0100 Subject: [PATCH] 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 * feat(vmimport): improved version that uses 'store.watch' instead of polling Signed-off-by: Dominik Wombacher * 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 * 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 * feat(vmimport): add custom headers for HCI.VMIMPORT Signed-off-by: Dominik Wombacher * feat(vmimport): add custom headers for HCI.VMIMPORT_SOURCE_V Signed-off-by: Dominik Wombacher * feat(vmimport): add custom headers for HCI.VMIMPORT_SOURCE_O Signed-off-by: Dominik Wombacher * 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 * fix(vmimport): registerAddonSideNav improved and refactored Clear comments, code refactoring, additional checks and validations Signed-off-by: Dominik Wombacher * fix(vmimport): show correct status for virtualmachineimport Signed-off-by: Dominik Wombacher * feat(vmimport): custom list components with ns grouping Signed-off-by: Dominik Wombacher * 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 * 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 * 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 * 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 * chore(vmimport): vmware source default endpoint and datacenter renamed Signed-off-by: Dominik Wombacher * 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 * chore(vmimport): align tab names on openstacksource Signed-off-by: Dominik Wombacher * fix(vmimport): import { TextArea } from '@components/Form/TextArea' not found Signed-off-by: Dominik Wombacher * 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 * feat(vmimport): rename side-nav entry to 'Virtual Machine Imports' Signed-off-by: Dominik Wombacher * fix(vmimport): OvaSource Auth tab throws error selecting existing secret Signed-off-by: Dominik Wombacher * fix(vmimport): Add missing caCert input field to vmware source Signed-off-by: Dominik Wombacher * 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 * refactor(vmimport): Move vars into types files and reference them Signed-off-by: Dominik Wombacher * refactor(vmimport): Use 'currentProduct' value instead of hardcoded 'harvester' string Signed-off-by: Dominik Wombacher * refactor(vmimport): shorten 'selectedOption.raw' usage Signed-off-by: Dominik Wombacher * refactor(vmimport): Checks to make splice() usage more robust Signed-off-by: Dominik Wombacher * refactor(vmimport): re-use existing rfc1123 val function Move rfc1123 validation error message to l10n/en-us.yaml Signed-off-by: Dominik Wombacher * fix(vmimport): var name typo in vmi edit rfc1123 check Signed-off-by: Dominik Wombacher * feat(vmimport): vmi use 'FormValidation' and l10n for labels Signed-off-by: Dominik Wombacher * feat(vmimport): oss use 'FormValidation' and l10n for labels Signed-off-by: Dominik Wombacher * feat(vmimport): ovas use 'FormValidation' and l10n for labels Signed-off-by: Dominik Wombacher * feat(vmimport): vms use 'FormValidation' and l10n for labels Signed-off-by: Dominik Wombacher * refactor(vmimport): Display error message at the top of the page Signed-off-by: Volker Theile --------- Signed-off-by: Dominik Wombacher Signed-off-by: Volker Theile Co-authored-by: Volker Theile --- pkg/harvester/config/harvester-cluster.js | 151 ++++- pkg/harvester/config/table-headers.js | 99 +++ pkg/harvester/config/types.js | 12 + ...ration.harvesterhci.io.openstacksource.vue | 357 +++++++++++ .../migration.harvesterhci.io.ovasource.vue | 322 ++++++++++ ...n.harvesterhci.io.virtualmachineimport.vue | 570 ++++++++++++++++++ ...migration.harvesterhci.io.vmwaresource.vue | 302 ++++++++++ pkg/harvester/l10n/en-us.yaml | 86 +++ ...vesterhci.io.migration.openstacksource.vue | 60 ++ ...rhci.io.migration.virtualmachineimport.vue | 60 ++ ...harvesterhci.io.migration.vmwaresource.vue | 60 ++ pkg/harvester/types.ts | 5 + pkg/harvester/utils/dynamic-nav.js | 99 +++ 13 files changed, 2182 insertions(+), 1 deletion(-) create mode 100644 pkg/harvester/edit/migration.harvesterhci.io.openstacksource.vue create mode 100644 pkg/harvester/edit/migration.harvesterhci.io.ovasource.vue create mode 100644 pkg/harvester/edit/migration.harvesterhci.io.virtualmachineimport.vue create mode 100644 pkg/harvester/edit/migration.harvesterhci.io.vmwaresource.vue create mode 100644 pkg/harvester/list/harvesterhci.io.migration.openstacksource.vue create mode 100644 pkg/harvester/list/harvesterhci.io.migration.virtualmachineimport.vue create mode 100644 pkg/harvester/list/harvesterhci.io.migration.vmwaresource.vue create mode 100644 pkg/harvester/utils/dynamic-nav.js diff --git a/pkg/harvester/config/harvester-cluster.js b/pkg/harvester/config/harvester-cluster.js index 5931c28b..1af27b9d 100644 --- a/pkg/harvester/config/harvester-cluster.js +++ b/pkg/harvester/config/harvester-cluster.js @@ -35,8 +35,21 @@ import { SNAPSHOT_TARGET_VOLUME, IMAGE_VIRTUAL_SIZE, IMAGE_STORAGE_CLASS, - HARVESTER_DESCRIPTION + HARVESTER_DESCRIPTION, + VM_IMPORT_SOURCE_VM, + VM_IMPORT_SOURCE_CLUSTER, + VM_IMPORT_STATUS, + VM_IMPORT_SOURCE_V_DC, + VM_IMPORT_SOURCE_V_ENDPOINT, + VM_IMPORT_SOURCE_V_STATUS, + VM_IMPORT_SOURCE_O_REGION, + VM_IMPORT_SOURCE_O_ENDPOINT, + VM_IMPORT_SOURCE_O_STATUS, + VM_IMPORT_SOURCE_OVA_URL, + VM_IMPORT_SOURCE_OVA_STATUS, } from './table-headers'; +import { ADD_ONS } from './harvester-map'; +import { registerAddonSideNav } from '../utils/dynamic-nav'; const TEMPLATE = HCI.VM_VERSION; const MONITORING_GROUP = 'Monitoring & Logging::Monitoring'; @@ -195,6 +208,142 @@ export function init($plugin, store) { exact: false }); + // =========================================================================== + // VM Import Controller UI Flow + // =========================================================================== + // Define group (Hidden by default) + weightGroup('vmimport', 0, false); + + // VirtualMachineImport + headers(HCI.VMIMPORT, [ + STATE, + NAME_COL, + NAMESPACE_COL, + VM_IMPORT_SOURCE_VM, + VM_IMPORT_SOURCE_CLUSTER, + VM_IMPORT_STATUS, + AGE + ]); + configureType(HCI.VMIMPORT, { + resource: HCI.VMIMPORT, + resourceDetail: HCI.VMIMPORT, + resourceEdit: HCI.VMIMPORT, + location: { + name: `${ PRODUCT_NAME }-c-cluster-resource`, + params: { resource: HCI.VMIMPORT } + } + }); + virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav() + name: HCI.VMIMPORT, + labelKey: 'harvester.addons.vmImport.labels.vmimport', + group: 'vmimport', + namespaced: true, + route: { + name: `${ PRODUCT_NAME }-c-cluster-resource`, + params: { resource: HCI.VMIMPORT } + } + }); + + // Source: VMware + headers(HCI.VMIMPORT_SOURCE_V, [ + STATE, + NAME_COL, + VM_IMPORT_SOURCE_V_ENDPOINT, + VM_IMPORT_SOURCE_V_DC, + VM_IMPORT_SOURCE_V_STATUS, + AGE + ]); + configureType(HCI.VMIMPORT_SOURCE_V, { + resource: HCI.VMIMPORT_SOURCE_V, + resourceDetail: HCI.VMIMPORT_SOURCE_V, + resourceEdit: HCI.VMIMPORT_SOURCE_V, + location: { + name: `${ PRODUCT_NAME }-c-cluster-resource`, + params: { resource: HCI.VMIMPORT_SOURCE_V } + } + }); + virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav() + name: HCI.VMIMPORT_SOURCE_V, + labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare', + group: 'vmimport', + namespaced: true, + route: { + name: `${ PRODUCT_NAME }-c-cluster-resource`, + params: { resource: HCI.VMIMPORT_SOURCE_V } + } + }); + + // Source: OpenStack + headers(HCI.VMIMPORT_SOURCE_O, [ + STATE, + NAME_COL, + VM_IMPORT_SOURCE_O_ENDPOINT, + VM_IMPORT_SOURCE_O_REGION, + VM_IMPORT_SOURCE_O_STATUS, + AGE + ]); + configureType(HCI.VMIMPORT_SOURCE_O, { + resource: HCI.VMIMPORT_SOURCE_O, + resourceDetail: HCI.VMIMPORT_SOURCE_O, + resourceEdit: HCI.VMIMPORT_SOURCE_O, + location: { + name: `${ PRODUCT_NAME }-c-cluster-resource`, + params: { resource: HCI.VMIMPORT_SOURCE_O } + } + }); + virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav() + name: HCI.VMIMPORT_SOURCE_O, + labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack', + group: 'vmimport', + namespaced: true, + route: { + name: `${ PRODUCT_NAME }-c-cluster-resource`, + params: { resource: HCI.VMIMPORT_SOURCE_O } + } + }); + + // Source: OVA + headers(HCI.VMIMPORT_SOURCE_OVA, [ + STATE, + NAME_COL, + VM_IMPORT_SOURCE_OVA_URL, + VM_IMPORT_SOURCE_OVA_STATUS, + AGE + ]); + configureType(HCI.VMIMPORT_SOURCE_OVA, { + resource: HCI.VMIMPORT_SOURCE_OVA, + resourceDetail: HCI.VMIMPORT_SOURCE_OVA, + resourceEdit: HCI.VMIMPORT_SOURCE_OVA, + location: { + name: `${ PRODUCT_NAME }-c-cluster-resource`, + params: { resource: HCI.VMIMPORT_SOURCE_OVA } + } + }); + virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav() + name: HCI.VMIMPORT_SOURCE_OVA, + labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA', + group: 'vmimport', + namespaced: true, + route: { + name: `${ PRODUCT_NAME }-c-cluster-resource`, + params: { resource: HCI.VMIMPORT_SOURCE_OVA } + } + }); + + // Enable SideNav based on Addon Status + registerAddonSideNav(store, PRODUCT_NAME, { + addonName: ADD_ONS.VM_IMPORT_CONTROLLER, + resourceType: HCI.ADD_ONS, + navGroup: 'vmimport', + types: [ + HCI.VMIMPORT_SOURCE_V, + HCI.VMIMPORT_SOURCE_O, + HCI.VMIMPORT_SOURCE_OVA, + HCI.VMIMPORT + ] + }); + // =========================================================================== + basicType([HCI.VOLUME]); configureType(HCI.VOLUME, { location: { diff --git a/pkg/harvester/config/table-headers.js b/pkg/harvester/config/table-headers.js index b753aa5b..92c1c3de 100644 --- a/pkg/harvester/config/table-headers.js +++ b/pkg/harvester/config/table-headers.js @@ -131,3 +131,102 @@ export const 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', +}; diff --git a/pkg/harvester/config/types.js b/pkg/harvester/config/types.js index 39dd3212..472ec034 100644 --- a/pkg/harvester/config/types.js +++ b/pkg/harvester/config/types.js @@ -29,3 +29,15 @@ 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', +}; diff --git a/pkg/harvester/edit/migration.harvesterhci.io.openstacksource.vue b/pkg/harvester/edit/migration.harvesterhci.io.openstacksource.vue new file mode 100644 index 00000000..20bfe0f1 --- /dev/null +++ b/pkg/harvester/edit/migration.harvesterhci.io.openstacksource.vue @@ -0,0 +1,357 @@ + + + diff --git a/pkg/harvester/edit/migration.harvesterhci.io.ovasource.vue b/pkg/harvester/edit/migration.harvesterhci.io.ovasource.vue new file mode 100644 index 00000000..649edc1c --- /dev/null +++ b/pkg/harvester/edit/migration.harvesterhci.io.ovasource.vue @@ -0,0 +1,322 @@ + + + diff --git a/pkg/harvester/edit/migration.harvesterhci.io.virtualmachineimport.vue b/pkg/harvester/edit/migration.harvesterhci.io.virtualmachineimport.vue new file mode 100644 index 00000000..b8971143 --- /dev/null +++ b/pkg/harvester/edit/migration.harvesterhci.io.virtualmachineimport.vue @@ -0,0 +1,570 @@ + + + + + diff --git a/pkg/harvester/edit/migration.harvesterhci.io.vmwaresource.vue b/pkg/harvester/edit/migration.harvesterhci.io.vmwaresource.vue new file mode 100644 index 00000000..7129d2c2 --- /dev/null +++ b/pkg/harvester/edit/migration.harvesterhci.io.vmwaresource.vue @@ -0,0 +1,302 @@ + + + diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index 387fb283..8979af8d 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -20,6 +20,7 @@ nav: Monitoring: Monitoring Logging: Logging 'Monitoring and Logging': Monitoring and Logging + vmimport: Virtual Machine Imports resourceTable: groupBy: @@ -295,6 +296,17 @@ harvester: totalSnapshotQuota: Total Snapshot Quota storageClass: Storage Class restore: Restore + vmImportSourceVm: Source VM + vmImportSourceCluster: Source Cluster + vmImportStatus: Import Status + vmImportSourceVDatacenter: Datacenter + vmImportSourceVEndpoint: Endpoint + vmImportSourceVClusterStatus: Cluster Status + vmImportSourceORegion: Region + vmImportSourceOEndpoint: Endpoint + vmImportSourceOClusterStatus: Cluster Status + vmImportSourceOVAUrl: URL + vmImportSourceOVAStatus: Status tab: volume: Volumes network: Networks @@ -1566,10 +1578,84 @@ harvester: 'harvester-system/harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations. 'harvester-csi-driver-lvm': harvester-csi-driver-lvm is an add-on allowing users to create PVC through the LVM with local devices. 'descheduler': 'The virtual machine auto balance optimizes workload scheduling by evicting pods that are not optimally placed according to administrator-defined policies.' + vmImport: titles: basic: Basic + auth: Authentication pvc: Volume + networking: Network Mapping + advanced: Advanced + labels: + vmimport: Virtual Machine Import + vmimportSourceVMWare: Source VMWare + vmimportSourceOpenStack: Source OpenStack + vmimportSourceOVA: Source OVA + fields: + sourceProvider: Source Provider Type + sourceCluster: Source Cluster + vmName: VM Name + targetStorageClass: Target Storage Class + sourceNetwork: Source Network Name + destNetwork: Destination Network + interfaceModel: Interface Model + folder: Folder + diskBus: Default Disk Bus + defaultInterface: Default Network Interface + skipPreflight: Skip Preflight Checks + forcePowerOff: Force Power Off Source VM + username: Username + password: Password + caCert: CA Certificate (PEM) + selectSecret: Select Secret + createSecret: Create New Credentials + useSecret: Use Existing Secret + none: None (Public URL) + placeholders: + selectCluster: Select a cluster... + selectProviderFirst: Select a provider type first + matchSource: Must match the name in the source cluster + folderExample: e.g. /Datacenters/DC1/vm + caCert: "-----BEGIN CERTIFICATE----- ..." + options: + useDefault: Use Default + actions: + addNetwork: Add Network Mapping + remove: Remove + errors: + rfc1123: 'Invalid format. Name must be lowercase, alphanumeric, and cannot contain spaces (e.g. "my-vm-1"). If your Source VM name does not match this, you must rename it on the Source cluster first.' + networkMappingRequired: Every Network Mapping row must have a Source and Destination selected. + openstack: + fields: + endpoint: Identity Service Endpoint + region: Region + projectName: Project Name + domainName: Domain Name + retryCount: Upload Image Retry Count + retryDelay: Upload Image Retry Delay + placeholders: + endpoint: "e.g. https://devstack/identity" + region: e.g. RegionOne + projectName: e.g. admin + domainName: e.g. default + retryCount: "Default: 30" + retryDelay: "Default: 10" + vmware: + fields: + endpoint: vCenter Endpoint + datacenter: Datacenter + placeholders: + endpoint: "e.g. https://vscim/sdk" + datacenter: e.g. DC0 + tooltips: + datacenter: The exact name of the Datacenter object in vCenter + ova: + fields: + url: OVA URL + httpTimeout: HTTP Timeout + placeholders: + url: "e.g. https://download.example.com/images/my-vm.ova" + rancherVcluster: accessRancher: Access the Rancher Dashboard hostname: Hostname diff --git a/pkg/harvester/list/harvesterhci.io.migration.openstacksource.vue b/pkg/harvester/list/harvesterhci.io.migration.openstacksource.vue new file mode 100644 index 00000000..479d52c3 --- /dev/null +++ b/pkg/harvester/list/harvesterhci.io.migration.openstacksource.vue @@ -0,0 +1,60 @@ + + + diff --git a/pkg/harvester/list/harvesterhci.io.migration.virtualmachineimport.vue b/pkg/harvester/list/harvesterhci.io.migration.virtualmachineimport.vue new file mode 100644 index 00000000..10578f6c --- /dev/null +++ b/pkg/harvester/list/harvesterhci.io.migration.virtualmachineimport.vue @@ -0,0 +1,60 @@ + + + diff --git a/pkg/harvester/list/harvesterhci.io.migration.vmwaresource.vue b/pkg/harvester/list/harvesterhci.io.migration.vmwaresource.vue new file mode 100644 index 00000000..006ad222 --- /dev/null +++ b/pkg/harvester/list/harvesterhci.io.migration.vmwaresource.vue @@ -0,0 +1,60 @@ + + + diff --git a/pkg/harvester/types.ts b/pkg/harvester/types.ts index 6f7bd64b..d8c1aa21 100644 --- a/pkg/harvester/types.ts +++ b/pkg/harvester/types.ts @@ -56,6 +56,11 @@ export const HCI = { IP_POOL: 'loadbalancer.harvesterhci.io.ippool', HARVESTER_CONFIG: 'rke-machine-config.cattle.io.harvesterconfig', LVM_VOLUME_GROUP: 'harvesterhci.io.lvmvolumegroup', + VMIMPORT_SOURCE_V: 'migration.harvesterhci.io.vmwaresource', + VMIMPORT_SOURCE_O: 'migration.harvesterhci.io.openstacksource', + VMIMPORT_SOURCE_OVA: 'migration.harvesterhci.io.ovasource', + VMIMPORT: 'migration.harvesterhci.io.virtualmachineimport', + MIGRATION: 'migration.harvesterhci.io', }; export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot'; diff --git a/pkg/harvester/utils/dynamic-nav.js b/pkg/harvester/utils/dynamic-nav.js new file mode 100644 index 00000000..489b7a6f --- /dev/null +++ b/pkg/harvester/utils/dynamic-nav.js @@ -0,0 +1,99 @@ +/** + * Dynamically toggles SideNav entries based on the enabled status of a specific Addon. + * + * @param {Object} store - The Vuex store instance. + * @param {String} productName - The product name (e.g. 'harvester'). + * @param {Object} config - Configuration object. + * @param {String} config.addonName - The name of the addon to watch. + * @param {String} config.resourceType - The schema ID for addons. + * @param {String} config.navGroup - The group name in the side nav. + * @param {Array} config.types - Array of Resource IDs to show/hide. + */ +export function registerAddonSideNav(store, productName, { + addonName, resourceType, navGroup, types +}) { + if (typeof window === 'undefined') { + return; + } + + // Forces the SideNav component to re-render by toggling a dummy user preference. + // Necessary because the menu component does not automatically detect + // changes to the allowed types list. + const kickSideNav = () => { + const TRIGGER = 'ui.refresh.trigger'; + + store.dispatch('type-map/addFavorite', TRIGGER); + + // SideNav component seem to ignore rapid state changes. + // Wait 600ms to ensure the toggle event triggers a re-render. + setTimeout(() => { + store.dispatch('type-map/removeFavorite', TRIGGER); + }, 600); + }; + + // Adds or removes the resource IDs from the product visibility whitelist. + const setMenuVisibility = (visible) => { + if (visible) { + store.commit('type-map/basicType', { + product: productName, + group: navGroup, + types + }); + } else { + // Manually delete the keys from the state object to hide them. + const basicTypes = store.state['type-map'].basicTypes[productName]; + + if (basicTypes) { + types.forEach((t) => delete basicTypes[t]); + } + } + kickSideNav(); + }; + + // Start polling to check if the store is ready. + let attempts = 0; + const MAX_ATTEMPTS = 60; + + const waitForStore = setInterval(() => { + attempts++; + + try { + // Check if the Schema definitions are loaded. + const hasSchema = store.getters[`${ productName }/schemaFor`] && + store.getters[`${ productName }/schemaFor`](resourceType); + + // Check if the resource list data is fully loaded to prevent race conditions. + const hasData = store.getters[`${ productName }/haveAll`] && + store.getters[`${ productName }/haveAll`](resourceType); + + if (hasSchema && hasData) { + // Store is ready. Stop polling. + clearInterval(waitForStore); + + // Watch the specific addon resource for changes to its enabled status. + store.watch( + (state, getters) => { + const addons = getters[`${ productName }/all`](resourceType); + const addon = addons.find((a) => a.metadata.name === addonName); + + return addon?.spec?.enabled === true; + }, + (isEnabled) => { + setMenuVisibility(isEnabled); + }, + { immediate: true, deep: true } + ); + } else if (hasSchema && !hasData) { + // If the schema is ready but the data is missing, request the list from the API. + // Ensures the script does not wait indefinitely if the UI has not loaded the addons yet. + store.dispatch(`${ productName }/findAll`, { type: resourceType }); + } else if (attempts >= MAX_ATTEMPTS) { + // Stop checking if the store does not load within the timeout limit. + clearInterval(waitForStore); + } + } catch (e) { + // Ignore errors if the store module is not yet registered and wait for the next attempt. + if (attempts >= MAX_ATTEMPTS) clearInterval(waitForStore); + } + }, 1000); +}