From 71e2fba1879097634e407e8a933aef2376e316ba Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 10 Mar 2026 12:09:11 -0400 Subject: [PATCH] ImportAKSProjects: Extract hook, add tests, refactor UI Signed-off-by: Evangelos Skopelitis --- .../aks-desktop/locales/cs/translation.json | 25 +- .../aks-desktop/locales/de/translation.json | 17 +- .../aks-desktop/locales/en/translation.json | 17 +- .../aks-desktop/locales/es/translation.json | 21 +- .../aks-desktop/locales/fr/translation.json | 21 +- .../aks-desktop/locales/hu/translation.json | 17 +- .../aks-desktop/locales/id/translation.json | 13 +- .../aks-desktop/locales/it/translation.json | 21 +- .../aks-desktop/locales/ja/translation.json | 13 +- .../aks-desktop/locales/ko/translation.json | 13 +- .../aks-desktop/locales/nl/translation.json | 17 +- .../aks-desktop/locales/pl/translation.json | 25 +- .../locales/pt-BR/translation.json | 21 +- .../locales/pt-PT/translation.json | 21 +- .../aks-desktop/locales/ru/translation.json | 25 +- .../aks-desktop/locales/sv/translation.json | 17 +- .../aks-desktop/locales/tr/translation.json | 17 +- .../locales/zh-Hans/translation.json | 13 +- .../locales/zh-Hant/translation.json | 13 +- .../ImportAKSProjects.test.tsx | 524 ++++++-------- .../ImportAKSProjects/ImportAKSProjects.tsx | 675 ++++-------------- .../hooks/useImportAKSProjects.test.ts | 390 ++++++++++ .../hooks/useImportAKSProjects.ts | 333 +++++++++ 23 files changed, 1174 insertions(+), 1095 deletions(-) create mode 100644 plugins/aks-desktop/src/components/ImportAKSProjects/hooks/useImportAKSProjects.test.ts create mode 100644 plugins/aks-desktop/src/components/ImportAKSProjects/hooks/useImportAKSProjects.ts diff --git a/plugins/aks-desktop/locales/cs/translation.json b/plugins/aks-desktop/locales/cs/translation.json index 10c9a0114..040a1d988 100644 --- a/plugins/aks-desktop/locales/cs/translation.json +++ b/plugins/aks-desktop/locales/cs/translation.json @@ -532,20 +532,14 @@ "Confirm & Import": "", "Please select at least one namespace to import": "", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_one": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_few": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_many": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "", "Failed to merge cluster: {{message}}": "", - "Converting {{name}} to AKS project (this may take a moment)...": "", "Failed to convert namespace: {{message}}": "", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "", "Project '{{name}}' successfully imported": "", "Namespace '{{name}}' converted and imported as project": "", - "Successfully merged {{count}} cluster(s)_one": "", - "Successfully merged {{count}} cluster(s)_few": "", - "Successfully merged {{count}} cluster(s)_many": "", - "Successfully merged {{count}} cluster(s)_other": "", + "Successfully imported from {{count}} cluster(s)_one": "", + "Successfully imported from {{count}} cluster(s)_few": "", + "Successfully imported from {{count}} cluster(s)_many": "", + "Successfully imported from {{count}} cluster(s)_other": "", "with {{count}} project(s)_one": "", "with {{count}} project(s)_few": "", "with {{count}} project(s)_many": "", @@ -554,16 +548,9 @@ "{{count}} failed._few": "", "{{count}} failed._many": "", "{{count}} failed._other": "", - "Failed to import any projects. See details below.": "", + "Failed to import any projects.": "", "Import AKS Projects": "", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "", - "Select Namespaces to Import": "", - "{{count}} selected_one": "", - "{{count}} selected_few": "", - "{{count}} selected_many": "", - "{{count}} selected_other": "", - "Select All": "", - "Deselect All": "", + "Browse and import existing AKS Projects": "", "AKS Managed": "", "Regular": "", "AKS Project?": "", diff --git a/plugins/aks-desktop/locales/de/translation.json b/plugins/aks-desktop/locales/de/translation.json index 8dfbed4dd..9c075d8a4 100644 --- a/plugins/aks-desktop/locales/de/translation.json +++ b/plugins/aks-desktop/locales/de/translation.json @@ -520,28 +520,19 @@ "Confirm & Import": "", "Please select at least one namespace to import": "", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_one": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "", "Failed to merge cluster: {{message}}": "", - "Converting {{name}} to AKS project (this may take a moment)...": "", "Failed to convert namespace: {{message}}": "", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "", "Project '{{name}}' successfully imported": "", "Namespace '{{name}}' converted and imported as project": "", - "Successfully merged {{count}} cluster(s)_one": "", - "Successfully merged {{count}} cluster(s)_other": "", + "Successfully imported from {{count}} cluster(s)_one": "", + "Successfully imported from {{count}} cluster(s)_other": "", "with {{count}} project(s)_one": "", "with {{count}} project(s)_other": "", "{{count}} failed._one": "", "{{count}} failed._other": "", - "Failed to import any projects. See details below.": "", + "Failed to import any projects.": "", "Import AKS Projects": "", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "", - "Select Namespaces to Import": "", - "{{count}} selected_one": "", - "{{count}} selected_other": "", - "Select All": "", - "Deselect All": "", + "Browse and import existing AKS Projects": "", "AKS Managed": "", "Regular": "", "AKS Project?": "", diff --git a/plugins/aks-desktop/locales/en/translation.json b/plugins/aks-desktop/locales/en/translation.json index 18733d00f..883a2cd71 100644 --- a/plugins/aks-desktop/locales/en/translation.json +++ b/plugins/aks-desktop/locales/en/translation.json @@ -520,28 +520,19 @@ "Confirm & Import": "Confirm & Import", "Please select at least one namespace to import": "Please select at least one namespace to import", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_one": "Merging cluster {{clusterName}} ({{count}} namespace(s))", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "Merging cluster {{clusterName}} ({{count}} namespace(s))", "Failed to merge cluster: {{message}}": "Failed to merge cluster: {{message}}", - "Converting {{name}} to AKS project (this may take a moment)...": "Converting {{name}} to AKS project (this may take a moment)...", "Failed to convert namespace: {{message}}": "Failed to convert namespace: {{message}}", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}", "Project '{{name}}' successfully imported": "Project '{{name}}' successfully imported", "Namespace '{{name}}' converted and imported as project": "Namespace '{{name}}' converted and imported as project", - "Successfully merged {{count}} cluster(s)_one": "Successfully merged {{count}} cluster(s)", - "Successfully merged {{count}} cluster(s)_other": "Successfully merged {{count}} cluster(s)", + "Successfully imported from {{count}} cluster(s)_one": "Successfully imported from {{count}} cluster(s)", + "Successfully imported from {{count}} cluster(s)_other": "Successfully imported from {{count}} cluster(s)", "with {{count}} project(s)_one": "with {{count}} project(s)", "with {{count}} project(s)_other": "with {{count}} project(s)", "{{count}} failed._one": "{{count}} failed.", "{{count}} failed._other": "{{count}} failed.", - "Failed to import any projects. See details below.": "Failed to import any projects. See details below.", + "Failed to import any projects.": "Failed to import any projects.", "Import AKS Projects": "Import AKS Projects", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.", - "Select Namespaces to Import": "Select Namespaces to Import", - "{{count}} selected_one": "{{count}} selected", - "{{count}} selected_other": "{{count}} selected", - "Select All": "Select All", - "Deselect All": "Deselect All", + "Browse and import existing AKS Projects": "Browse and import existing AKS Projects", "AKS Managed": "AKS Managed", "Regular": "Regular", "AKS Project?": "AKS Project?", diff --git a/plugins/aks-desktop/locales/es/translation.json b/plugins/aks-desktop/locales/es/translation.json index 47041d161..c69151ac1 100644 --- a/plugins/aks-desktop/locales/es/translation.json +++ b/plugins/aks-desktop/locales/es/translation.json @@ -526,33 +526,22 @@ "Confirm & Import": "", "Please select at least one namespace to import": "", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_one": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_many": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "", "Failed to merge cluster: {{message}}": "", - "Converting {{name}} to AKS project (this may take a moment)...": "", "Failed to convert namespace: {{message}}": "", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "", "Project '{{name}}' successfully imported": "", "Namespace '{{name}}' converted and imported as project": "", - "Successfully merged {{count}} cluster(s)_one": "", - "Successfully merged {{count}} cluster(s)_many": "", - "Successfully merged {{count}} cluster(s)_other": "", + "Successfully imported from {{count}} cluster(s)_one": "", + "Successfully imported from {{count}} cluster(s)_many": "", + "Successfully imported from {{count}} cluster(s)_other": "", "with {{count}} project(s)_one": "", "with {{count}} project(s)_many": "", "with {{count}} project(s)_other": "", "{{count}} failed._one": "", "{{count}} failed._many": "", "{{count}} failed._other": "", - "Failed to import any projects. See details below.": "", + "Failed to import any projects.": "", "Import AKS Projects": "", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "", - "Select Namespaces to Import": "", - "{{count}} selected_one": "", - "{{count}} selected_many": "", - "{{count}} selected_other": "", - "Select All": "", - "Deselect All": "", + "Browse and import existing AKS Projects": "", "AKS Managed": "", "Regular": "", "AKS Project?": "", diff --git a/plugins/aks-desktop/locales/fr/translation.json b/plugins/aks-desktop/locales/fr/translation.json index 47041d161..c69151ac1 100644 --- a/plugins/aks-desktop/locales/fr/translation.json +++ b/plugins/aks-desktop/locales/fr/translation.json @@ -526,33 +526,22 @@ "Confirm & Import": "", "Please select at least one namespace to import": "", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_one": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_many": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "", "Failed to merge cluster: {{message}}": "", - "Converting {{name}} to AKS project (this may take a moment)...": "", "Failed to convert namespace: {{message}}": "", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "", "Project '{{name}}' successfully imported": "", "Namespace '{{name}}' converted and imported as project": "", - "Successfully merged {{count}} cluster(s)_one": "", - "Successfully merged {{count}} cluster(s)_many": "", - "Successfully merged {{count}} cluster(s)_other": "", + "Successfully imported from {{count}} cluster(s)_one": "", + "Successfully imported from {{count}} cluster(s)_many": "", + "Successfully imported from {{count}} cluster(s)_other": "", "with {{count}} project(s)_one": "", "with {{count}} project(s)_many": "", "with {{count}} project(s)_other": "", "{{count}} failed._one": "", "{{count}} failed._many": "", "{{count}} failed._other": "", - "Failed to import any projects. See details below.": "", + "Failed to import any projects.": "", "Import AKS Projects": "", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "", - "Select Namespaces to Import": "", - "{{count}} selected_one": "", - "{{count}} selected_many": "", - "{{count}} selected_other": "", - "Select All": "", - "Deselect All": "", + "Browse and import existing AKS Projects": "", "AKS Managed": "", "Regular": "", "AKS Project?": "", diff --git a/plugins/aks-desktop/locales/hu/translation.json b/plugins/aks-desktop/locales/hu/translation.json index 8dfbed4dd..9c075d8a4 100644 --- a/plugins/aks-desktop/locales/hu/translation.json +++ b/plugins/aks-desktop/locales/hu/translation.json @@ -520,28 +520,19 @@ "Confirm & Import": "", "Please select at least one namespace to import": "", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_one": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "", "Failed to merge cluster: {{message}}": "", - "Converting {{name}} to AKS project (this may take a moment)...": "", "Failed to convert namespace: {{message}}": "", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "", "Project '{{name}}' successfully imported": "", "Namespace '{{name}}' converted and imported as project": "", - "Successfully merged {{count}} cluster(s)_one": "", - "Successfully merged {{count}} cluster(s)_other": "", + "Successfully imported from {{count}} cluster(s)_one": "", + "Successfully imported from {{count}} cluster(s)_other": "", "with {{count}} project(s)_one": "", "with {{count}} project(s)_other": "", "{{count}} failed._one": "", "{{count}} failed._other": "", - "Failed to import any projects. See details below.": "", + "Failed to import any projects.": "", "Import AKS Projects": "", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "", - "Select Namespaces to Import": "", - "{{count}} selected_one": "", - "{{count}} selected_other": "", - "Select All": "", - "Deselect All": "", + "Browse and import existing AKS Projects": "", "AKS Managed": "", "Regular": "", "AKS Project?": "", diff --git a/plugins/aks-desktop/locales/id/translation.json b/plugins/aks-desktop/locales/id/translation.json index e08e8d42c..2e38b140d 100644 --- a/plugins/aks-desktop/locales/id/translation.json +++ b/plugins/aks-desktop/locales/id/translation.json @@ -514,23 +514,16 @@ "Confirm & Import": "", "Please select at least one namespace to import": "", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "", "Failed to merge cluster: {{message}}": "", - "Converting {{name}} to AKS project (this may take a moment)...": "", "Failed to convert namespace: {{message}}": "", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "", "Project '{{name}}' successfully imported": "", "Namespace '{{name}}' converted and imported as project": "", - "Successfully merged {{count}} cluster(s)_other": "", + "Successfully imported from {{count}} cluster(s)_other": "", "with {{count}} project(s)_other": "", "{{count}} failed._other": "", - "Failed to import any projects. See details below.": "", + "Failed to import any projects.": "", "Import AKS Projects": "", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "", - "Select Namespaces to Import": "", - "{{count}} selected_other": "", - "Select All": "", - "Deselect All": "", + "Browse and import existing AKS Projects": "", "AKS Managed": "", "Regular": "", "AKS Project?": "", diff --git a/plugins/aks-desktop/locales/it/translation.json b/plugins/aks-desktop/locales/it/translation.json index 47041d161..c69151ac1 100644 --- a/plugins/aks-desktop/locales/it/translation.json +++ b/plugins/aks-desktop/locales/it/translation.json @@ -526,33 +526,22 @@ "Confirm & Import": "", "Please select at least one namespace to import": "", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_one": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_many": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "", "Failed to merge cluster: {{message}}": "", - "Converting {{name}} to AKS project (this may take a moment)...": "", "Failed to convert namespace: {{message}}": "", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "", "Project '{{name}}' successfully imported": "", "Namespace '{{name}}' converted and imported as project": "", - "Successfully merged {{count}} cluster(s)_one": "", - "Successfully merged {{count}} cluster(s)_many": "", - "Successfully merged {{count}} cluster(s)_other": "", + "Successfully imported from {{count}} cluster(s)_one": "", + "Successfully imported from {{count}} cluster(s)_many": "", + "Successfully imported from {{count}} cluster(s)_other": "", "with {{count}} project(s)_one": "", "with {{count}} project(s)_many": "", "with {{count}} project(s)_other": "", "{{count}} failed._one": "", "{{count}} failed._many": "", "{{count}} failed._other": "", - "Failed to import any projects. See details below.": "", + "Failed to import any projects.": "", "Import AKS Projects": "", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "", - "Select Namespaces to Import": "", - "{{count}} selected_one": "", - "{{count}} selected_many": "", - "{{count}} selected_other": "", - "Select All": "", - "Deselect All": "", + "Browse and import existing AKS Projects": "", "AKS Managed": "", "Regular": "", "AKS Project?": "", diff --git a/plugins/aks-desktop/locales/ja/translation.json b/plugins/aks-desktop/locales/ja/translation.json index e08e8d42c..2e38b140d 100644 --- a/plugins/aks-desktop/locales/ja/translation.json +++ b/plugins/aks-desktop/locales/ja/translation.json @@ -514,23 +514,16 @@ "Confirm & Import": "", "Please select at least one namespace to import": "", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "", "Failed to merge cluster: {{message}}": "", - "Converting {{name}} to AKS project (this may take a moment)...": "", "Failed to convert namespace: {{message}}": "", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "", "Project '{{name}}' successfully imported": "", "Namespace '{{name}}' converted and imported as project": "", - "Successfully merged {{count}} cluster(s)_other": "", + "Successfully imported from {{count}} cluster(s)_other": "", "with {{count}} project(s)_other": "", "{{count}} failed._other": "", - "Failed to import any projects. See details below.": "", + "Failed to import any projects.": "", "Import AKS Projects": "", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "", - "Select Namespaces to Import": "", - "{{count}} selected_other": "", - "Select All": "", - "Deselect All": "", + "Browse and import existing AKS Projects": "", "AKS Managed": "", "Regular": "", "AKS Project?": "", diff --git a/plugins/aks-desktop/locales/ko/translation.json b/plugins/aks-desktop/locales/ko/translation.json index e08e8d42c..2e38b140d 100644 --- a/plugins/aks-desktop/locales/ko/translation.json +++ b/plugins/aks-desktop/locales/ko/translation.json @@ -514,23 +514,16 @@ "Confirm & Import": "", "Please select at least one namespace to import": "", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "", "Failed to merge cluster: {{message}}": "", - "Converting {{name}} to AKS project (this may take a moment)...": "", "Failed to convert namespace: {{message}}": "", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "", "Project '{{name}}' successfully imported": "", "Namespace '{{name}}' converted and imported as project": "", - "Successfully merged {{count}} cluster(s)_other": "", + "Successfully imported from {{count}} cluster(s)_other": "", "with {{count}} project(s)_other": "", "{{count}} failed._other": "", - "Failed to import any projects. See details below.": "", + "Failed to import any projects.": "", "Import AKS Projects": "", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "", - "Select Namespaces to Import": "", - "{{count}} selected_other": "", - "Select All": "", - "Deselect All": "", + "Browse and import existing AKS Projects": "", "AKS Managed": "", "Regular": "", "AKS Project?": "", diff --git a/plugins/aks-desktop/locales/nl/translation.json b/plugins/aks-desktop/locales/nl/translation.json index 8dfbed4dd..9c075d8a4 100644 --- a/plugins/aks-desktop/locales/nl/translation.json +++ b/plugins/aks-desktop/locales/nl/translation.json @@ -520,28 +520,19 @@ "Confirm & Import": "", "Please select at least one namespace to import": "", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_one": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "", "Failed to merge cluster: {{message}}": "", - "Converting {{name}} to AKS project (this may take a moment)...": "", "Failed to convert namespace: {{message}}": "", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "", "Project '{{name}}' successfully imported": "", "Namespace '{{name}}' converted and imported as project": "", - "Successfully merged {{count}} cluster(s)_one": "", - "Successfully merged {{count}} cluster(s)_other": "", + "Successfully imported from {{count}} cluster(s)_one": "", + "Successfully imported from {{count}} cluster(s)_other": "", "with {{count}} project(s)_one": "", "with {{count}} project(s)_other": "", "{{count}} failed._one": "", "{{count}} failed._other": "", - "Failed to import any projects. See details below.": "", + "Failed to import any projects.": "", "Import AKS Projects": "", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "", - "Select Namespaces to Import": "", - "{{count}} selected_one": "", - "{{count}} selected_other": "", - "Select All": "", - "Deselect All": "", + "Browse and import existing AKS Projects": "", "AKS Managed": "", "Regular": "", "AKS Project?": "", diff --git a/plugins/aks-desktop/locales/pl/translation.json b/plugins/aks-desktop/locales/pl/translation.json index 10c9a0114..040a1d988 100644 --- a/plugins/aks-desktop/locales/pl/translation.json +++ b/plugins/aks-desktop/locales/pl/translation.json @@ -532,20 +532,14 @@ "Confirm & Import": "", "Please select at least one namespace to import": "", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_one": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_few": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_many": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "", "Failed to merge cluster: {{message}}": "", - "Converting {{name}} to AKS project (this may take a moment)...": "", "Failed to convert namespace: {{message}}": "", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "", "Project '{{name}}' successfully imported": "", "Namespace '{{name}}' converted and imported as project": "", - "Successfully merged {{count}} cluster(s)_one": "", - "Successfully merged {{count}} cluster(s)_few": "", - "Successfully merged {{count}} cluster(s)_many": "", - "Successfully merged {{count}} cluster(s)_other": "", + "Successfully imported from {{count}} cluster(s)_one": "", + "Successfully imported from {{count}} cluster(s)_few": "", + "Successfully imported from {{count}} cluster(s)_many": "", + "Successfully imported from {{count}} cluster(s)_other": "", "with {{count}} project(s)_one": "", "with {{count}} project(s)_few": "", "with {{count}} project(s)_many": "", @@ -554,16 +548,9 @@ "{{count}} failed._few": "", "{{count}} failed._many": "", "{{count}} failed._other": "", - "Failed to import any projects. See details below.": "", + "Failed to import any projects.": "", "Import AKS Projects": "", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "", - "Select Namespaces to Import": "", - "{{count}} selected_one": "", - "{{count}} selected_few": "", - "{{count}} selected_many": "", - "{{count}} selected_other": "", - "Select All": "", - "Deselect All": "", + "Browse and import existing AKS Projects": "", "AKS Managed": "", "Regular": "", "AKS Project?": "", diff --git a/plugins/aks-desktop/locales/pt-BR/translation.json b/plugins/aks-desktop/locales/pt-BR/translation.json index 47041d161..c69151ac1 100644 --- a/plugins/aks-desktop/locales/pt-BR/translation.json +++ b/plugins/aks-desktop/locales/pt-BR/translation.json @@ -526,33 +526,22 @@ "Confirm & Import": "", "Please select at least one namespace to import": "", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_one": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_many": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "", "Failed to merge cluster: {{message}}": "", - "Converting {{name}} to AKS project (this may take a moment)...": "", "Failed to convert namespace: {{message}}": "", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "", "Project '{{name}}' successfully imported": "", "Namespace '{{name}}' converted and imported as project": "", - "Successfully merged {{count}} cluster(s)_one": "", - "Successfully merged {{count}} cluster(s)_many": "", - "Successfully merged {{count}} cluster(s)_other": "", + "Successfully imported from {{count}} cluster(s)_one": "", + "Successfully imported from {{count}} cluster(s)_many": "", + "Successfully imported from {{count}} cluster(s)_other": "", "with {{count}} project(s)_one": "", "with {{count}} project(s)_many": "", "with {{count}} project(s)_other": "", "{{count}} failed._one": "", "{{count}} failed._many": "", "{{count}} failed._other": "", - "Failed to import any projects. See details below.": "", + "Failed to import any projects.": "", "Import AKS Projects": "", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "", - "Select Namespaces to Import": "", - "{{count}} selected_one": "", - "{{count}} selected_many": "", - "{{count}} selected_other": "", - "Select All": "", - "Deselect All": "", + "Browse and import existing AKS Projects": "", "AKS Managed": "", "Regular": "", "AKS Project?": "", diff --git a/plugins/aks-desktop/locales/pt-PT/translation.json b/plugins/aks-desktop/locales/pt-PT/translation.json index 47041d161..c69151ac1 100644 --- a/plugins/aks-desktop/locales/pt-PT/translation.json +++ b/plugins/aks-desktop/locales/pt-PT/translation.json @@ -526,33 +526,22 @@ "Confirm & Import": "", "Please select at least one namespace to import": "", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_one": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_many": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "", "Failed to merge cluster: {{message}}": "", - "Converting {{name}} to AKS project (this may take a moment)...": "", "Failed to convert namespace: {{message}}": "", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "", "Project '{{name}}' successfully imported": "", "Namespace '{{name}}' converted and imported as project": "", - "Successfully merged {{count}} cluster(s)_one": "", - "Successfully merged {{count}} cluster(s)_many": "", - "Successfully merged {{count}} cluster(s)_other": "", + "Successfully imported from {{count}} cluster(s)_one": "", + "Successfully imported from {{count}} cluster(s)_many": "", + "Successfully imported from {{count}} cluster(s)_other": "", "with {{count}} project(s)_one": "", "with {{count}} project(s)_many": "", "with {{count}} project(s)_other": "", "{{count}} failed._one": "", "{{count}} failed._many": "", "{{count}} failed._other": "", - "Failed to import any projects. See details below.": "", + "Failed to import any projects.": "", "Import AKS Projects": "", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "", - "Select Namespaces to Import": "", - "{{count}} selected_one": "", - "{{count}} selected_many": "", - "{{count}} selected_other": "", - "Select All": "", - "Deselect All": "", + "Browse and import existing AKS Projects": "", "AKS Managed": "", "Regular": "", "AKS Project?": "", diff --git a/plugins/aks-desktop/locales/ru/translation.json b/plugins/aks-desktop/locales/ru/translation.json index 10c9a0114..040a1d988 100644 --- a/plugins/aks-desktop/locales/ru/translation.json +++ b/plugins/aks-desktop/locales/ru/translation.json @@ -532,20 +532,14 @@ "Confirm & Import": "", "Please select at least one namespace to import": "", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_one": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_few": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_many": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "", "Failed to merge cluster: {{message}}": "", - "Converting {{name}} to AKS project (this may take a moment)...": "", "Failed to convert namespace: {{message}}": "", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "", "Project '{{name}}' successfully imported": "", "Namespace '{{name}}' converted and imported as project": "", - "Successfully merged {{count}} cluster(s)_one": "", - "Successfully merged {{count}} cluster(s)_few": "", - "Successfully merged {{count}} cluster(s)_many": "", - "Successfully merged {{count}} cluster(s)_other": "", + "Successfully imported from {{count}} cluster(s)_one": "", + "Successfully imported from {{count}} cluster(s)_few": "", + "Successfully imported from {{count}} cluster(s)_many": "", + "Successfully imported from {{count}} cluster(s)_other": "", "with {{count}} project(s)_one": "", "with {{count}} project(s)_few": "", "with {{count}} project(s)_many": "", @@ -554,16 +548,9 @@ "{{count}} failed._few": "", "{{count}} failed._many": "", "{{count}} failed._other": "", - "Failed to import any projects. See details below.": "", + "Failed to import any projects.": "", "Import AKS Projects": "", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "", - "Select Namespaces to Import": "", - "{{count}} selected_one": "", - "{{count}} selected_few": "", - "{{count}} selected_many": "", - "{{count}} selected_other": "", - "Select All": "", - "Deselect All": "", + "Browse and import existing AKS Projects": "", "AKS Managed": "", "Regular": "", "AKS Project?": "", diff --git a/plugins/aks-desktop/locales/sv/translation.json b/plugins/aks-desktop/locales/sv/translation.json index 8dfbed4dd..9c075d8a4 100644 --- a/plugins/aks-desktop/locales/sv/translation.json +++ b/plugins/aks-desktop/locales/sv/translation.json @@ -520,28 +520,19 @@ "Confirm & Import": "", "Please select at least one namespace to import": "", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_one": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "", "Failed to merge cluster: {{message}}": "", - "Converting {{name}} to AKS project (this may take a moment)...": "", "Failed to convert namespace: {{message}}": "", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "", "Project '{{name}}' successfully imported": "", "Namespace '{{name}}' converted and imported as project": "", - "Successfully merged {{count}} cluster(s)_one": "", - "Successfully merged {{count}} cluster(s)_other": "", + "Successfully imported from {{count}} cluster(s)_one": "", + "Successfully imported from {{count}} cluster(s)_other": "", "with {{count}} project(s)_one": "", "with {{count}} project(s)_other": "", "{{count}} failed._one": "", "{{count}} failed._other": "", - "Failed to import any projects. See details below.": "", + "Failed to import any projects.": "", "Import AKS Projects": "", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "", - "Select Namespaces to Import": "", - "{{count}} selected_one": "", - "{{count}} selected_other": "", - "Select All": "", - "Deselect All": "", + "Browse and import existing AKS Projects": "", "AKS Managed": "", "Regular": "", "AKS Project?": "", diff --git a/plugins/aks-desktop/locales/tr/translation.json b/plugins/aks-desktop/locales/tr/translation.json index 8dfbed4dd..9c075d8a4 100644 --- a/plugins/aks-desktop/locales/tr/translation.json +++ b/plugins/aks-desktop/locales/tr/translation.json @@ -520,28 +520,19 @@ "Confirm & Import": "", "Please select at least one namespace to import": "", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_one": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "", "Failed to merge cluster: {{message}}": "", - "Converting {{name}} to AKS project (this may take a moment)...": "", "Failed to convert namespace: {{message}}": "", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "", "Project '{{name}}' successfully imported": "", "Namespace '{{name}}' converted and imported as project": "", - "Successfully merged {{count}} cluster(s)_one": "", - "Successfully merged {{count}} cluster(s)_other": "", + "Successfully imported from {{count}} cluster(s)_one": "", + "Successfully imported from {{count}} cluster(s)_other": "", "with {{count}} project(s)_one": "", "with {{count}} project(s)_other": "", "{{count}} failed._one": "", "{{count}} failed._other": "", - "Failed to import any projects. See details below.": "", + "Failed to import any projects.": "", "Import AKS Projects": "", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "", - "Select Namespaces to Import": "", - "{{count}} selected_one": "", - "{{count}} selected_other": "", - "Select All": "", - "Deselect All": "", + "Browse and import existing AKS Projects": "", "AKS Managed": "", "Regular": "", "AKS Project?": "", diff --git a/plugins/aks-desktop/locales/zh-Hans/translation.json b/plugins/aks-desktop/locales/zh-Hans/translation.json index e08e8d42c..2e38b140d 100644 --- a/plugins/aks-desktop/locales/zh-Hans/translation.json +++ b/plugins/aks-desktop/locales/zh-Hans/translation.json @@ -514,23 +514,16 @@ "Confirm & Import": "", "Please select at least one namespace to import": "", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "", "Failed to merge cluster: {{message}}": "", - "Converting {{name}} to AKS project (this may take a moment)...": "", "Failed to convert namespace: {{message}}": "", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "", "Project '{{name}}' successfully imported": "", "Namespace '{{name}}' converted and imported as project": "", - "Successfully merged {{count}} cluster(s)_other": "", + "Successfully imported from {{count}} cluster(s)_other": "", "with {{count}} project(s)_other": "", "{{count}} failed._other": "", - "Failed to import any projects. See details below.": "", + "Failed to import any projects.": "", "Import AKS Projects": "", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "", - "Select Namespaces to Import": "", - "{{count}} selected_other": "", - "Select All": "", - "Deselect All": "", + "Browse and import existing AKS Projects": "", "AKS Managed": "", "Regular": "", "AKS Project?": "", diff --git a/plugins/aks-desktop/locales/zh-Hant/translation.json b/plugins/aks-desktop/locales/zh-Hant/translation.json index e08e8d42c..2e38b140d 100644 --- a/plugins/aks-desktop/locales/zh-Hant/translation.json +++ b/plugins/aks-desktop/locales/zh-Hant/translation.json @@ -514,23 +514,16 @@ "Confirm & Import": "", "Please select at least one namespace to import": "", "Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.": "", - "Merging cluster {{clusterName}} ({{count}} namespace(s))_other": "", "Failed to merge cluster: {{message}}": "", - "Converting {{name}} to AKS project (this may take a moment)...": "", "Failed to convert namespace: {{message}}": "", - "Importing {{current}} of {{total}}: {{name}} from {{clusterName}}": "", "Project '{{name}}' successfully imported": "", "Namespace '{{name}}' converted and imported as project": "", - "Successfully merged {{count}} cluster(s)_other": "", + "Successfully imported from {{count}} cluster(s)_other": "", "with {{count}} project(s)_other": "", "{{count}} failed._other": "", - "Failed to import any projects. See details below.": "", + "Failed to import any projects.": "", "Import AKS Projects": "", - "Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.": "", - "Select Namespaces to Import": "", - "{{count}} selected_other": "", - "Select All": "", - "Deselect All": "", + "Browse and import existing AKS Projects": "", "AKS Managed": "", "Regular": "", "AKS Project?": "", diff --git a/plugins/aks-desktop/src/components/ImportAKSProjects/ImportAKSProjects.test.tsx b/plugins/aks-desktop/src/components/ImportAKSProjects/ImportAKSProjects.test.tsx index 3e9298ac5..0067d36c0 100644 --- a/plugins/aks-desktop/src/components/ImportAKSProjects/ImportAKSProjects.test.tsx +++ b/plugins/aks-desktop/src/components/ImportAKSProjects/ImportAKSProjects.test.tsx @@ -3,16 +3,13 @@ // @vitest-environment jsdom import '@testing-library/jest-dom/vitest'; -import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; -// --- Mocks (must be defined before imports that use them) --- -const mockPush = vi.fn(); -const mockReplace = vi.fn(); -vi.mock('react-router-dom', () => ({ - useHistory: () => ({ push: mockPush, replace: mockReplace }), -})); +// --------------------------------------------------------------------------- +// Mocks +// --------------------------------------------------------------------------- vi.mock('@kinvolk/headlamp-plugin/lib', () => { const t = (key: string, params?: Record) => { @@ -35,124 +32,156 @@ vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({ {children} ), - Table: ({ data, columns, loading }: any) => ( - - - - {columns.map((c: any, i: number) => ( - - ))} - - - - {data.map((item: any, i: number) => ( - - {columns.map((col: any, j: number) => ( - + // Minimal Table mock: renders rows and exposes renderRowSelectionToolbar via a toolbar slot + Table: ({ data, columns, loading, renderRowSelectionToolbar, enableRowSelection }: any) => { + const [selected, setSelected] = React.useState>(new Set()); + + const fakeTable = { + getSelectedRowModel: () => ({ + rows: Array.from(selected).map(i => ({ original: data[i] })), + }), + }; + + return ( +
+
+ {renderRowSelectionToolbar && renderRowSelectionToolbar({ table: fakeTable })} +
+
{c.header}
- {col.Cell ? col.Cell({ row: { original: item } }) : String(col.accessorFn(item))} -
+ + + {enableRowSelection && + ))} + + + + {(data ?? []).map((item: any, i: number) => ( + + {enableRowSelection && ( + + )} + {columns.map((col: any, j: number) => ( + + ))} + ))} - - ))} - -
} + {columns.map((c: any, i: number) => ( + {c.header}
+ { + const next = new Set(selected); + if (next.has(i)) next.delete(i); + else next.add(i); + setSelected(next); + }} + /> + + {col.Cell + ? col.Cell({ row: { original: item } }) + : String(col.accessorFn(item))} +
- ), -})); - -const mockUseNamespaceDiscovery = vi.fn(); -vi.mock('../../hooks/useNamespaceDiscovery', () => ({ - useNamespaceDiscovery: () => mockUseNamespaceDiscovery(), -})); - -const mockUseRegisteredClusters = vi.fn(); -vi.mock('../../hooks/useRegisteredClusters', () => ({ - useRegisteredClusters: () => mockUseRegisteredClusters(), -})); - -const mockRegisterAKSCluster = vi.fn(); -vi.mock('../../utils/azure/aks', () => ({ - registerAKSCluster: (...args: any[]) => mockRegisterAKSCluster(...args), -})); - -const mockApplyProjectLabels = vi.fn(); -vi.mock('../../utils/kubernetes/namespaceUtils', () => ({ - applyProjectLabels: (...args: any[]) => mockApplyProjectLabels(...args), -})); - -const mockSetClusterSettings = vi.fn(); -vi.mock('../../utils/shared/clusterSettings', () => ({ - getClusterSettings: () => ({ allowedNamespaces: [] }), - setClusterSettings: (...args: any[]) => mockSetClusterSettings(...args), + + + + ); + }, })); vi.mock('../AzureAuth/AzureAuthGuard', () => ({ default: ({ children }: any) =>
{children}
, })); -vi.mock('../AzureCliWarning', () => ({ - default: () => null, -})); - vi.mock('@iconify/react', () => ({ Icon: ({ icon }: any) => , })); +// Mock the hook so component tests focus purely on rendering/wiring +const mockHandleImportClick = vi.fn(); +const mockHandleConversionClose = vi.fn(); +const mockHandleConversionConfirm = vi.fn(); +const mockHandleGoToProjects = vi.fn(); +const mockRefresh = vi.fn(); +const mockClearError = vi.fn(); +const mockClearSuccess = vi.fn(); +const mockClearDiscoveryError = vi.fn(); + +let mockHookReturn: any; + +vi.mock('./hooks/useImportAKSProjects', () => ({ + useImportAKSProjects: () => mockHookReturn, +})); + // Import after mocks +import type { DiscoveredNamespace } from '../../hooks/useNamespaceDiscovery'; import ImportAKSProjects from './ImportAKSProjects'; -function makeDiscoveredNamespace(overrides: Partial = {}) { +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function makeNs(overrides: Partial = {}): DiscoveredNamespace { return { name: 'test-ns', clusterName: 'test-cluster', resourceGroup: 'test-rg', subscriptionId: 'test-sub', - labels: null, + labels: {}, provisioningState: 'Succeeded', - isAksProject: false, + isAksProject: true, isManagedNamespace: true, - category: 'needs-conversion' as const, + category: 'needs-import', ...overrides, }; } -function defaultDiscoveryReturn(namespaces: any[] = []) { +function defaultHookReturn(overrides: Partial = {}) { return { - namespaces, - needsConversion: namespaces.filter((ns: any) => ns.category === 'needs-conversion'), - needsImport: namespaces.filter((ns: any) => ns.category === 'needs-import'), - loading: false, - error: null, - refresh: vi.fn(), + error: '', + success: '', + namespaces: [], + loadingNamespaces: false, + discoveryError: null, + importing: false, + importResults: undefined, + showConversionDialog: false, + namespacesToConvert: [], + namespacesToImport: [], + refresh: mockRefresh, + clearError: mockClearError, + clearSuccess: mockClearSuccess, + clearDiscoveryError: mockClearDiscoveryError, + handleImportClick: mockHandleImportClick, + handleConversionConfirm: mockHandleConversionConfirm, + handleConversionClose: mockHandleConversionClose, + handleGoToProjects: mockHandleGoToProjects, + ...overrides, }; } +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + describe('ImportAKSProjects', () => { beforeEach(() => { - mockPush.mockReset(); - mockReplace.mockReset(); - mockRegisterAKSCluster.mockReset(); - mockApplyProjectLabels.mockReset(); - mockSetClusterSettings.mockReset(); - mockUseRegisteredClusters.mockReturnValue(new Set()); - mockUseNamespaceDiscovery.mockReturnValue(defaultDiscoveryReturn([])); + vi.clearAllMocks(); + mockHookReturn = defaultHookReturn(); }); afterEach(() => { cleanup(); }); + // ------------------------------------------------------------------------- + // Rendering + // ------------------------------------------------------------------------- + test('renders namespace table with discovered namespaces', () => { - const ns1 = makeDiscoveredNamespace({ - name: 'ns1', - category: 'needs-conversion', - isAksProject: false, - }); - const ns2 = makeDiscoveredNamespace({ - name: 'ns2', - category: 'needs-import', - isAksProject: true, + mockHookReturn = defaultHookReturn({ + namespaces: [makeNs({ name: 'ns1' }), makeNs({ name: 'ns2' })], }); - mockUseNamespaceDiscovery.mockReturnValue(defaultDiscoveryReturn([ns1, ns2])); render(); @@ -160,300 +189,195 @@ describe('ImportAKSProjects', () => { expect(screen.getByTestId('row-ns2')).toBeInTheDocument(); }); - test('shows loading state while discovering', () => { - mockUseNamespaceDiscovery.mockReturnValue({ - ...defaultDiscoveryReturn([]), - loading: true, - }); + test('passes loading state to table', () => { + mockHookReturn = defaultHookReturn({ loadingNamespaces: true }); render(); - const table = screen.getByTestId('namespace-table'); - expect(table).toHaveAttribute('data-loading', 'true'); + expect(screen.getByTestId('namespace-table')).toHaveAttribute('data-loading', 'true'); }); - test('disables import button when no namespace is selected', () => { - const ns = makeDiscoveredNamespace({ name: 'ns1' }); - mockUseNamespaceDiscovery.mockReturnValue(defaultDiscoveryReturn([ns])); + test('shows error alert for import error', () => { + mockHookReturn = defaultHookReturn({ error: 'Something went wrong' }); render(); - // The Import Selected button should be disabled when nothing is selected - const importButton = screen.getByText('Import Selected Projects').closest('button'); - expect(importButton).toBeDisabled(); - - // Select the namespace, then the button should be enabled - const row = screen.getByTestId('row-ns1'); - const checkbox = row.querySelector('input[type="checkbox"]') as HTMLInputElement; - fireEvent.click(checkbox); - - expect(importButton).not.toBeDisabled(); + expect(screen.getByText('Something went wrong')).toBeInTheDocument(); }); - test('shows conversion dialog when selected namespaces need conversion', () => { - const ns = makeDiscoveredNamespace({ - name: 'ns1', - isAksProject: false, - category: 'needs-conversion', - }); - mockUseNamespaceDiscovery.mockReturnValue(defaultDiscoveryReturn([ns])); + test('shows error alert for discovery error', () => { + mockHookReturn = defaultHookReturn({ discoveryError: 'Discovery failed' }); render(); - // Select the namespace by clicking its checkbox - const row = screen.getByTestId('row-ns1'); - const checkbox = row.querySelector('input[type="checkbox"]') as HTMLInputElement; - fireEvent.click(checkbox); + expect(screen.getByText('Discovery failed')).toBeInTheDocument(); + }); - // Click Import Selected - fireEvent.click(screen.getByText('Import Selected Projects')); + test('shows success alert', () => { + mockHookReturn = defaultHookReturn({ success: 'Import complete' }); - // Conversion dialog should appear - expect(screen.getByText('Convert Namespaces to AKS Projects')).toBeInTheDocument(); + render(); + + expect(screen.getByText('Import complete')).toBeInTheDocument(); }); - test('skips conversion dialog when all selected are already projects', async () => { - const ns = makeDiscoveredNamespace({ - name: 'ns1', - isAksProject: true, - category: 'needs-import', - }); - mockUseNamespaceDiscovery.mockReturnValue(defaultDiscoveryReturn([ns])); - mockRegisterAKSCluster.mockResolvedValue({ success: true }); + // ------------------------------------------------------------------------- + // Table toolbar — Import button wiring + // ------------------------------------------------------------------------- + + test('calls handleImportClick with selected namespaces when Import is clicked', () => { + const ns = makeNs({ name: 'ns1' }); + mockHookReturn = defaultHookReturn({ namespaces: [ns] }); render(); - // Select the namespace - const row = screen.getByTestId('row-ns1'); - const checkbox = row.querySelector('input[type="checkbox"]') as HTMLInputElement; + // Select the row via the table mock checkbox + const checkbox = screen.getByTestId('row-ns1').querySelector('input[type="checkbox"]')!; fireEvent.click(checkbox); - // Click Import Selected fireEvent.click(screen.getByText('Import Selected Projects')); - // Conversion dialog should NOT appear - expect(screen.queryByText('Convert Namespaces to AKS Projects')).not.toBeInTheDocument(); - - // Wait for success results to appear - await waitFor(() => { - expect(screen.getByText(/successfully imported/)).toBeInTheDocument(); - }); + expect(mockHandleImportClick).toHaveBeenCalledWith([{ namespace: ns }]); }); - test('calls applyProjectLabels for namespaces needing conversion', async () => { - const ns = makeDiscoveredNamespace({ - name: 'ns1', - clusterName: 'cluster-a', - resourceGroup: 'rg-a', - subscriptionId: 'sub-a', - isAksProject: false, - category: 'needs-conversion', - }); - mockUseNamespaceDiscovery.mockReturnValue(defaultDiscoveryReturn([ns])); - mockApplyProjectLabels.mockResolvedValue(undefined); - mockRegisterAKSCluster.mockResolvedValue({ success: true }); - + test('calls refresh when Refresh button is clicked', () => { render(); - // Select the namespace - const row = screen.getByTestId('row-ns1'); - const checkbox = row.querySelector('input[type="checkbox"]') as HTMLInputElement; - fireEvent.click(checkbox); + fireEvent.click(screen.getByText('Refresh')); - // Click Import Selected -- opens the conversion dialog - fireEvent.click(screen.getByText('Import Selected Projects')); + expect(mockRefresh).toHaveBeenCalledTimes(1); + }); - // Click Confirm & Import in the dialog - fireEvent.click(screen.getByText('Confirm & Import')); + test('disables Refresh button while importing', () => { + mockHookReturn = defaultHookReturn({ importing: true }); - await waitFor(() => { - expect(mockApplyProjectLabels).toHaveBeenCalledWith({ - namespaceName: 'ns1', - clusterName: 'cluster-a', - subscriptionId: 'sub-a', - resourceGroup: 'rg-a', - }); - }); + render(); + + expect(screen.getByText('Refresh').closest('button')).toBeDisabled(); }); - test('handles permission error during label application', async () => { - const ns = makeDiscoveredNamespace({ - name: 'ns1', - isAksProject: false, - category: 'needs-conversion', - }); - mockUseNamespaceDiscovery.mockReturnValue(defaultDiscoveryReturn([ns])); - mockRegisterAKSCluster.mockResolvedValue({ success: true }); - mockApplyProjectLabels.mockRejectedValue(new Error('Forbidden')); + test('disables Import button while importing', () => { + mockHookReturn = defaultHookReturn({ importing: true }); render(); - // Select the namespace - const row = screen.getByTestId('row-ns1'); - const checkbox = row.querySelector('input[type="checkbox"]') as HTMLInputElement; - fireEvent.click(checkbox); + expect(screen.getByText(/Importing/).closest('button')).toBeDisabled(); + }); - // Click Import Selected -- opens the conversion dialog - fireEvent.click(screen.getByText('Import Selected Projects')); + test('disables Import button while namespaces are loading', () => { + mockHookReturn = defaultHookReturn({ loadingNamespaces: true }); - // Click Confirm & Import in the dialog - fireEvent.click(screen.getByText('Confirm & Import')); + render(); - await waitFor(() => { - expect(screen.getByText(/Failed to convert namespace/)).toBeInTheDocument(); - }); + expect(screen.getByText('Import Selected Projects').closest('button')).toBeDisabled(); }); - test('does not re-register already registered clusters', async () => { - mockUseRegisteredClusters.mockReturnValue(new Set(['test-cluster'])); + // ------------------------------------------------------------------------- + // Table / results visibility + // ------------------------------------------------------------------------- - const ns = makeDiscoveredNamespace({ - name: 'ns1', - clusterName: 'test-cluster', - isAksProject: true, - category: 'needs-import', + test('hides table and shows results when all imports succeed', () => { + mockHookReturn = defaultHookReturn({ + importResults: [ + { namespace: 'ns1 (cluster-a)', clusterName: 'cluster-a', success: true, message: 'ok' }, + ], }); - mockUseNamespaceDiscovery.mockReturnValue(defaultDiscoveryReturn([ns])); render(); - // Select the namespace - const row = screen.getByTestId('row-ns1'); - const checkbox = row.querySelector('input[type="checkbox"]') as HTMLInputElement; - fireEvent.click(checkbox); - - // Click Import Selected - fireEvent.click(screen.getByText('Import Selected Projects')); + expect(screen.queryByTestId('namespace-table')).not.toBeInTheDocument(); + expect(screen.getByText(/ns1 \(cluster-a\)/)).toBeInTheDocument(); + }); - await waitFor(() => { - expect(screen.getByText(/successfully imported/)).toBeInTheDocument(); + test('keeps table visible when all imports fail (allows retry)', () => { + mockHookReturn = defaultHookReturn({ + importResults: [ + { + namespace: 'ns1 (cluster-a)', + clusterName: 'cluster-a', + success: false, + message: 'auth error', + }, + ], }); - expect(mockRegisterAKSCluster).not.toHaveBeenCalled(); + render(); + + expect(screen.getByTestId('namespace-table')).toBeInTheDocument(); }); - test('select all / deselect all work correctly', () => { - const ns1 = makeDiscoveredNamespace({ name: 'ns1' }); - const ns2 = makeDiscoveredNamespace({ name: 'ns2' }); - const ns3 = makeDiscoveredNamespace({ name: 'ns3' }); - mockUseNamespaceDiscovery.mockReturnValue(defaultDiscoveryReturn([ns1, ns2, ns3])); + test('shows Go To Projects button when some imports succeed', () => { + mockHookReturn = defaultHookReturn({ + importResults: [{ namespace: 'ns1 (cl)', clusterName: 'cl', success: true, message: 'ok' }], + }); render(); - // Click Select All - fireEvent.click(screen.getByText('Select All')); - expect(screen.getByText(/3 selected/)).toBeInTheDocument(); - - // Click Deselect All - fireEvent.click(screen.getByText('Deselect All')); - expect(screen.getByText(/0 selected/)).toBeInTheDocument(); + expect(screen.getByText('Go To Projects')).toBeInTheDocument(); }); - test('cancel navigates to home', () => { - mockUseNamespaceDiscovery.mockReturnValue(defaultDiscoveryReturn([])); + test('hides Go To Projects button when all imports fail', () => { + mockHookReturn = defaultHookReturn({ + importResults: [{ namespace: 'ns1 (cl)', clusterName: 'cl', success: false, message: 'err' }], + }); render(); - fireEvent.click(screen.getByText('Cancel')); - - expect(mockPush).toHaveBeenCalledWith('/'); + expect(screen.queryByText('Go To Projects')).not.toBeInTheDocument(); }); - test('displays error when cluster registration fails', async () => { - const ns = makeDiscoveredNamespace({ - name: 'ns1', - clusterName: 'cluster-a', - resourceGroup: 'rg-a', - subscriptionId: 'sub-a', - isAksProject: false, - category: 'needs-conversion', + test('calls handleGoToProjects when Go To Projects is clicked', () => { + mockHookReturn = defaultHookReturn({ + importResults: [{ namespace: 'ns1 (cl)', clusterName: 'cl', success: true, message: 'ok' }], }); - mockUseNamespaceDiscovery.mockReturnValue(defaultDiscoveryReturn([ns])); - mockRegisterAKSCluster.mockResolvedValue({ success: false, message: 'Auth failed' }); render(); + fireEvent.click(screen.getByText('Go To Projects')); - // Select the namespace - const row = screen.getByTestId('row-ns1'); - const checkbox = row.querySelector('input[type="checkbox"]') as HTMLInputElement; - fireEvent.click(checkbox); + expect(mockHandleGoToProjects).toHaveBeenCalledTimes(1); + }); - // Click Import Selected -- opens the conversion dialog - fireEvent.click(screen.getByText('Import Selected Projects')); - fireEvent.click(screen.getByText('Confirm & Import')); + // ------------------------------------------------------------------------- + // ConversionDialog wiring + // ------------------------------------------------------------------------- - await waitFor(() => { - expect(screen.getByText(/Auth failed/)).toBeInTheDocument(); + test('shows ConversionDialog when showConversionDialog is true', () => { + mockHookReturn = defaultHookReturn({ + showConversionDialog: true, + namespacesToConvert: [makeNs({ name: 'ns-convert', isAksProject: false })], + namespacesToImport: [], }); + + render(); + + expect(screen.getByText('Convert Namespaces to AKS Projects')).toBeInTheDocument(); }); - test('calls setClusterSettings after successful import', async () => { - const ns = makeDiscoveredNamespace({ - name: 'ns1', - clusterName: 'test-cluster', - isAksProject: true, - category: 'needs-import', + test('calls handleConversionClose when Cancel is clicked in dialog', () => { + mockHookReturn = defaultHookReturn({ + showConversionDialog: true, + namespacesToConvert: [makeNs({ name: 'ns-convert', isAksProject: false })], + namespacesToImport: [], }); - mockUseNamespaceDiscovery.mockReturnValue(defaultDiscoveryReturn([ns])); - mockRegisterAKSCluster.mockResolvedValue({ success: true }); render(); + // The dialog Cancel button (not the table toolbar) + const cancelButtons = screen.getAllByText('Cancel'); + fireEvent.click(cancelButtons[0]); - // Select the namespace - const row = screen.getByTestId('row-ns1'); - const checkbox = row.querySelector('input[type="checkbox"]') as HTMLInputElement; - fireEvent.click(checkbox); - - // Click Import Selected (already a project, no conversion dialog) - fireEvent.click(screen.getByText('Import Selected Projects')); - - await waitFor(() => { - expect(mockSetClusterSettings).toHaveBeenCalledWith( - 'test-cluster', - expect.objectContaining({ - allowedNamespaces: expect.arrayContaining(['ns1']), - }) - ); - }); + expect(mockHandleConversionClose).toHaveBeenCalledTimes(1); }); - test('handles mixed results with some successes and some failures', async () => { - const ns1 = makeDiscoveredNamespace({ - name: 'ns-ok', - clusterName: 'cluster-a', - resourceGroup: 'rg-a', - subscriptionId: 'sub-a', - isAksProject: false, - category: 'needs-conversion', - }); - const ns2 = makeDiscoveredNamespace({ - name: 'ns-fail', - clusterName: 'cluster-a', - resourceGroup: 'rg-a', - subscriptionId: 'sub-a', - isAksProject: false, - category: 'needs-conversion', + test('calls handleConversionConfirm when Confirm & Import is clicked in dialog', () => { + mockHookReturn = defaultHookReturn({ + showConversionDialog: true, + namespacesToConvert: [makeNs({ name: 'ns-convert', isAksProject: false })], + namespacesToImport: [], }); - mockUseNamespaceDiscovery.mockReturnValue(defaultDiscoveryReturn([ns1, ns2])); - mockRegisterAKSCluster.mockResolvedValue({ success: true }); - mockApplyProjectLabels - .mockResolvedValueOnce(undefined) // ns-ok succeeds - .mockRejectedValueOnce(new Error('Forbidden')); // ns-fail fails render(); - - // Select all namespaces - fireEvent.click(screen.getByText('Select All')); - - // Click Import Selected -- opens the conversion dialog - fireEvent.click(screen.getByText('Import Selected Projects')); fireEvent.click(screen.getByText('Confirm & Import')); - await waitFor(() => { - // Should show both success and error results - expect(screen.getByText(/converted and imported/)).toBeInTheDocument(); - }); - - expect(screen.getByText(/Failed to convert namespace/)).toBeInTheDocument(); + expect(mockHandleConversionConfirm).toHaveBeenCalledTimes(1); }); }); diff --git a/plugins/aks-desktop/src/components/ImportAKSProjects/ImportAKSProjects.tsx b/plugins/aks-desktop/src/components/ImportAKSProjects/ImportAKSProjects.tsx index ee5d6b323..7f89798af 100644 --- a/plugins/aks-desktop/src/components/ImportAKSProjects/ImportAKSProjects.tsx +++ b/plugins/aks-desktop/src/components/ImportAKSProjects/ImportAKSProjects.tsx @@ -4,573 +4,188 @@ import { Icon } from '@iconify/react'; import { useTranslation } from '@kinvolk/headlamp-plugin/lib'; import { PageGrid, SectionBox, Table } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; -import { Alert, Box, Button, Checkbox, Chip, CircularProgress, Typography } from '@mui/material'; -import React, { useEffect, useState } from 'react'; -import { useHistory } from 'react-router-dom'; -import { DiscoveredNamespace, useNamespaceDiscovery } from '../../hooks/useNamespaceDiscovery'; -import { useRegisteredClusters } from '../../hooks/useRegisteredClusters'; -import { registerAKSCluster } from '../../utils/azure/aks'; -import { applyProjectLabels } from '../../utils/kubernetes/namespaceUtils'; -import { getClusterSettings, setClusterSettings } from '../../utils/shared/clusterSettings'; +import { Alert, Box, Button, Chip, CircularProgress } from '@mui/material'; +import React from 'react'; +import type { DiscoveredNamespace } from '../../hooks/useNamespaceDiscovery'; import AzureAuthGuard from '../AzureAuth/AzureAuthGuard'; import { ConversionDialog } from './components/ConversionDialog'; - -interface ImportSelection { - namespace: DiscoveredNamespace; - selected: boolean; -} +import { ImportSelection, useImportAKSProjects } from './hooks/useImportAKSProjects'; function ImportAKSProjectsContent() { - const history = useHistory(); const { t } = useTranslation(); - const registeredClusters = useRegisteredClusters(); - - const [error, setError] = useState(''); - const [success, setSuccess] = useState(''); - - // Discovery const { - namespaces: discovered, - loading: loadingNamespaces, - error: discoveryError, + error, + success, + namespaces, + loadingNamespaces, + discoveryError, + importing, + importResults, + showConversionDialog, + namespacesToConvert, + namespacesToImport, refresh, - } = useNamespaceDiscovery(); - - // Allow user to dismiss discoveryError - const [dismissedDiscoveryError, setDismissedDiscoveryError] = useState(false); - useEffect(() => { - setDismissedDiscoveryError(false); - }, [discoveryError]); - - // Selection state layered on top of discovered namespaces - const [selections, setSelections] = useState>(new Set()); - - // Derive selectable list from discovered namespaces - const namespaces: ImportSelection[] = discovered.map(ns => ({ - namespace: ns, - selected: selections.has(`${ns.clusterName}/${ns.name}`), - })); - - const selectedNamespaces = namespaces.filter(ns => ns.selected); - const selectedCount = selectedNamespaces.length; - - // Conversion dialog - const [showConversionDialog, setShowConversionDialog] = useState(false); - - // Import state - const [importing, setImporting] = useState(false); - const [importProgress, setImportProgress] = useState(''); - const [importResults, setImportResults] = useState< - Array<{ namespace: string; clusterName: string; success: boolean; message: string }> | undefined - >(); - - const toggleSelection = (ns: DiscoveredNamespace) => { - setSelections(prev => { - const key = `${ns.clusterName}/${ns.name}`; - const next = new Set(prev); - if (next.has(key)) { - next.delete(key); - } else { - next.add(key); - } - return next; - }); - }; - - const selectAll = () => { - setSelections(new Set(discovered.map(ns => `${ns.clusterName}/${ns.name}`))); - }; - - const deselectAll = () => { - setSelections(new Set()); - }; - - const handleCancel = () => { - history.push('/'); - }; - - /** Called when user clicks "Import Selected" */ - const handleImportClick = () => { - if (selectedCount === 0) { - setError(t('Please select at least one namespace to import')); - return; - } - - const needsConversion = selectedNamespaces.filter(s => !s.namespace.isAksProject); - - if (needsConversion.length > 0) { - // Show confirmation dialog for namespaces that need label conversion - setShowConversionDialog(true); - } else { - // All selected are already AKS projects — import directly - processImport(); - } - }; - - /** Process the import (called after confirmation if conversion needed) */ - const processImport = async () => { - setShowConversionDialog(false); - setImporting(true); - setError(''); - setSuccess(''); - setImportResults(undefined); - - const results: Array<{ - namespace: string; - clusterName: string; - success: boolean; - message: string; - }> = []; - - // Build a lookup of cluster -> Azure metadata from ALL discovered namespaces - // (not just selected). This ensures we have Azure metadata for clusters even - // when the user only selects namespaces that were discovered via K8s API. - const clusterAzureMeta = new Map(); - for (const ns of discovered) { - if (ns.resourceGroup && ns.subscriptionId && !clusterAzureMeta.has(ns.clusterName)) { - clusterAzureMeta.set(ns.clusterName, { - resourceGroup: ns.resourceGroup, - subscriptionId: ns.subscriptionId, - }); - } - } - - // Step 1: Group all selected namespaces by cluster, preferring managed namespace metadata - const clusterMap = new Map< - string, - { - key: { clusterName: string; resourceGroup: string; subscriptionId: string }; - namespaces: DiscoveredNamespace[]; - } - >(); - for (const item of selectedNamespaces) { - const ns = item.namespace; - const meta = clusterAzureMeta.get(ns.clusterName); - const existing = clusterMap.get(ns.clusterName); - if (!existing) { - clusterMap.set(ns.clusterName, { - key: { - clusterName: ns.clusterName, - resourceGroup: ns.resourceGroup || meta?.resourceGroup || '', - subscriptionId: ns.subscriptionId || meta?.subscriptionId || '', - }, - namespaces: [ns], - }); - } else { - existing.namespaces.push(ns); - // Prefer managed namespace metadata (non-empty resourceGroup/subscriptionId) - if (ns.resourceGroup && ns.subscriptionId && !existing.key.resourceGroup) { - existing.key.resourceGroup = ns.resourceGroup; - existing.key.subscriptionId = ns.subscriptionId; - } - } - } - - // Step 2: Register clusters, convert namespaces, and import — per cluster - let processedCount = 0; - const totalCount = selectedNamespaces.length; - for (const { - key: { clusterName, resourceGroup, subscriptionId }, - namespaces: namespacesInCluster, - } of clusterMap.values()) { - try { - // 2a: Register the cluster if it's not already registered in Headlamp. - // Re-registering with a managedNamespace param overwrites the kubeconfig - // with namespace-scoped credentials, which would break access to - // previously imported namespaces on this cluster. - if (!registeredClusters.has(clusterName)) { - // Non-managed namespaces lack Azure metadata, so we can't register the cluster - // on their behalf. The cluster must already be registered. - if (!subscriptionId || !resourceGroup) { - for (const ns of namespacesInCluster) { - results.push({ - namespace: `${ns.name} (${clusterName})`, - clusterName, - success: false, - message: t( - 'Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.', - { clusterName } - ), - }); - } - continue; - } - - setImportProgress( - `${t('Merging cluster {{clusterName}} ({{count}} namespace(s))', { - clusterName, - count: namespacesInCluster.length, - })}...` - ); - - const registerResult = await registerAKSCluster( - subscriptionId, - resourceGroup, - clusterName - ); - - if (!registerResult.success) { - for (const ns of namespacesInCluster) { - results.push({ - namespace: `${ns.name} (${clusterName})`, - clusterName, - success: false, - message: t('Failed to merge cluster: {{message}}', { - message: registerResult.message, - }), - }); - } - continue; - } - } - - // 2b: Apply project labels to namespaces that need conversion. - // The project-id label is required for Headlamp to recognize the namespace as a project, - // so we must wait for this to complete before importing. - const failedNames = new Set(); - for (const ns of namespacesInCluster) { - if (ns.isAksProject) continue; - - setImportProgress( - t('Converting {{name}} to AKS project (this may take a moment)...', { - name: ns.name, - }) - ); - try { - // For managed namespaces, fall back to cluster-level Azure metadata - // (from other managed namespaces on the same cluster) so applyProjectLabels - // uses the ARM API. For regular namespaces, use their own (empty) metadata - // so applyProjectLabels uses the K8s API — the ARM API would reject them - // because they don't exist as managed namespace resources. - await applyProjectLabels({ - namespaceName: ns.name, - clusterName: ns.clusterName, - subscriptionId: ns.isManagedNamespace - ? ns.subscriptionId || subscriptionId - : ns.subscriptionId, - resourceGroup: ns.isManagedNamespace - ? ns.resourceGroup || resourceGroup - : ns.resourceGroup, - }); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - failedNames.add(ns.name); - results.push({ - namespace: `${ns.name} (${clusterName})`, - clusterName, - success: false, - message: t('Failed to convert namespace: {{message}}', { message }), - }); - } - } - - // 2c: Update allowed namespaces in localStorage - const importableInCluster = namespacesInCluster.filter(ns => !failedNames.has(ns.name)); - if (importableInCluster.length > 0) { - try { - const settings = getClusterSettings(clusterName); - settings.allowedNamespaces = [ - ...new Set([ - ...(settings.allowedNamespaces ?? []), - ...importableInCluster.map(ns => ns.name), - ]), - ]; - setClusterSettings(clusterName, settings); - } catch (e) { - console.error('Failed to update allowed namespaces for cluster ' + clusterName, e); - } - } - - for (const ns of importableInCluster) { - processedCount++; - setImportProgress( - `${t('Importing {{current}} of {{total}}: {{name}} from {{clusterName}}', { - current: processedCount, - total: totalCount, - name: ns.name, - clusterName, - })}...` - ); - - results.push({ - namespace: `${ns.name} (${clusterName})`, - clusterName, - success: true, - message: ns.isAksProject - ? t("Project '{{name}}' successfully imported", { name: ns.name }) - : t("Namespace '{{name}}' converted and imported as project", { name: ns.name }), - }); - } - } catch (err) { - for (const ns of namespacesInCluster) { - results.push({ - namespace: `${ns.name} (${clusterName})`, - clusterName, - success: false, - message: err instanceof Error ? err.message : t('Unknown error'), - }); - } - } - } - - setImportResults(results); - - const successCount = results.filter(r => r.success).length; - const failureCount = results.filter(r => !r.success).length; - const successfulClusters = new Set(results.filter(r => r.success).map(r => r.clusterName)).size; + clearError, + clearSuccess, + clearDiscoveryError, + handleImportClick, + handleConversionConfirm, + handleConversionClose, + handleGoToProjects, + } = useImportAKSProjects(); - if (successCount > 0) { - const clusterText = t('Successfully merged {{count}} cluster(s)', { - count: successfulClusters, - }); - const projectText = t('with {{count}} project(s)', { count: successCount }); - const failureSuffix = - failureCount > 0 ? ` ${t('{{count}} failed.', { count: failureCount })}` : '.'; - setSuccess(`${clusterText} ${projectText}${failureSuffix}`); - } else if (results.length > 0) { - setError(t('Failed to import any projects. See details below.')); - } - - setImporting(false); - setImportProgress(''); - }; - - const displayError = error || (!dismissedDiscoveryError && discoveryError) || ''; + const displayError = error || discoveryError || ''; return ( - <> - - - - {t( - 'Import existing managed namespaces and regular namespaces as projects. Namespaces that are not yet AKS Desktop projects will be converted by adding the required project label.' - )} - - - {displayError && ( - { - if (error) { - setError(''); - } else { - setDismissedDiscoveryError(true); - } - }} - sx={{ mb: 2 }} - > - {displayError} - - )} - - {success && ( - setSuccess('')} sx={{ mb: 2 }}> - {success} - - )} - - {!importResults && ( - <> - - - {t('Select Namespaces to Import')}{' '} - {t('{{count}} selected', { count: selectedCount })} - + + + {displayError && ( + + {displayError} + + )} + + {success && ( + + {success} + + )} + + {(!importResults || importResults.every(r => !r.success)) && ( + n.name, + }, + { + header: t('Type'), + accessorFn: (n: DiscoveredNamespace) => + n.isManagedNamespace ? 'AKS Managed' : 'Regular', + gridTemplate: 'min-content', + Cell: ({ row: { original: ns } }: { row: { original: DiscoveredNamespace } }) => ( + + ), + }, + { + header: t('Cluster'), + accessorFn: (n: DiscoveredNamespace) => n.clusterName, + }, + { + header: t('Resource Group'), + accessorFn: (n: DiscoveredNamespace) => n.resourceGroup, + }, + { + header: t('AKS Project?'), + accessorFn: (n: DiscoveredNamespace) => (n.isAksProject ? 'Yes' : 'No'), + gridTemplate: 'min-content', + Cell: ({ row: { original: ns } }: { row: { original: DiscoveredNamespace } }) => + ns.isAksProject ? ( + } + label={t('Yes')} + color="success" + size="small" + variant="outlined" + /> + ) : ( + } + label={t('No')} + color="default" + size="small" + variant="outlined" + /> + ), + }, + ]} + renderRowSelectionToolbar={({ table }) => ( + <> - - - - -
n.selected, - gridTemplate: 'min-content', - enableSorting: false, - Cell: ({ row: { original: item } }: { row: { original: ImportSelection } }) => ( - toggleSelection(item.namespace)} - disabled={importing} - size="small" - sx={{ padding: '4px' }} - /> - ), - }, - { - header: t('Name'), - accessorFn: (n: ImportSelection) => n.namespace.name, - }, - { - header: t('Type'), - accessorFn: (n: ImportSelection) => - n.namespace.isManagedNamespace ? 'AKS Managed' : 'Regular', - gridTemplate: 'min-content', - Cell: ({ row: { original: item } }: { row: { original: ImportSelection } }) => ( - - ), - }, - { - header: t('Cluster'), - accessorFn: (n: ImportSelection) => n.namespace.clusterName, - }, - { - header: t('Resource Group'), - accessorFn: (n: ImportSelection) => n.namespace.resourceGroup, - }, - { - header: t('AKS Project?'), - accessorFn: (n: ImportSelection) => (n.namespace.isAksProject ? 'Yes' : 'No'), - gridTemplate: 'min-content', - Cell: ({ row: { original: item } }: { row: { original: ImportSelection } }) => - item.namespace.isAksProject ? ( - } - label={t('Yes')} - color="success" - size="small" - variant="outlined" - /> - ) : ( - } - label={t('No')} - color="default" - size="small" - variant="outlined" - /> - ), - }, - ]} - /> - - - - - - )} - - {/* Import Results */} - {importResults && importResults.length > 0 && ( - <> - - {importResults.map(result => ( - - {result.namespace}: {result.message} - - ))} - - - {importResults.some(r => r.success) && ( - - )} + + )} + /> + )} + + {importResults && importResults.length > 0 && ( + <> + + {importResults.map(result => ( + + {result.namespace}: {result.message} + + ))} + + + {importResults.some(r => r.success) && ( - - - )} - - + )} + + + )} + setShowConversionDialog(false)} - onConfirm={processImport} - namespacesToConvert={selectedNamespaces - .filter(s => !s.namespace.isAksProject) - .map(s => s.namespace)} - namespacesToImport={selectedNamespaces - .filter(s => s.namespace.isAksProject) - .map(s => s.namespace)} + onClose={handleConversionClose} + onConfirm={handleConversionConfirm} + namespacesToConvert={namespacesToConvert} + namespacesToImport={namespacesToImport} converting={importing} /> - + ); } diff --git a/plugins/aks-desktop/src/components/ImportAKSProjects/hooks/useImportAKSProjects.test.ts b/plugins/aks-desktop/src/components/ImportAKSProjects/hooks/useImportAKSProjects.test.ts new file mode 100644 index 000000000..84695eb0f --- /dev/null +++ b/plugins/aks-desktop/src/components/ImportAKSProjects/hooks/useImportAKSProjects.test.ts @@ -0,0 +1,390 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the Apache 2.0. + +// @vitest-environment jsdom +import { act, renderHook } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; + +// --- Mocks (vi.hoisted ensures variables are available when vi.mock is hoisted) --- + +const mockRegisterAKSCluster = vi.hoisted(() => vi.fn()); +const mockApplyProjectLabels = vi.hoisted(() => vi.fn()); +const mockHistoryReplace = vi.hoisted(() => vi.fn()); +const mockDiscover = vi.hoisted(() => vi.fn()); +const mockGetClusterSettings = vi.hoisted(() => vi.fn()); +const mockSetClusterSettings = vi.hoisted(() => vi.fn()); + +let mockNamespaces: any[] = []; +let mockRegisteredClusters: Set = new Set(); + +vi.mock('../../../utils/azure/aks', () => ({ + registerAKSCluster: mockRegisterAKSCluster, +})); + +vi.mock('../../../utils/kubernetes/namespaceUtils', () => ({ + applyProjectLabels: mockApplyProjectLabels, +})); + +vi.mock('../../../utils/shared/clusterSettings', () => ({ + getClusterSettings: mockGetClusterSettings, + setClusterSettings: mockSetClusterSettings, +})); + +vi.mock('../../../hooks/useNamespaceDiscovery', () => ({ + useNamespaceDiscovery: () => ({ + namespaces: mockNamespaces, + loading: false, + error: null, + refresh: mockDiscover, + }), +})); + +vi.mock('../../../hooks/useRegisteredClusters', () => ({ + useRegisteredClusters: () => mockRegisteredClusters, +})); + +vi.mock('react-router-dom', () => ({ + useHistory: () => ({ replace: mockHistoryReplace }), +})); + +vi.mock('@kinvolk/headlamp-plugin/lib', () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})); + +import type { DiscoveredNamespace } from '../../../hooks/useNamespaceDiscovery'; +import type { ImportSelection } from './useImportAKSProjects'; +import { useImportAKSProjects } from './useImportAKSProjects'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function makeNamespace( + name: string, + clusterName: string, + resourceGroup = 'rg', + subscriptionId = 'sub', + isAksProject = true, + isManagedNamespace = true +): DiscoveredNamespace { + return { + name, + clusterName, + resourceGroup, + subscriptionId, + labels: {}, + provisioningState: 'Succeeded', + isAksProject, + isManagedNamespace, + category: isAksProject ? 'needs-import' : 'needs-conversion', + }; +} + +function makeSelection(ns: DiscoveredNamespace): ImportSelection { + return { namespace: ns }; +} + +describe('useImportAKSProjects', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockNamespaces = []; + mockRegisteredClusters = new Set(); + mockRegisterAKSCluster.mockResolvedValue({ success: true, message: '' }); + mockApplyProjectLabels.mockResolvedValue(undefined); + mockGetClusterSettings.mockReturnValue({}); + mockSetClusterSettings.mockReturnValue(undefined); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + // ------------------------------------------------------------------------- + // Initial state + // ------------------------------------------------------------------------- + + test('starts with correct initial state', () => { + const { result } = renderHook(() => useImportAKSProjects()); + + expect(result.current.error).toBe(''); + expect(result.current.success).toBe(''); + expect(result.current.importing).toBe(false); + expect(result.current.importResults).toBeUndefined(); + expect(result.current.showConversionDialog).toBe(false); + }); + + // ------------------------------------------------------------------------- + // handleImportClick — guard + // ------------------------------------------------------------------------- + + test('handleImportClick sets error when nothing selected', () => { + const { result } = renderHook(() => useImportAKSProjects()); + + act(() => result.current.handleImportClick([])); + + expect(result.current.error).toBe('Please select at least one namespace to import'); + expect(mockRegisterAKSCluster).not.toHaveBeenCalled(); + }); + + // ------------------------------------------------------------------------- + // ConversionDialog flow + // ------------------------------------------------------------------------- + + test('handleImportClick opens ConversionDialog when non-project namespaces selected', () => { + const { result } = renderHook(() => useImportAKSProjects()); + const ns = makeNamespace('ns-1', 'cl', 'rg', 'sub', false); + + act(() => result.current.handleImportClick([makeSelection(ns)])); + + expect(result.current.showConversionDialog).toBe(true); + expect(result.current.namespacesToConvert).toHaveLength(1); + expect(result.current.namespacesToConvert[0].name).toBe('ns-1'); + }); + + test('handleImportClick skips ConversionDialog when all namespaces are already projects', () => { + const { result } = renderHook(() => useImportAKSProjects()); + const ns = makeNamespace('ns-1', 'cl'); + + act(() => result.current.handleImportClick([makeSelection(ns)])); + + expect(result.current.showConversionDialog).toBe(false); + }); + + test('handleConversionClose resets dialog and pending selection', () => { + const { result } = renderHook(() => useImportAKSProjects()); + const ns = makeNamespace('ns-1', 'cl', 'rg', 'sub', false); + + act(() => result.current.handleImportClick([makeSelection(ns)])); + expect(result.current.showConversionDialog).toBe(true); + + act(() => result.current.handleConversionClose()); + expect(result.current.showConversionDialog).toBe(false); + expect(mockRegisterAKSCluster).not.toHaveBeenCalled(); + }); + + test('handleConversionConfirm closes dialog and starts import', async () => { + const { result } = renderHook(() => useImportAKSProjects()); + const ns = makeNamespace('ns-1', 'cl', 'rg', 'sub', false); + + act(() => result.current.handleImportClick([makeSelection(ns)])); + await act(async () => result.current.handleConversionConfirm()); + + expect(result.current.showConversionDialog).toBe(false); + expect(result.current.importResults).toBeDefined(); + }); + + // ------------------------------------------------------------------------- + // handleImportClick — cluster registration + // ------------------------------------------------------------------------- + + test('skips registerAKSCluster for already-registered clusters', async () => { + mockRegisteredClusters = new Set(['cl']); + const { result } = renderHook(() => useImportAKSProjects()); + const ns = makeNamespace('ns-1', 'cl'); + + await act(async () => result.current.handleImportClick([makeSelection(ns)])); + + expect(mockRegisterAKSCluster).not.toHaveBeenCalled(); + expect(result.current.importResults![0].success).toBe(true); + }); + + test('calls registerAKSCluster once per unregistered cluster', async () => { + const { result } = renderHook(() => useImportAKSProjects()); + + await act(async () => + result.current.handleImportClick([ + makeSelection(makeNamespace('ns-1', 'cl', 'rg', 'sub')), + makeSelection(makeNamespace('ns-2', 'cl', 'rg', 'sub')), + ]) + ); + + expect(mockRegisterAKSCluster).toHaveBeenCalledTimes(1); + expect(mockRegisterAKSCluster).toHaveBeenCalledWith('sub', 'rg', 'cl'); + }); + + test('marks cluster namespaces failed when registerAKSCluster returns failure', async () => { + mockRegisterAKSCluster.mockResolvedValue({ success: false, message: 'auth error' }); + const { result } = renderHook(() => useImportAKSProjects()); + + await act(async () => + result.current.handleImportClick([ + makeSelection(makeNamespace('ns-1', 'cl')), + makeSelection(makeNamespace('ns-2', 'cl')), + ]) + ); + + expect(result.current.importResults).toHaveLength(2); + expect(result.current.importResults!.every(r => !r.success)).toBe(true); + expect(result.current.error).toBe('Failed to import any projects.'); + }); + + test('marks namespaces failed and skips registration for unregistered namespace without Azure metadata', async () => { + const { result } = renderHook(() => useImportAKSProjects()); + const ns = makeNamespace('ns-1', 'cl', '', '', true, false); // no resourceGroup/subscriptionId + + await act(async () => result.current.handleImportClick([makeSelection(ns)])); + + expect(result.current.importResults![0].success).toBe(false); + expect(mockRegisterAKSCluster).not.toHaveBeenCalled(); + }); + + // ------------------------------------------------------------------------- + // handleImportClick — label conversion + // ------------------------------------------------------------------------- + + test('calls applyProjectLabels for non-project namespaces', async () => { + mockRegisteredClusters = new Set(['cl']); + const { result } = renderHook(() => useImportAKSProjects()); + const ns = makeNamespace('ns-1', 'cl', 'rg', 'sub', false); + + // non-project namespace triggers ConversionDialog first; confirm to run processImport + act(() => result.current.handleImportClick([makeSelection(ns)])); + await act(async () => result.current.handleConversionConfirm()); + + expect(mockApplyProjectLabels).toHaveBeenCalledTimes(1); + expect(mockApplyProjectLabels).toHaveBeenCalledWith( + expect.objectContaining({ namespaceName: 'ns-1', clusterName: 'cl' }) + ); + }); + + test('skips applyProjectLabels for already-project namespaces', async () => { + mockRegisteredClusters = new Set(['cl']); + const { result } = renderHook(() => useImportAKSProjects()); + const ns = makeNamespace('ns-1', 'cl', 'rg', 'sub', true); + + await act(async () => result.current.handleImportClick([makeSelection(ns)])); + + expect(mockApplyProjectLabels).not.toHaveBeenCalled(); + }); + + test('marks namespace failed and continues when applyProjectLabels throws', async () => { + mockRegisteredClusters = new Set(['cl']); + mockApplyProjectLabels.mockRejectedValue(new Error('label error')); + const { result } = renderHook(() => useImportAKSProjects()); + const ns = makeNamespace('ns-1', 'cl', 'rg', 'sub', false); + + // non-project namespace triggers ConversionDialog first; confirm to run processImport + act(() => result.current.handleImportClick([makeSelection(ns)])); + await act(async () => result.current.handleConversionConfirm()); + + expect(result.current.importResults![0].success).toBe(false); + expect(result.current.importResults![0].message).toContain('Failed to convert namespace'); + }); + + // ------------------------------------------------------------------------- + // handleImportClick — localStorage + // ------------------------------------------------------------------------- + + test('calls setClusterSettings with imported namespace names', async () => { + mockRegisteredClusters = new Set(['cl']); + mockGetClusterSettings.mockReturnValue({ allowedNamespaces: [] }); + const { result } = renderHook(() => useImportAKSProjects()); + + await act(async () => + result.current.handleImportClick([makeSelection(makeNamespace('ns-1', 'cl'))]) + ); + + expect(mockSetClusterSettings).toHaveBeenCalledWith( + 'cl', + expect.objectContaining({ allowedNamespaces: expect.arrayContaining(['ns-1']) }) + ); + }); + + test('deduplicates allowedNamespaces when merging with existing settings', async () => { + mockRegisteredClusters = new Set(['cl']); + mockGetClusterSettings.mockReturnValue({ allowedNamespaces: ['ns-1'] }); + const { result } = renderHook(() => useImportAKSProjects()); + + await act(async () => + result.current.handleImportClick([makeSelection(makeNamespace('ns-1', 'cl'))]) + ); + + const [, settings] = mockSetClusterSettings.mock.calls[0]; + expect(settings.allowedNamespaces.filter((n: string) => n === 'ns-1')).toHaveLength(1); + }); + + // ------------------------------------------------------------------------- + // handleImportClick — success / failure outcomes + // ------------------------------------------------------------------------- + + test('sets success message when all namespaces import successfully', async () => { + mockRegisteredClusters = new Set(['cl']); + const { result } = renderHook(() => useImportAKSProjects()); + + await act(async () => + result.current.handleImportClick([makeSelection(makeNamespace('ns-1', 'cl'))]) + ); + + expect(result.current.success).not.toBe(''); + expect(result.current.error).toBe(''); + expect(result.current.importResults![0].success).toBe(true); + }); + + test('sets error message when all imports fail', async () => { + mockRegisterAKSCluster.mockResolvedValue({ success: false, message: 'denied' }); + const { result } = renderHook(() => useImportAKSProjects()); + + await act(async () => + result.current.handleImportClick([makeSelection(makeNamespace('ns-1', 'cl'))]) + ); + + expect(result.current.error).toBe('Failed to import any projects.'); + expect(result.current.success).toBe(''); + }); + + // ------------------------------------------------------------------------- + // clearError, clearSuccess + // ------------------------------------------------------------------------- + + test('clearError clears the error message', () => { + const { result } = renderHook(() => useImportAKSProjects()); + act(() => result.current.handleImportClick([])); + expect(result.current.error).not.toBe(''); + + act(() => result.current.clearError()); + expect(result.current.error).toBe(''); + }); + + test('clearSuccess clears the success message', async () => { + mockRegisteredClusters = new Set(['cl']); + const { result } = renderHook(() => useImportAKSProjects()); + + await act(async () => + result.current.handleImportClick([makeSelection(makeNamespace('ns-1', 'cl'))]) + ); + expect(result.current.success).not.toBe(''); + + act(() => result.current.clearSuccess()); + expect(result.current.success).toBe(''); + }); + + // ------------------------------------------------------------------------- + // handleGoToProjects + // ------------------------------------------------------------------------- + + test('handleGoToProjects replaces history and reloads', () => { + const originalLocation = window.location; + const originalDescriptor = Object.getOwnPropertyDescriptor(window, 'location'); + const reloadMock = vi.fn(); + // Strip accessor fields (get/set) — can't mix with value descriptor + const { configurable, enumerable } = originalDescriptor ?? {}; + Object.defineProperty(window, 'location', { + configurable, + enumerable, + value: { reload: reloadMock }, + writable: true, + }); + try { + const { result } = renderHook(() => useImportAKSProjects()); + act(() => result.current.handleGoToProjects()); + expect(mockHistoryReplace).toHaveBeenCalledWith('/'); + expect(reloadMock).toHaveBeenCalledTimes(1); + } finally { + if (originalDescriptor) { + Object.defineProperty(window, 'location', originalDescriptor); + } else { + (window as any).location = originalLocation; + } + } + }); +}); diff --git a/plugins/aks-desktop/src/components/ImportAKSProjects/hooks/useImportAKSProjects.ts b/plugins/aks-desktop/src/components/ImportAKSProjects/hooks/useImportAKSProjects.ts new file mode 100644 index 000000000..e4e6335d4 --- /dev/null +++ b/plugins/aks-desktop/src/components/ImportAKSProjects/hooks/useImportAKSProjects.ts @@ -0,0 +1,333 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the Apache 2.0. + +import { useTranslation } from '@kinvolk/headlamp-plugin/lib'; +import { useEffect, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import type { + DiscoveredNamespace, + UseNamespaceDiscoveryReturn, +} from '../../../hooks/useNamespaceDiscovery'; +import { useNamespaceDiscovery } from '../../../hooks/useNamespaceDiscovery'; +import { useRegisteredClusters } from '../../../hooks/useRegisteredClusters'; +import { registerAKSCluster } from '../../../utils/azure/aks'; +import { applyProjectLabels } from '../../../utils/kubernetes/namespaceUtils'; +import { getClusterSettings, setClusterSettings } from '../../../utils/shared/clusterSettings'; + +export interface ImportSelection { + namespace: DiscoveredNamespace; +} + +export interface ImportResult { + namespace: string; + clusterName: string; + success: boolean; + message: string; +} + +/** + * Return type for the {@link useImportAKSProjects} hook. + */ +interface UseImportAKSProjectsResult { + error: string; + success: string; + namespaces: DiscoveredNamespace[]; + loadingNamespaces: boolean; + discoveryError: string | null; + importing: boolean; + importResults: ImportResult[] | undefined; + showConversionDialog: boolean; + namespacesToConvert: DiscoveredNamespace[]; + namespacesToImport: DiscoveredNamespace[]; + refresh: UseNamespaceDiscoveryReturn['refresh']; + clearError: () => void; + clearSuccess: () => void; + clearDiscoveryError: () => void; + handleImportClick: (selected: ImportSelection[]) => void; + handleConversionConfirm: () => void; + handleConversionClose: () => void; + handleGoToProjects: () => void; +} + +/** + * Manages all state and logic for the Import AKS Projects flow. + * + * Discovers namespaces via {@link useNamespaceDiscovery} (managed namespaces via Azure + * Resource Graph + regular namespaces via the K8s API). Accepts a caller-provided selection, + * shows a ConversionDialog when non-project namespaces are selected, then orchestrates the + * import by registering each unique cluster (skipping already-registered ones), applying + * project labels to namespaces that need conversion, and writing localStorage allowed + * namespaces. + */ +export const useImportAKSProjects = (): UseImportAKSProjectsResult => { + const history = useHistory(); + const { t } = useTranslation(); + const registeredClusters = useRegisteredClusters(); + + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + + const [importing, setImporting] = useState(false); + const [importResults, setImportResults] = useState(); + + const [showConversionDialog, setShowConversionDialog] = useState(false); + const [pendingSelection, setPendingSelection] = useState([]); + + const { + namespaces, + loading: loadingNamespaces, + error: discoveryError, + refresh, + } = useNamespaceDiscovery(); + + const [dismissedDiscoveryError, setDismissedDiscoveryError] = useState(false); + useEffect(() => { + setDismissedDiscoveryError(false); + }, [discoveryError]); + + const namespacesToConvert = pendingSelection + .filter(s => !s.namespace.isAksProject) + .map(s => s.namespace); + const namespacesToImport = pendingSelection + .filter(s => s.namespace.isAksProject) + .map(s => s.namespace); + + /** Called when user clicks "Import Selected" in the toolbar. */ + const handleImportClick = (selected: ImportSelection[]) => { + if (selected.length === 0) { + setError(t('Please select at least one namespace to import')); + return; + } + + setPendingSelection(selected); + + if (selected.some(s => !s.namespace.isAksProject)) { + setShowConversionDialog(true); + } else { + void processImport(selected); + } + }; + + const handleConversionConfirm = () => { + setShowConversionDialog(false); + void processImport(pendingSelection); + }; + + const handleConversionClose = () => { + setShowConversionDialog(false); + setPendingSelection([]); + }; + + const processImport = async (selectedItems: ImportSelection[]) => { + setImporting(true); + setError(''); + setSuccess(''); + setImportResults(undefined); + + try { + const results: ImportResult[] = []; + + // Build a lookup of cluster -> Azure metadata from ALL discovered namespaces + // so we have metadata for clusters even when the user only selects regular namespaces. + const clusterAzureMeta = new Map(); + for (const ns of namespaces) { + if (ns.resourceGroup && ns.subscriptionId && !clusterAzureMeta.has(ns.clusterName)) { + clusterAzureMeta.set(ns.clusterName, { + resourceGroup: ns.resourceGroup, + subscriptionId: ns.subscriptionId, + }); + } + } + + // Group selected namespaces by cluster, preferring managed namespace metadata. + const clusterMap = new Map< + string, + { + clusterName: string; + resourceGroup: string; + subscriptionId: string; + items: DiscoveredNamespace[]; + } + >(); + for (const { namespace: ns } of selectedItems) { + const meta = clusterAzureMeta.get(ns.clusterName); + const existing = clusterMap.get(ns.clusterName); + if (!existing) { + clusterMap.set(ns.clusterName, { + clusterName: ns.clusterName, + resourceGroup: ns.resourceGroup || meta?.resourceGroup || '', + subscriptionId: ns.subscriptionId || meta?.subscriptionId || '', + items: [ns], + }); + } else { + existing.items.push(ns); + if (ns.resourceGroup && ns.subscriptionId && !existing.resourceGroup) { + existing.resourceGroup = ns.resourceGroup; + existing.subscriptionId = ns.subscriptionId; + } + } + } + + for (const { + clusterName, + resourceGroup, + subscriptionId, + items: namespacesInCluster, + } of clusterMap.values()) { + try { + // Register the cluster if it's not already registered in Headlamp. + // Re-registering overwrites the kubeconfig with namespace-scoped credentials, + // which would break access to previously imported namespaces on this cluster. + if (!registeredClusters.has(clusterName)) { + if (!subscriptionId || !resourceGroup) { + for (const ns of namespacesInCluster) { + results.push({ + namespace: `${ns.name} (${clusterName})`, + clusterName, + success: false, + message: t( + 'Cluster {{clusterName}} must be registered before importing regular namespaces. Import a managed namespace from this cluster first.', + { clusterName } + ), + }); + } + continue; + } + + const registerResult = await registerAKSCluster( + subscriptionId, + resourceGroup, + clusterName + ); + + if (!registerResult.success) { + for (const ns of namespacesInCluster) { + results.push({ + namespace: `${ns.name} (${clusterName})`, + clusterName, + success: false, + message: t('Failed to merge cluster: {{message}}', { + message: registerResult.message, + }), + }); + } + continue; + } + } + + // Apply project labels to namespaces that need conversion. + const failedNames = new Set(); + for (const ns of namespacesInCluster) { + if (ns.isAksProject) continue; + + try { + await applyProjectLabels({ + namespaceName: ns.name, + clusterName: ns.clusterName, + subscriptionId: ns.isManagedNamespace + ? ns.subscriptionId || subscriptionId + : ns.subscriptionId, + resourceGroup: ns.isManagedNamespace + ? ns.resourceGroup || resourceGroup + : ns.resourceGroup, + }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + failedNames.add(ns.name); + results.push({ + namespace: `${ns.name} (${clusterName})`, + clusterName, + success: false, + message: t('Failed to convert namespace: {{message}}', { message }), + }); + } + } + + // Update allowed namespaces in localStorage. + const importableInCluster = namespacesInCluster.filter(ns => !failedNames.has(ns.name)); + if (importableInCluster.length > 0) { + try { + const settings = getClusterSettings(clusterName); + settings.allowedNamespaces = [ + ...new Set([ + ...(settings.allowedNamespaces ?? []), + ...importableInCluster.map(ns => ns.name), + ]), + ]; + setClusterSettings(clusterName, settings); + } catch (e) { + console.error('Failed to update allowed namespaces for cluster ' + clusterName, e); + } + } + + for (const ns of importableInCluster) { + results.push({ + namespace: `${ns.name} (${clusterName})`, + clusterName, + success: true, + message: ns.isAksProject + ? t("Project '{{name}}' successfully imported", { name: ns.name }) + : t("Namespace '{{name}}' converted and imported as project", { name: ns.name }), + }); + } + } catch (err) { + for (const ns of namespacesInCluster) { + results.push({ + namespace: `${ns.name} (${clusterName})`, + clusterName, + success: false, + message: err instanceof Error ? err.message : t('Unknown error'), + }); + } + } + } + + setImportResults(results); + + const successCount = results.filter(r => r.success).length; + const failureCount = results.filter(r => !r.success).length; + const successfulClusters = new Set(results.filter(r => r.success).map(r => r.clusterName)) + .size; + + if (successCount > 0) { + const clusterText = t('Successfully imported from {{count}} cluster(s)', { + count: successfulClusters, + }); + const projectText = t('with {{count}} project(s)', { count: successCount }); + const failureSuffix = + failureCount > 0 ? ` ${t('{{count}} failed.', { count: failureCount })}` : '.'; + setSuccess(`${clusterText} ${projectText}${failureSuffix}`); + } else { + setError(t('Failed to import any projects.')); + } + } finally { + setImporting(false); + } + }; + + const handleGoToProjects = () => { + history.replace('/'); + window.location.reload(); + }; + + return { + error, + success, + namespaces, + loadingNamespaces, + discoveryError: dismissedDiscoveryError ? null : discoveryError, + importing, + importResults, + showConversionDialog, + namespacesToConvert, + namespacesToImport, + refresh, + clearError: () => setError(''), + clearSuccess: () => setSuccess(''), + clearDiscoveryError: () => setDismissedDiscoveryError(true), + handleImportClick, + handleConversionConfirm, + handleConversionClose, + handleGoToProjects, + }; +};