diff --git a/.changeset/fix-catalog-filter-flicker.md b/.changeset/fix-catalog-filter-flicker.md new file mode 100644 index 0000000000..5964f7ada9 --- /dev/null +++ b/.changeset/fix-catalog-filter-flicker.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Fixed a UI flicker in the catalog entity list where changing a filter would briefly flash stale data before showing the new results. diff --git a/.changeset/fix-catalog-table-loading-flash.md b/.changeset/fix-catalog-table-loading-flash.md new file mode 100644 index 0000000000..1e75a6d7af --- /dev/null +++ b/.changeset/fix-catalog-table-loading-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Fixed the catalog table briefly showing an empty loading state when changing filters. The table now keeps displaying stale results until new data arrives. diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index 12eb9dff6c..a18575befa 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -454,30 +454,36 @@ export const EntityListProvider = ( [paginationMode], ); + // Use resolvedValue directly when available to avoid an extra render cycle. + // Without this, there's a render where loading has flipped back to false but + // outputState hasn't been updated yet (it syncs via useEffect), causing a + // flash of stale data between the loading state and the new results. + const latestOutput = resolvedValue ?? outputState; + const pageInfo = useMemo(() => { if (paginationMode !== 'cursor') { return undefined; } - const prevCursor = outputState.pageInfo?.prevCursor; - const nextCursor = outputState.pageInfo?.nextCursor; + const prevCursor = latestOutput.pageInfo?.prevCursor; + const nextCursor = latestOutput.pageInfo?.nextCursor; return { prev: prevCursor ? () => setCursor(prevCursor) : undefined, next: nextCursor ? () => setCursor(nextCursor) : undefined, }; - }, [paginationMode, outputState.pageInfo]); + }, [paginationMode, latestOutput.pageInfo]); const value = useMemo( () => ({ - filters: outputState.appliedFilters, - entities: outputState.entities, - backendEntities: outputState.backendEntities, + filters: latestOutput.appliedFilters, + entities: latestOutput.entities, + backendEntities: latestOutput.backendEntities, updateFilters, queryParameters, loading, error, pageInfo, - totalItems: outputState.totalItems, + totalItems: latestOutput.totalItems, limit, offset, setLimit, @@ -485,7 +491,7 @@ export const EntityListProvider = ( paginationMode, }), [ - outputState, + latestOutput, updateFilters, queryParameters, loading, diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 7875431246..80b37002c5 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -34,6 +34,7 @@ import { useEntityList, useStarredEntities, } from '@backstage/plugin-catalog-react'; +import CircularProgress from '@material-ui/core/CircularProgress'; import Typography from '@material-ui/core/Typography'; import { visuallyHidden } from '@mui/utils'; import Edit from '@material-ui/icons/Edit'; @@ -108,6 +109,14 @@ export const CatalogTable = (props: CatalogTableProps) => { 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; + const tableColumns = useMemo( () => typeof columns === 'function' ? columns(entityListContext) : columns, @@ -189,17 +198,28 @@ export const CatalogTable = (props: CatalogTableProps) => { const titlePreamble = capitalize( filters.user?.value ?? t('catalogTable.allFilters'), ); - const title = + const titleText = props.title || [titlePreamble, currentType, pluralize(currentKind), currentCount] .filter(s => s) .join(' '); + const title = + loading && !isLoading ? ( + + {titleText} + + + ) : ( + titleText + ); const actions = props.actions || defaultActions; const options: TableProps['options'] = { actionsColumnIndex: -1, loadingType: 'linear' as const, - showEmptyDataSourceMessage: !loading, + showEmptyDataSourceMessage: !isLoading, padding: 'dense' as const, ...tableOptions, }; @@ -209,7 +229,7 @@ export const CatalogTable = (props: CatalogTableProps) => { { { return ( - isLoading={loading} + isLoading={isLoading} columns={tableColumns} options={{ paging: showPagination,