diff --git a/dashboard/pkg/epinio/config/epinio.ts b/dashboard/pkg/epinio/config/epinio.ts index 74d0d115..0476e4cd 100644 --- a/dashboard/pkg/epinio/config/epinio.ts +++ b/dashboard/pkg/epinio/config/epinio.ts @@ -12,13 +12,13 @@ export const BLANK_CLUSTER = '_'; // function to watch epinio route so css overrides only apply on epinio pages const watchEpinioRoute = () => { const observer = new MutationObserver(() => { - const isEpinio = window.location.pathname.includes('epinio'); + const isEpinio = window.location.pathname.startsWith('/epinio'); document.body.classList.toggle('epinio-active', isEpinio); }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] }); - document.body.classList.toggle('epinio-active', window.location.pathname.includes('epinio')); + document.body.classList.toggle('epinio-active', window.location.pathname.startsWith('/epinio')); } export function init($plugin: any, store: any) { diff --git a/dashboard/pkg/epinio/pages/c/_cluster/applications/index.vue b/dashboard/pkg/epinio/pages/c/_cluster/applications/index.vue index 81580080..ba2a1627 100644 --- a/dashboard/pkg/epinio/pages/c/_cluster/applications/index.vue +++ b/dashboard/pkg/epinio/pages/c/_cluster/applications/index.vue @@ -283,15 +283,21 @@ function handleDeleted(app: any) { onMounted(async () => { window.addEventListener('resize', onResize); - const namespaces = visibleNamespaceNames(); - - await Promise.all([ + const [,,, grouped] = await Promise.all([ store.dispatch('epinio/me'), store.dispatch('epinio/findAll', { type: EPINIO_TYPES.CONFIGURATION }), store.dispatch('epinio/findAll', { type: EPINIO_TYPES.SERVICE_INSTANCE }), - ...namespaces.map(ns => fetchNamespaceApps(ns, 1, '', false)), + store.dispatch('epinio/findGroupedApps'), ]); + // ?? {} so a failed grouped fetch degrades to an empty loop rather than throwing + for (const [ns, nsData] of Object.entries(grouped ?? {})) { + const { items, meta } = nsData as { items: any[]; meta: any }; + // spread-replace instead of direct mutation so Vue tracks the change + namespaceRows.value = { ...namespaceRows.value, [ns]: items }; + namespaceMeta.value = { ...namespaceMeta.value, [ns]: meta }; + } + pending.value = false; startPolling(['namespaces', 'configurations', 'services'], store); @@ -300,10 +306,17 @@ onMounted(async () => { appModal.value?.openCreate(); } - appsPollIntervalId = window.setInterval(() => { - visibleNamespaceNames().forEach(ns => { - fetchNamespaceApps(ns, namespaceCurrentPages.value[ns] ?? 1, searchQueries.value[ns] ?? '', true); - }); + appsPollIntervalId = window.setInterval(async() => { + // Sequential await respects per-namespace page/search state and avoids + // firing all calls simultaneously through the k8s client rate limiter. + for (const ns of visibleNamespaceNames()) { + await fetchNamespaceApps( + ns, + namespaceCurrentPages.value[ns] ?? 1, + searchQueries.value[ns] ?? '', + true, + ); + } }, APPS_POLL_RATE_MS); }); @@ -336,7 +349,7 @@ onUnmounted(() => { -
+

Loading applications... @@ -402,16 +415,16 @@ onUnmounted(() => { .namespace-group { margin-bottom: 2rem; - &:last-child { - margin-bottom: 0; - } - trailhand-table { --sortable-table-row-hover-bg: var(--sortable-table-hover-bg); --sortable-table-header-hover-bg: var(--sortable-table-hover-bg); --sortable-table-header-sorted-bg: var(--sortable-table-hover-bg); overflow-wrap: anywhere; } + + &:last-child { + margin-bottom: 0; + } } .namespace-group-header { diff --git a/dashboard/pkg/epinio/store/epinio-store/actions.ts b/dashboard/pkg/epinio/store/epinio-store/actions.ts index 841776f6..7e1e223c 100644 --- a/dashboard/pkg/epinio/store/epinio-store/actions.ts +++ b/dashboard/pkg/epinio/store/epinio-store/actions.ts @@ -386,6 +386,54 @@ export default { return info; }, + + /** + * Fetch page 1 of apps for every namespace in a single server call. + * Returns a map of namespace → { items, meta } matching findAppsInNamespace's shape. + * Falls back to an empty map on error so the UI can degrade gracefully. + */ + findGroupedApps: async( + ctx: any, + { page = 1, pageSize = 10, search = '' }: { + page?: number; + pageSize?: number; + search?: string + } = {} + ) => { + const { dispatch } = ctx; + let url = `/api/v1/applications/grouped?page=${ page }&pageSize=${ pageSize }`; + if (search) { + url += `&search=${ encodeURIComponent(search) }`; + } + + // The request action runs epiniofy on the response, which spreads the namespace + // map and injects id/type keys. The guard below skips those non-namespace entries. + const grouped: Record = + await dispatch('request', { opt: { url, _skipPaginationMeta: true } }) ?? {}; + + const appSchema = ctx.getters.schemaFor(EPINIO_TYPES.APP); + const result: Record = {}; + for (const [ns, nsData] of Object.entries(grouped)) { + if (!nsData || typeof nsData !== 'object' || !Array.isArray((nsData as any).items)) { + continue; + } + const items = (nsData.items ?? []).map((item: any) => + classify(ctx, epiniofy(item, appSchema, EPINIO_TYPES.APP)) + ); + result[ns] = { + items, + meta: { + page: nsData.page, + pageSize: nsData.pageSize, + totalItems: nsData.totalItems, + totalPages: nsData.totalPages, + }, + }; + } + + return result; + }, + /** * Fetch a single page of applications scoped to a specific namespace. * Does NOT touch global paginationMeta so per-namespace tables stay independent. diff --git a/dashboard/pkg/epinio/utils/polling.ts b/dashboard/pkg/epinio/utils/polling.ts index b7faf5dd..508be8c2 100644 --- a/dashboard/pkg/epinio/utils/polling.ts +++ b/dashboard/pkg/epinio/utils/polling.ts @@ -7,7 +7,7 @@ import { _MERGE } from '@shell/plugins/dashboard-store/actions'; * polling on specific resource types as needed by particular lists or pages. */ -const pollingRate = 15000; //15 seconds +const pollingRate = 30000; //30 seconds const polling: any = {}; export function startPolling(types: string[], store: any): any {