From 378bfd085b6cc1303106c6a7c08c0d9027cc4b86 Mon Sep 17 00:00:00 2001 From: Ashraf Masarwa Date: Tue, 9 Jun 2026 17:27:33 +0300 Subject: [PATCH 1/2] FLPATH-4262 | [DCM] Provider health status is static with no refresh or polling --- .../dcm/.changeset/provider-status-refresh.md | 7 +++ .../dcm/src/components/DcmCrudTabLayout.tsx | 47 ++++++++++++++++--- .../dcm/plugins/dcm/src/hooks/useCrudTab.ts | 21 +++++++-- .../pages/providers/ProvidersTabContent.tsx | 2 + 4 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 workspaces/dcm/.changeset/provider-status-refresh.md diff --git a/workspaces/dcm/.changeset/provider-status-refresh.md b/workspaces/dcm/.changeset/provider-status-refresh.md new file mode 100644 index 0000000000..a7898c6997 --- /dev/null +++ b/workspaces/dcm/.changeset/provider-status-refresh.md @@ -0,0 +1,7 @@ +--- +'@red-hat-developer-hub/backstage-plugin-dcm': patch +--- + +Add a manual refresh button to the Providers table to update health status without a full page reload. + +A sync icon button now appears next to the search field in the Providers card header. Clicking it re-fetches the provider list (including `health_status`) while keeping the table visible. A spinner is shown on the button during the request. The initial page load behaviour is unchanged. diff --git a/workspaces/dcm/plugins/dcm/src/components/DcmCrudTabLayout.tsx b/workspaces/dcm/plugins/dcm/src/components/DcmCrudTabLayout.tsx index 5e29d991ea..16252c0f10 100644 --- a/workspaces/dcm/plugins/dcm/src/components/DcmCrudTabLayout.tsx +++ b/workspaces/dcm/plugins/dcm/src/components/DcmCrudTabLayout.tsx @@ -20,7 +20,14 @@ import { InfoCard, Progress, } from '@backstage/core-components'; -import { Box, Button } from '@material-ui/core'; +import { + Box, + Button, + CircularProgress, + IconButton, + Tooltip, +} from '@material-ui/core'; +import SyncIcon from '@material-ui/icons/Sync'; import { Dispatch, SetStateAction } from 'react'; import MuiAlert from '@material-ui/lab/Alert'; import type { BoxProps } from '@material-ui/core/Box'; @@ -67,6 +74,12 @@ export type DcmCrudTabLayoutProps = Readonly<{ // ── Card header ────────────────────────────────────────────────────────── entityLabel: string; + + // ── Refresh ────────────────────────────────────────────────────────────── + /** When provided, a refresh icon button is shown next to the search field. */ + onRefresh?: () => void; + /** When true, the refresh button shows a spinner instead of the sync icon. */ + refreshing?: boolean; }>; function ActionErrorAlert({ @@ -127,6 +140,8 @@ export function DcmCrudTabLayout({ onPrimaryAction, illustrationSrc, entityLabel, + onRefresh, + refreshing, }: DcmCrudTabLayoutProps) { const classes = useDcmStyles(); @@ -183,11 +198,31 @@ export function DcmCrudTabLayout({ + + + {onRefresh && ( + + + + {refreshing ? ( + + ) : ( + + )} + + + + )} + } className={classes.dataCard} titleTypographyProps={{ className: classes.cardTitle }} diff --git a/workspaces/dcm/plugins/dcm/src/hooks/useCrudTab.ts b/workspaces/dcm/plugins/dcm/src/hooks/useCrudTab.ts index 0540fb9007..cdb0522926 100644 --- a/workspaces/dcm/plugins/dcm/src/hooks/useCrudTab.ts +++ b/workspaces/dcm/plugins/dcm/src/hooks/useCrudTab.ts @@ -99,6 +99,8 @@ export interface UseCrudTabResult> { items: T[]; setItems: React.Dispatch>; loading: boolean; + /** True only during a manual reload after the first successful load. The table stays visible. */ + refreshing: boolean; loadError: string | null; reload: () => void; @@ -181,7 +183,9 @@ export function useCrudTab>( // ── List ───────────────────────────────────────────────────────────────── const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); const [loadError, setLoadError] = useState(null); + const hasLoadedRef = useRef(false); // ── Search + pagination ────────────────────────────────────────────────── const [search, setSearch] = useState(''); @@ -232,16 +236,26 @@ export function useCrudTab>( // ── Load ───────────────────────────────────────────────────────────────── const reload = useCallback(() => { - setLoading(true); + if (hasLoadedRef.current) { + setRefreshing(true); + } else { + setLoading(true); + } setLoadError(null); optsRef.current .loadFn() - .then(setItems) + .then(result => { + setItems(result); + hasLoadedRef.current = true; + }) .catch(err => { setLoadError(extractApiError(err)); setItems([]); }) - .finally(() => setLoading(false)); + .finally(() => { + setLoading(false); + setRefreshing(false); + }); }, []); // stable — reads via ref useEffect(() => { @@ -386,6 +400,7 @@ export function useCrudTab>( items, setItems, loading, + refreshing, loadError, reload, diff --git a/workspaces/dcm/plugins/dcm/src/pages/providers/ProvidersTabContent.tsx b/workspaces/dcm/plugins/dcm/src/pages/providers/ProvidersTabContent.tsx index 79f6f6f8ca..06767b5cbd 100644 --- a/workspaces/dcm/plugins/dcm/src/pages/providers/ProvidersTabContent.tsx +++ b/workspaces/dcm/plugins/dcm/src/pages/providers/ProvidersTabContent.tsx @@ -310,6 +310,8 @@ export function ProvidersTabContent() { onPrimaryAction={crud.handleOpenCreate} illustrationSrc={emptyIllustration} entityLabel="Providers" + onRefresh={crud.reload} + refreshing={crud.refreshing} /> {formDialog({ From 2c1c175fa5e1adfd3cf5f26fa253488f3bf8f7a9 Mon Sep 17 00:00:00 2001 From: Ashraf Masarwa Date: Wed, 10 Jun 2026 10:45:12 +0300 Subject: [PATCH 2/2] Reset hasLoadedRef on Failure --- .../plugins/dcm/src/hooks/useCrudTab.test.ts | 26 +++++++++++++++++++ .../dcm/plugins/dcm/src/hooks/useCrudTab.ts | 1 + 2 files changed, 27 insertions(+) diff --git a/workspaces/dcm/plugins/dcm/src/hooks/useCrudTab.test.ts b/workspaces/dcm/plugins/dcm/src/hooks/useCrudTab.test.ts index a76065848f..8e10fe63c7 100644 --- a/workspaces/dcm/plugins/dcm/src/hooks/useCrudTab.test.ts +++ b/workspaces/dcm/plugins/dcm/src/hooks/useCrudTab.test.ts @@ -284,6 +284,32 @@ describe('useCrudTab', () => { expect(loadFn).toHaveBeenCalledTimes(2); }); + it('sets refreshing=true and loading=false during a manual reload', async () => { + let resolveSecond!: (items: Item[]) => void; + const loadFn = jest + .fn() + .mockResolvedValueOnce([...ITEMS]) + .mockImplementationOnce( + () => + new Promise(r => { + resolveSecond = r; + }), + ); + + const { result } = renderHook(() => + useCrudTab(makeOptions({ loadFn })), + ); + await waitFor(() => expect(result.current.loading).toBe(false)); + + act(() => result.current.reload()); + + expect(result.current.refreshing).toBe(true); + expect(result.current.loading).toBe(false); + + act(() => resolveSecond([...ITEMS])); + await waitFor(() => expect(result.current.refreshing).toBe(false)); + }); + it('clears loadError on successful reload', async () => { let callCount = 0; const loadFn = jest.fn().mockImplementation(() => { diff --git a/workspaces/dcm/plugins/dcm/src/hooks/useCrudTab.ts b/workspaces/dcm/plugins/dcm/src/hooks/useCrudTab.ts index cdb0522926..93f09e4a9b 100644 --- a/workspaces/dcm/plugins/dcm/src/hooks/useCrudTab.ts +++ b/workspaces/dcm/plugins/dcm/src/hooks/useCrudTab.ts @@ -251,6 +251,7 @@ export function useCrudTab>( .catch(err => { setLoadError(extractApiError(err)); setItems([]); + hasLoadedRef.current = false; }) .finally(() => { setLoading(false);