Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion pkg/suse-ai-lifecycle-manager/pages/components/AppWizard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -67,6 +68,7 @@ const versionInfo = ref<any | null>(null);
const questionsLoading = ref(false);
const versionInfoKey = ref('');
const defaultValuesSnapshot = ref<Record<string, any>>({});
const namespaceOptions = ref<{label: string, value: string}[]>([]);
// Multi-cluster install progress state
const showProgressModal = ref(false);
Expand All @@ -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<string>();
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(() => [
{
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -969,6 +1018,7 @@ function previousStep() {
v-model:form="basicInfoForm"
:version-options="versionOptions"
:loading-versions="loadingVersions"
:namespace-options="namespaceOptions"
/>

<!-- Step: Target Cluster -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@
/>
</div>
<div class="col span-6">
<LabeledInput
<LabeledSelect
v-model:value="namespace"
:label="t('suseai.wizard.form.namespace', 'Namespace')"
:placeholder="t('suseai.wizard.form.namespacePlaceholder', 'Enter namespace')"
required
:options="namespaceOptions"
:placeholder="t('suseai.wizard.form.namespacePlaceholder', 'Select or create a namespace')"
:taggable="true"
:searchable="true"
:clearable="false"
:required="true"
/>
</div>
</div>
Expand Down Expand Up @@ -68,6 +72,7 @@ interface Props {
form: BasicInfoForm;
versionOptions: Array<{ label: string; value: string }>;
loadingVersions: boolean;
namespaceOptions: Array<{ label: string; value: string }>;
}
interface Emits {
Expand All @@ -80,12 +85,6 @@ const emit = defineEmits<Emits>();
// 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,
Expand All @@ -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({
Expand Down
9 changes: 6 additions & 3 deletions pkg/suse-ai-lifecycle-manager/services/rancher-apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]> {
const url = `/k8s/clusters/${encodeURIComponent(clusterId)}/api/v1/namespaces?limit=5000`;
export async function listNamespaces($store: RancherStore, clusterId: string): Promise<string[]> {
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);
}

Expand Down