diff --git a/pkg/suse-ai-lifecycle-manager/pages/components/AppWizard.vue b/pkg/suse-ai-lifecycle-manager/pages/components/AppWizard.vue index 082c5d2..69cdf4f 100644 --- a/pkg/suse-ai-lifecycle-manager/pages/components/AppWizard.vue +++ b/pkg/suse-ai-lifecycle-manager/pages/components/AppWizard.vue @@ -23,7 +23,8 @@ import { getClusters, getInstalledHelmDetails, inferClusterRepoForChart, - listClusterRepos + listClusterRepos, + listNamespaces } from '../../services/rancher-apps'; import { getRepoAuthForClusterRepo } from '../../services/repo-auth'; import { persistLoad, persistSave, persistClear } from '../../services/ui-persist'; @@ -67,6 +68,7 @@ const versionInfo = ref(null); const questionsLoading = ref(false); const versionInfoKey = ref(''); const defaultValuesSnapshot = ref>({}); +const namespaceOptions = ref<{label: string, value: string}[]>([]); // Multi-cluster install progress state const showProgressModal = ref(false); @@ -93,6 +95,52 @@ const versionOptions = computed(() => (versions.value || []).map(v => ({ label: v, value: v })) ); +// Fetch all namespaces from all clusters and filter out system namespaces +async function fetchAllNamespaces() { + if (!store) { + return; + } + + try { + const clusters = await getClusters(store); + console.log('[SUSE-AI] All available clusters:', clusters); + + const allNamespaces = new Set(); + + await Promise.all(clusters.map(async (cluster) => { + console.log('[SUSE-AI] Trying to get namespaces for cluster:', cluster); + try { + const namespaces = await listNamespaces(store, cluster.id); + namespaces.forEach(ns => allNamespaces.add(ns)); + } catch (e) { + console.warn(`[SUSE-AI] Failed to fetch namespaces for cluster ${cluster.id}:`, e); + } + })); + + console.log('[SUSE-AI] Found all unique namespaces:', allNamespaces); + + const systemPrefixes = ['c-', 'p-', 'kube-', 'cattle-', 'rancher', 'longhorn-', 'fleet-', 'cluster-fleet-', 'system-', 'istio-', 'neuvector', 'ingress-', 'cert-manager']; + const userNamespaces = Array.from(allNamespaces).filter(name => + !systemPrefixes.some(prefix => name.startsWith(prefix)) + ); + + const desiredDefault = `${props.slug}-system`; + if (!userNamespaces.includes(desiredDefault)) { + userNamespaces.push(desiredDefault); + } + + const sortedNamespaces = userNamespaces.sort(); + namespaceOptions.value = sortedNamespaces.map(ns => ({ label: ns, value: ns })); + + if (isInstallMode.value) { + form.value.namespace = desiredDefault; + } + + } catch (e) { + console.warn(`[SUSE-AI] Failed to fetch all namespaces:`, e); + } +} + // Wizard step configuration for Rancher Wizard component const wizardSteps = computed(() => [ { @@ -184,6 +232,7 @@ const basicInfoForm = computed({ onMounted(async () => { try { await initializeWizard(); + await fetchAllNamespaces(); } catch (e) { error.value = `Failed to initialize: ${e.message || 'Unknown error'}`; } finally { @@ -969,6 +1018,7 @@ function previousStep() { v-model:form="basicInfoForm" :version-options="versionOptions" :loading-versions="loadingVersions" + :namespace-options="namespaceOptions" /> diff --git a/pkg/suse-ai-lifecycle-manager/pages/components/wizard/BasicInfoStep.vue b/pkg/suse-ai-lifecycle-manager/pages/components/wizard/BasicInfoStep.vue index ded1048..a4cbc5c 100644 --- a/pkg/suse-ai-lifecycle-manager/pages/components/wizard/BasicInfoStep.vue +++ b/pkg/suse-ai-lifecycle-manager/pages/components/wizard/BasicInfoStep.vue @@ -10,11 +10,15 @@ />
-
@@ -68,6 +72,7 @@ interface Props { form: BasicInfoForm; versionOptions: Array<{ label: string; value: string }>; loadingVersions: boolean; + namespaceOptions: Array<{ label: string; value: string }>; } interface Emits { @@ -80,12 +85,6 @@ const emit = defineEmits(); // Simple fallback function for translations const t = (key: string, fallback: string) => fallback; -// Create individual computed properties for better reactivity -const localForm = computed({ - get: () => props.form, - set: (value: BasicInfoForm) => emit('update:form', value) -}); - // Individual field computeds for better reactivity const release = computed({ get: () => props.form.release, @@ -94,7 +93,10 @@ const release = computed({ const namespace = computed({ get: () => props.form.namespace, - set: (value: string) => emit('update:form', { ...props.form, namespace: value }) + set: (value: string | { label: string }) => { + const namespaceName = typeof value === 'object' ? value.label : value; + emit('update:form', { ...props.form, namespace: namespaceName }); + } }); const chartRepo = computed({ diff --git a/pkg/suse-ai-lifecycle-manager/services/rancher-apps.ts b/pkg/suse-ai-lifecycle-manager/services/rancher-apps.ts index 5fa2479..72b6b75 100644 --- a/pkg/suse-ai-lifecycle-manager/services/rancher-apps.ts +++ b/pkg/suse-ai-lifecycle-manager/services/rancher-apps.ts @@ -362,10 +362,13 @@ export async function listCatalogApps($store: RancherStore, clusterId: string): return res?.data?.items || res?.data || res?.items || []; } -async function listNamespaces($store: RancherStore, clusterId: string): Promise { - const url = `/k8s/clusters/${encodeURIComponent(clusterId)}/api/v1/namespaces?limit=5000`; +export async function listNamespaces($store: RancherStore, clusterId: string): Promise { + const url = clusterId === 'local' + ? '/api/v1/namespaces?limit=5000' + : `/k8s/clusters/${encodeURIComponent(clusterId)}/api/v1/namespaces?limit=5000`; const res = await $store.dispatch('rancher/request', { url, timeout: 20000 }); - const items = res?.data?.items || res?.data || []; + const items = res?.data?.items || res?.data || res?.items || []; + return (items || []).map((n: NamespaceResource) => n?.metadata?.name).filter((n: string) => !!n); }