diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 5634b56df4..7f45bf81e9 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -81,20 +81,20 @@ export const catalogReactTranslationRef: TranslationRef< readonly 'inspectEntityDialog.overviewPage.metadata.title': 'Metadata'; readonly 'inspectEntityDialog.overviewPage.labels': 'Labels'; readonly 'inspectEntityDialog.overviewPage.status.title': 'Status'; - readonly 'inspectEntityDialog.overviewPage.identity.title': 'Identity'; - readonly 'inspectEntityDialog.overviewPage.tags': 'Tags'; - readonly 'inspectEntityDialog.overviewPage.annotations': 'Annotations'; readonly 'inspectEntityDialog.overviewPage.relation.title': 'Relations'; + readonly 'inspectEntityDialog.overviewPage.annotations': 'Annotations'; + readonly 'inspectEntityDialog.overviewPage.tags': 'Tags'; readonly 'inspectEntityDialog.overviewPage.copyAriaLabel': 'Copy {{label}}'; readonly 'inspectEntityDialog.overviewPage.copiedStatus': 'Copied'; readonly 'inspectEntityDialog.overviewPage.helpLinkAriaLabel': 'Learn more'; + readonly 'inspectEntityDialog.overviewPage.identity.title': 'Identity'; readonly 'inspectEntityDialog.yamlPage.title': 'Entity as YAML'; readonly 'inspectEntityDialog.yamlPage.description': 'This is the raw entity data as received from the catalog, on YAML form.'; readonly 'inspectEntityDialog.tabNames.json': 'Raw JSON'; - readonly 'inspectEntityDialog.tabNames.yaml': 'Raw YAML'; readonly 'inspectEntityDialog.tabNames.overview': 'Overview'; readonly 'inspectEntityDialog.tabNames.ancestry': 'Ancestry'; readonly 'inspectEntityDialog.tabNames.colocated': 'Colocated'; + readonly 'inspectEntityDialog.tabNames.yaml': 'Raw YAML'; readonly 'unregisterEntityDialog.title': 'Are you sure you want to unregister this entity?'; readonly 'unregisterEntityDialog.cancelButtonTitle': 'Cancel'; readonly 'unregisterEntityDialog.deleteButtonTitle': 'Delete Entity'; @@ -120,13 +120,13 @@ export const catalogReactTranslationRef: TranslationRef< readonly 'entityTableColumnTitle.label': 'Label'; readonly 'entityTableColumnTitle.title': 'Title'; readonly 'entityTableColumnTitle.description': 'Description'; + readonly 'entityTableColumnTitle.domain': 'Domain'; readonly 'entityTableColumnTitle.system': 'System'; readonly 'entityTableColumnTitle.namespace': 'Namespace'; - readonly 'entityTableColumnTitle.tags': 'Tags'; - readonly 'entityTableColumnTitle.domain': 'Domain'; - readonly 'entityTableColumnTitle.owner': 'Owner'; readonly 'entityTableColumnTitle.lifecycle': 'Lifecycle'; + readonly 'entityTableColumnTitle.owner': 'Owner'; readonly 'entityTableColumnTitle.targets': 'Targets'; + readonly 'entityTableColumnTitle.tags': 'Tags'; readonly 'entityRelationCard.emptyHelpLinkTitle': 'Learn how to change this.'; readonly 'missingAnnotationEmptyState.title': 'Missing Annotation'; readonly 'missingAnnotationEmptyState.readMore': 'Read more'; @@ -666,16 +666,16 @@ export const EntityTableColumnTitle: ( input: EntityTableColumnTitleProps, ) => | 'System' - | 'Title' | 'Domain' + | 'Name' + | 'Description' | 'Lifecycle' | 'Namespace' | 'Owner' | 'Tags' | 'Type' - | 'Name' - | 'Description' | 'Targets' + | 'Title' | 'Label'; // @alpha (undocumented) diff --git a/plugins/catalog-react/report.api.md b/plugins/catalog-react/report.api.md index 1fcac904ff..6614ec729e 100644 --- a/plugins/catalog-react/report.api.md +++ b/plugins/catalog-react/report.api.md @@ -203,20 +203,20 @@ export const catalogReactTranslationRef: TranslationRef< readonly 'inspectEntityDialog.overviewPage.metadata.title': 'Metadata'; readonly 'inspectEntityDialog.overviewPage.labels': 'Labels'; readonly 'inspectEntityDialog.overviewPage.status.title': 'Status'; - readonly 'inspectEntityDialog.overviewPage.identity.title': 'Identity'; - readonly 'inspectEntityDialog.overviewPage.tags': 'Tags'; - readonly 'inspectEntityDialog.overviewPage.annotations': 'Annotations'; readonly 'inspectEntityDialog.overviewPage.relation.title': 'Relations'; + readonly 'inspectEntityDialog.overviewPage.annotations': 'Annotations'; + readonly 'inspectEntityDialog.overviewPage.tags': 'Tags'; readonly 'inspectEntityDialog.overviewPage.copyAriaLabel': 'Copy {{label}}'; readonly 'inspectEntityDialog.overviewPage.copiedStatus': 'Copied'; readonly 'inspectEntityDialog.overviewPage.helpLinkAriaLabel': 'Learn more'; + readonly 'inspectEntityDialog.overviewPage.identity.title': 'Identity'; readonly 'inspectEntityDialog.yamlPage.title': 'Entity as YAML'; readonly 'inspectEntityDialog.yamlPage.description': 'This is the raw entity data as received from the catalog, on YAML form.'; readonly 'inspectEntityDialog.tabNames.json': 'Raw JSON'; - readonly 'inspectEntityDialog.tabNames.yaml': 'Raw YAML'; readonly 'inspectEntityDialog.tabNames.overview': 'Overview'; readonly 'inspectEntityDialog.tabNames.ancestry': 'Ancestry'; readonly 'inspectEntityDialog.tabNames.colocated': 'Colocated'; + readonly 'inspectEntityDialog.tabNames.yaml': 'Raw YAML'; readonly 'unregisterEntityDialog.title': 'Are you sure you want to unregister this entity?'; readonly 'unregisterEntityDialog.cancelButtonTitle': 'Cancel'; readonly 'unregisterEntityDialog.deleteButtonTitle': 'Delete Entity'; @@ -242,13 +242,13 @@ export const catalogReactTranslationRef: TranslationRef< readonly 'entityTableColumnTitle.label': 'Label'; readonly 'entityTableColumnTitle.title': 'Title'; readonly 'entityTableColumnTitle.description': 'Description'; + readonly 'entityTableColumnTitle.domain': 'Domain'; readonly 'entityTableColumnTitle.system': 'System'; readonly 'entityTableColumnTitle.namespace': 'Namespace'; - readonly 'entityTableColumnTitle.tags': 'Tags'; - readonly 'entityTableColumnTitle.domain': 'Domain'; - readonly 'entityTableColumnTitle.owner': 'Owner'; readonly 'entityTableColumnTitle.lifecycle': 'Lifecycle'; + readonly 'entityTableColumnTitle.owner': 'Owner'; readonly 'entityTableColumnTitle.targets': 'Targets'; + readonly 'entityTableColumnTitle.tags': 'Tags'; readonly 'entityRelationCard.emptyHelpLinkTitle': 'Learn how to change this.'; readonly 'missingAnnotationEmptyState.title': 'Missing Annotation'; readonly 'missingAnnotationEmptyState.readMore': 'Read more'; @@ -466,6 +466,7 @@ export type EntityListContextProps< prev?: () => void; }; totalItems?: number; + totalItemsLoading: boolean; limit: number; offset?: number; setLimit: (limit: number) => void; diff --git a/plugins/catalog-react/src/deprecated.tsx b/plugins/catalog-react/src/deprecated.tsx index 7d347dda6b..dcb176dff4 100644 --- a/plugins/catalog-react/src/deprecated.tsx +++ b/plugins/catalog-react/src/deprecated.tsx @@ -73,6 +73,7 @@ export function MockEntityListContextProvider< error: value?.error, totalItems: value?.totalItems ?? (value?.entities ?? defaultValues.entities).length, + totalItemsLoading: value?.totalItemsLoading ?? false, limit: value?.limit ?? 20, offset: value?.offset, setLimit: value?.setLimit ?? (() => {}), diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index a1f358d739..e0abfccfbd 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -1239,6 +1239,7 @@ describe('versioned context', () => { updateFilters: jest.fn(), queryParameters: {}, loading: true, + totalItemsLoading: false, limit: 277, setLimit: jest.fn(), setOffset: jest.fn(), diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index 7feb079aae..e42e391ee9 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -121,6 +121,7 @@ export type EntityListContextProps< prev?: () => void; }; totalItems?: number; + totalItemsLoading: boolean; limit: number; offset?: number; setLimit: (limit: number) => void; @@ -350,25 +351,26 @@ export const EntityListProvider = ( // decoupled from the main list fetch so that page navigation doesn't // re-run the expensive count query, and so that the count can arrive // asynchronously without blocking the list response. - const [{ value: asyncTotalItems }, refreshCount] = useAsyncFn(async () => { - if (paginationMode === 'none') { - return undefined; - } - const compacted = compact(Object.values(adjustedFilters)); - if (compacted.length === 0) { - return undefined; - } - const backendFilter = reduceCatalogFilters(compacted); - try { - const response = await catalogApi.queryEntities({ - ...backendFilter, - limit: 0, - }); - return response.totalItems; - } catch { - return undefined; - } - }, [catalogApi, paginationMode, adjustedFilters]); + const [{ value: totalItems, loading: totalItemsLoading }, refreshCount] = + useAsyncFn(async () => { + if (paginationMode === 'none') { + return undefined; + } + const compacted = compact(Object.values(adjustedFilters)); + if (compacted.length === 0) { + return undefined; + } + const backendFilter = reduceCatalogFilters(compacted); + try { + const response = await catalogApi.queryEntities({ + ...backendFilter, + limit: 0, + }); + return response.totalItems; + } catch { + return undefined; + } + }, [catalogApi, paginationMode, adjustedFilters]); useDebounce(refreshCount, 10, [adjustedFilters]); @@ -475,7 +477,8 @@ export const EntityListProvider = ( totalItems: paginationMode === 'none' ? entities.length - : asyncTotalItems ?? backendState.totalItems, + : totalItems ?? backendState.totalItems, + totalItemsLoading: paginationMode !== 'none' && totalItemsLoading, limit, offset, setLimit, @@ -486,7 +489,8 @@ export const EntityListProvider = ( requestedFilters, entities, backendState, - asyncTotalItems, + totalItems, + totalItemsLoading, updateFilters, queryParameters, loading, diff --git a/plugins/catalog-react/src/testUtils/MockEntityListContextProvider.tsx b/plugins/catalog-react/src/testUtils/MockEntityListContextProvider.tsx index e75e7e20e3..c13821b815 100644 --- a/plugins/catalog-react/src/testUtils/MockEntityListContextProvider.tsx +++ b/plugins/catalog-react/src/testUtils/MockEntityListContextProvider.tsx @@ -73,6 +73,7 @@ export function MockEntityListContextProvider< error: value?.error, totalItems: value?.totalItems ?? (value?.entities ?? defaultValues.entities).length, + totalItemsLoading: value?.totalItemsLoading ?? false, limit: value?.limit ?? 20, offset: value?.offset, setLimit: value?.setLimit ?? (() => {}), diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index 0d81753653..76a6c35b35 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -126,7 +126,8 @@ describe('CatalogTable component', () => { }, }, ); - expect(screen.getByText(/Owned Components \(3\)/)).toBeInTheDocument(); + expect(screen.getByText(/Owned Components/)).toBeInTheDocument(); + expect(screen.getByText(/\(3\)/)).toBeInTheDocument(); expect(screen.getByText(/component1/)).toBeInTheDocument(); expect(screen.getByText(/component2/)).toBeInTheDocument(); expect(screen.getByText(/component3/)).toBeInTheDocument(); diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index b021aae0e1..e76f4ef27a 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -110,16 +110,15 @@ export const CatalogTable = (props: CatalogTableProps) => { filters, pageInfo, totalItems, + totalItemsLoading, paginationMode, } = entityListContext; - // For non-paginated tables, only show the full loading indicator when - // there's no data yet (initial load). During filter changes we keep stale - // data visible and let the new results swap in seamlessly. For paginated - // tables we always show loading, since stale data from a different page - // would be misleading. - const isLoading = - paginationMode === 'none' ? loading && entities.length === 0 : loading; + // Only show the full-table loading spinner on the very first load when + // there's no data yet. During subsequent fetches (filter changes, page + // navigation) we keep stale rows visible so the user sees content + // immediately — a small spinner next to the title signals the refresh. + const isLoading = loading && entities.length === 0; const tableColumns = useMemo( () => @@ -202,22 +201,26 @@ export const CatalogTable = (props: CatalogTableProps) => { const titlePreamble = capitalize( filters.user?.value ?? t('catalogTable.allFilters'), ); - const titleText = + const titleBase = props.title || - [titlePreamble, currentType, pluralize(currentKind), currentCount] + [titlePreamble, currentType, pluralize(currentKind)] .filter(s => s) .join(' '); - const title = - loading && !isLoading ? ( - - {titleText} + const title = ( + + {titleBase} + {currentCount && ( + + {currentCount} + + )} + {loading && !isLoading && ( - - ) : ( - titleText - ); + )} + + ); const actions = props.actions || defaultActions; const options: TableProps['options'] = {