Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions workspaces/dcm/.changeset/provider-status-refresh.md
Original file line number Diff line number Diff line change
@@ -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.
47 changes: 41 additions & 6 deletions workspaces/dcm/plugins/dcm/src/components/DcmCrudTabLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -67,6 +74,12 @@ export type DcmCrudTabLayoutProps<T extends object> = 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({
Expand Down Expand Up @@ -127,6 +140,8 @@ export function DcmCrudTabLayout<T extends object>({
onPrimaryAction,
illustrationSrc,
entityLabel,
onRefresh,
refreshing,
}: DcmCrudTabLayoutProps<T>) {
const classes = useDcmStyles();

Expand Down Expand Up @@ -183,11 +198,31 @@ export function DcmCrudTabLayout<T extends object>({
<InfoCard
title={`${entityLabel} (${filtered.length})`}
action={
<DcmSearchCardAction
value={search}
setValue={onSearchChange}
classes={classes}
/>
<Box display="flex" alignItems="center">
<DcmSearchCardAction
value={search}
setValue={onSearchChange}
classes={classes}
/>
{onRefresh && (
<Tooltip title="Refresh">
<span>
<IconButton
size="small"
aria-label="Refresh"
onClick={onRefresh}
disabled={refreshing}
>
{refreshing ? (
<CircularProgress size={16} />
) : (
<SyncIcon fontSize="small" />
)}
</IconButton>
</span>
</Tooltip>
)}
</Box>
}
className={classes.dataCard}
titleTypographyProps={{ className: classes.cardTitle }}
Expand Down
26 changes: 26 additions & 0 deletions workspaces/dcm/plugins/dcm/src/hooks/useCrudTab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Item, Form>(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(() => {
Expand Down
22 changes: 19 additions & 3 deletions workspaces/dcm/plugins/dcm/src/hooks/useCrudTab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export interface UseCrudTabResult<T, F extends Record<string, unknown>> {
items: T[];
setItems: React.Dispatch<React.SetStateAction<T[]>>;
loading: boolean;
/** True only during a manual reload after the first successful load. The table stays visible. */
refreshing: boolean;
loadError: string | null;
reload: () => void;

Expand Down Expand Up @@ -181,7 +183,9 @@ export function useCrudTab<T, F extends Record<string, unknown>>(
// ── List ─────────────────────────────────────────────────────────────────
const [items, setItems] = useState<T[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
const hasLoadedRef = useRef(false);

// ── Search + pagination ──────────────────────────────────────────────────
const [search, setSearch] = useState('');
Expand Down Expand Up @@ -232,16 +236,27 @@ export function useCrudTab<T, F extends Record<string, unknown>>(

// ── 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([]);
hasLoadedRef.current = false;
})
.finally(() => setLoading(false));
.finally(() => {
Comment thread
jkilzi marked this conversation as resolved.
setLoading(false);
setRefreshing(false);
});
}, []); // stable — reads via ref

useEffect(() => {
Expand Down Expand Up @@ -386,6 +401,7 @@ export function useCrudTab<T, F extends Record<string, unknown>>(
items,
setItems,
loading,
refreshing,
loadError,
reload,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ export function ProvidersTabContent() {
onPrimaryAction={crud.handleOpenCreate}
illustrationSrc={emptyIllustration}
entityLabel="Providers"
onRefresh={crud.reload}
refreshing={crud.refreshing}
/>

{formDialog({
Expand Down
Loading