diff --git a/dashboard/pkg/epinio/components/configuration/ConfigurationModal.vue b/dashboard/pkg/epinio/components/configuration/ConfigurationModal.vue index 69c5ddc8..9fa9642d 100644 --- a/dashboard/pkg/epinio/components/configuration/ConfigurationModal.vue +++ b/dashboard/pkg/epinio/components/configuration/ConfigurationModal.vue @@ -1,5 +1,5 @@ @@ -456,16 +521,18 @@ defineExpose({ openCreate, openView, openEdit }); - diff --git a/dashboard/pkg/epinio/components/service/ServiceInstanceModal.vue b/dashboard/pkg/epinio/components/service/ServiceInstanceModal.vue index f83923cd..8f5d7b7d 100644 --- a/dashboard/pkg/epinio/components/service/ServiceInstanceModal.vue +++ b/dashboard/pkg/epinio/components/service/ServiceInstanceModal.vue @@ -1,5 +1,5 @@ @@ -414,17 +531,18 @@ defineExpose({ openCreate, openEdit, openView }); - + :onDropdownChange="(e: CustomEvent) => { formCatalogService = e.detail.value; resetChartValues(); }" + :fetchAllResources="fetchCatalogServices" + :searchResources="searchCatalogServices" + :isLoading="isLoadingCatalogServices" + /> - + diff --git a/dashboard/pkg/epinio/list/configurations.vue b/dashboard/pkg/epinio/list/configurations.vue index 13a88447..e4c109bb 100644 --- a/dashboard/pkg/epinio/list/configurations.vue +++ b/dashboard/pkg/epinio/list/configurations.vue @@ -104,17 +104,34 @@ const canCreateConfiguration = computed(() => { const canEdit = canCreateConfiguration; const canDelete = canCreateConfiguration; +// Watch the active namespace cache key and update the active namespaces in the store +watch( + () => { + void store.state.activeNamespaceCacheKey; + const active = store.state.activeNamespaceCache; + return active ? Object.keys(active) : null; + }, + async (namespacesArray) => { + paginating.value = true; + try { + await store.dispatch('epinio/setActiveNamespaces', { type: resource, namespaces: namespacesArray }); + } finally { + paginating.value = false; + } + + }, + { immediate: true } +); + watchEffect(() => { - void store.state.activeNamespaceCacheKey; - const activeNamespaces = store.state.activeNamespaceCache; const all = store.getters['epinio/all'](EPINIO_TYPES.CONFIGURATION) as any[]; all.forEach((row: any) => { void row.status; void row.stateDisplay; void row.meta; void row.configuration; }); - const filtered = all.filter((row: any) => { - const ns = row.meta?.namespace; - - return !activeNamespaces || Object.keys(activeNamespaces).length === 0 || activeNamespaces[ns]; + // Filter empty rows that are added during delete + const filtered = all.filter((row) => { + if (!row.id) return false; + return true; }); const overrides = [ diff --git a/dashboard/pkg/epinio/list/services.vue b/dashboard/pkg/epinio/list/services.vue index 1d85d784..e80f268c 100644 --- a/dashboard/pkg/epinio/list/services.vue +++ b/dashboard/pkg/epinio/list/services.vue @@ -70,18 +70,33 @@ const canEdit = computed(() => { const canDelete = canEdit; const canCreate = canEdit; -watchEffect(() => { - void store.state.activeNamespaceCacheKey; - const activeNamespaces = store.state.activeNamespaceCache; +// Watch the active namespace cache key and update the active namespaces in the store +watch( + () => { + void store.state.activeNamespaceCacheKey; + const active = store.state.activeNamespaceCache; + return active ? Object.keys(active) : null; + }, + async (namespacesArray) => { + paginating.value = true; + try { + await store.dispatch('epinio/setActiveNamespaces', { type: resource, namespaces: namespacesArray }); + } finally { + paginating.value = false; + } + + }, + { immediate: true } +); + +watchEffect(async () => { const all = store.getters['epinio/all'](EPINIO_TYPES.SERVICE_INSTANCE) as any[]; all.forEach((row: any) => { void row.status; void row.stateDisplay; void row.meta; void row.boundapps; }); - // Filter empty rows that are added during delete, and filter by active namespace + // Filter empty rows that are added during delete const filtered = all.filter((row) => { if (!row.id) return false; - const ns = row.meta?.namespace; - - return !activeNamespaces || Object.keys(activeNamespaces).length === 0 || activeNamespaces[ns]; + return true; }); // Build the row action menu with RBAC gating. The model already gates the diff --git a/dashboard/pkg/epinio/pages/c/_cluster/applications/index.vue b/dashboard/pkg/epinio/pages/c/_cluster/applications/index.vue index 8fb20606..d50185ff 100644 --- a/dashboard/pkg/epinio/pages/c/_cluster/applications/index.vue +++ b/dashboard/pkg/epinio/pages/c/_cluster/applications/index.vue @@ -1,35 +1,73 @@ diff --git a/dashboard/pkg/epinio/store/epinio-store/actions.ts b/dashboard/pkg/epinio/store/epinio-store/actions.ts index 9934db5c..94e454b9 100644 --- a/dashboard/pkg/epinio/store/epinio-store/actions.ts +++ b/dashboard/pkg/epinio/store/epinio-store/actions.ts @@ -562,6 +562,13 @@ export default { await dispatch('findAll', { type, opt: { force: true } }); }, + setActiveNamespaces: async({ commit, dispatch }: any, { type, namespaces }: { type: string; namespaces: string[] | null }) => { + commit('setActiveNamespaces', namespaces); + commit('setPaginationPage', { type, page: 1 }); + commit('clearAll', type); // clear before fetch so merge starts from empty + await dispatch('findAll', { type, opt: { force: true } }); + }, + me: async({ dispatch, commit }: any): Promise => { // Always fetch fresh so permissions reflect the current user (no stale cache after login switch) const me = await dispatch('request', { opt: { url: `/api/v1/me` } }); diff --git a/dashboard/pkg/epinio/store/epinio-store/getters.ts b/dashboard/pkg/epinio/store/epinio-store/getters.ts index 47ea9fc7..0fc02e1f 100644 --- a/dashboard/pkg/epinio/store/epinio-store/getters.ts +++ b/dashboard/pkg/epinio/store/epinio-store/getters.ts @@ -51,6 +51,7 @@ export default { '/api/v1/namespaces': EPINIO_TYPES.NAMESPACE, '/api/v1/configurations': EPINIO_TYPES.CONFIGURATION, '/api/v1/services': EPINIO_TYPES.SERVICE_INSTANCE, + '/api/v1/applications': EPINIO_TYPES.APP, '/api/v1/catalogservices': EPINIO_TYPES.CATALOG_SERVICE, }; @@ -76,6 +77,13 @@ export default { params.delete('search'); } + const namespaces = state.activeNamespaces; + if (namespaces && namespaces.length) { + params.set('namespaces', namespaces.join(',')); + } else { + params.delete('namespaces'); + } + const qs = params.toString(); return qs ? `${ path }?${ qs }` : path; @@ -159,6 +167,8 @@ export default { return out; }, + activeNamespaces: (state: any) => () => state.activeNamespaces, + singleProductCNSI: (state: any) => () => state.singleProductCNSI, info: (state: any) => () => state.info, diff --git a/dashboard/pkg/epinio/store/epinio-store/index.ts b/dashboard/pkg/epinio/store/epinio-store/index.ts index 99cf641e..209352c7 100644 --- a/dashboard/pkg/epinio/store/epinio-store/index.ts +++ b/dashboard/pkg/epinio/store/epinio-store/index.ts @@ -18,7 +18,9 @@ const epinioFactory = (): CoreStoreSpecifics => { searchQuery: {} as Record, // Server-side namespace search results (names) for the navbar filter. // null means no active query -> the filter shows the full list. - namespaceSearch: null as string[] | null + namespaceSearch: null as string[] | null, + // Active namespaces from the top-level filter. null means no active filter -> the list shows all namespaces. + activeNamespaces: null as string[] | null, }; }, diff --git a/dashboard/pkg/epinio/store/epinio-store/mutations.ts b/dashboard/pkg/epinio/store/epinio-store/mutations.ts index 69cdec1b..65cfc7b4 100644 --- a/dashboard/pkg/epinio/store/epinio-store/mutations.ts +++ b/dashboard/pkg/epinio/store/epinio-store/mutations.ts @@ -50,6 +50,10 @@ export default { state.searchQuery[type] = query; }, + setActiveNamespaces(state: any, namespaces: string[] | null) { + state.activeNamespaces = namespaces; + }, + setNamespaceSearch(state: any, names: string[] | null) { state.namespaceSearch = names; }, diff --git a/dashboard/pkg/epinio/utils/namespaces.ts b/dashboard/pkg/epinio/utils/namespaces.ts index 80131c5f..5dba2f1c 100644 --- a/dashboard/pkg/epinio/utils/namespaces.ts +++ b/dashboard/pkg/epinio/utils/namespaces.ts @@ -21,10 +21,12 @@ export function useNamespaces(store: any, opts: UseNamespacesOptions = {}) { const isLoading = ref(false); const classify = (rawData: any[]) => Promise.all( - rawData.map((item: any) => store.dispatch( - 'epinio/create', - { type: EPINIO_TYPES.NAMESPACE, ...item } - )) + rawData.map((item: any) => + store.dispatch('epinio/create', { + type: EPINIO_TYPES.NAMESPACE, + ...item + }) + ) ); const load = async (url: string) => { @@ -47,25 +49,25 @@ export function useNamespaces(store: any, opts: UseNamespacesOptions = {}) { return rows; } - return rows.filter((ns: any) => !!active[ns.meta?.name ?? ns.metadata?.name]); + return rows.filter( + (ns: any) => !!active[ns.meta?.name ?? ns.metadata?.name] + ); }; // Cached, so reopening a dropdown in the same form doesn't refetch async function fetchAll() { if (cached.value.length > 0) { namespaces.value = cached.value; - return; } isLoading.value = true; try { - const rows = await load('/api/v1/namespaces'); - const scoped = opts.scopeToActiveFilter ? inActiveFilter(rows) : rows; + const classifiedData = await load('/api/v1/namespaces'); - namespaces.value = scoped; - cached.value = scoped; + namespaces.value = classifiedData; + cached.value = classifiedData; } catch (e) { if (opts.onError) { opts.onError(e); @@ -81,9 +83,9 @@ export function useNamespaces(store: any, opts: UseNamespacesOptions = {}) { isLoading.value = true; try { - namespaces.value = await load( - `/api/v1/namespaces?search=${ encodeURIComponent(query) }` - ); + const classifiedData = await load(`/api/v1/namespaces?search=${ encodeURIComponent(query) }`); + + namespaces.value = classifiedData; } catch { namespaces.value = []; } finally { @@ -91,24 +93,51 @@ export function useNamespaces(store: any, opts: UseNamespacesOptions = {}) { } } - // Read-only namespace fields (view/edit) only need the record's own namespace - // present for the label to render. Leaves the cache empty so a later create - // still loads the full list. function seed(name: string) { namespaces.value = name ? [{ meta: { name } }] : []; cached.value = []; } - const sorted = computed(() => sortBy(namespaces.value, (ns: any) => ns.meta?.name) as any[]); + const filteredNamespaces = computed(() => { + if (!opts.scopeToActiveFilter) { + return namespaces.value; + } - const options = computed(() => sorted.value.map((ns: any) => ({ - label: ns.meta?.name || '', - value: ns.meta?.name || '', - }))); + // Access the reactive store state inside the computed. + const active = store.state.activeNamespaceCache; + + if (!active || Object.keys(active).length === 0) { + return namespaces.value; + } - const firstName = computed(() => sorted.value[0]?.meta?.name || ''); + return inActiveFilter(namespaces.value); + }); + + const sorted = computed(() => + sortBy( + filteredNamespaces.value, + (ns: any) => ns.meta?.name + ) as any[] + ); + + const options = computed(() => + sorted.value.map((ns: any) => ({ + label: ns.meta?.name || '', + value: ns.meta?.name || '', + })) + ); + + const firstName = computed( + () => sorted.value[0]?.meta?.name || '' + ); return { - namespaces: sorted, options, isLoading, firstName, fetchAll, search, seed + namespaces: sorted, + options, + isLoading, + firstName, + fetchAll, + search, + seed }; } diff --git a/dashboard/pkg/epinio/utils/table-formatters.ts b/dashboard/pkg/epinio/utils/table-formatters.ts index 0d1e5b8e..89b6dff1 100644 --- a/dashboard/pkg/epinio/utils/table-formatters.ts +++ b/dashboard/pkg/epinio/utils/table-formatters.ts @@ -22,6 +22,10 @@ export function makeRouterLinks( ): HTMLSpanElement { const span = document.createElement('span'); + span.style.whiteSpace = 'normal'; + span.style.overflowWrap = 'anywhere'; + span.style.wordBreak = 'break-word'; + items.forEach((item, index) => { const a = document.createElement('a');