-
-
- Scan History
-
+
{ if (!next) onClose(); }}>
+
+
+
+ Security ยท Node {activeNode?.name ?? '-'}
+
+
-
+
Completed vulnerability scans on this node, grouped by image. Select two to compare.
-
-
-
+
+
+
@@ -233,7 +253,7 @@ export function SecurityHistoryView() {
) : (
-
+
{groups.map((group) => (
@@ -311,22 +331,22 @@ export function SecurityHistoryView() {
)}
-
-
+
- setCompareIds(null)}
- />
+ setCompareIds(null)}
+ />
- setInspectScanId(null)}
- canGenerateSbom={isPaid}
- canCompare={false}
- canManageSuppressions={isPaid && isAdmin}
- />
-
+ setInspectScanId(null)}
+ canGenerateSbom={isPaid}
+ canCompare={false}
+ canManageSuppressions={isPaid && isAdmin}
+ />
+
+
);
}
diff --git a/frontend/src/components/__tests__/SecurityHistoryView.test.tsx b/frontend/src/components/__tests__/SecurityHistoryView.test.tsx
index f5d288ef..6bf2bdc5 100644
--- a/frontend/src/components/__tests__/SecurityHistoryView.test.tsx
+++ b/frontend/src/components/__tests__/SecurityHistoryView.test.tsx
@@ -106,7 +106,7 @@ afterEach(() => vi.clearAllMocks());
describe('SecurityHistoryView', () => {
it('fetches completed scans on mount with server-driven pagination params', async () => {
mockedFetch.mockResolvedValue(listResponse([scan()]));
- render(
);
+ render(
);
await waitFor(() => expect(mockedFetch).toHaveBeenCalled());
const url = mockedFetch.mock.calls[0][0] as string;
expect(url).toMatch(/^\/security\/scans\?/);
@@ -118,7 +118,7 @@ describe('SecurityHistoryView', () => {
it('advances offset when the user pages forward', async () => {
mockedFetch.mockResolvedValue(listResponse([scan()], 250));
const user = userEvent.setup();
- render(
);
+ render(
);
await waitFor(() => expect(mockedFetch).toHaveBeenCalledTimes(1));
@@ -135,11 +135,11 @@ describe('SecurityHistoryView', () => {
it('re-fetches when activeNode.id changes', async () => {
mockedFetch.mockResolvedValue(listResponse([scan()]));
- const { rerender } = render(
);
+ const { rerender } = render(
);
await waitFor(() => expect(mockedFetch).toHaveBeenCalledTimes(1));
nodesState.activeNode = { id: 2 };
- rerender(
);
+ rerender(
);
await waitFor(() => expect(mockedFetch).toHaveBeenCalledTimes(2));
});
@@ -152,7 +152,7 @@ describe('SecurityHistoryView', () => {
]),
);
const user = userEvent.setup();
- render(
);
+ render(
);
const checkboxes = await screen.findAllByRole('checkbox');
expect(checkboxes).toHaveLength(3);
@@ -175,7 +175,7 @@ describe('SecurityHistoryView', () => {
]),
);
const user = userEvent.setup();
- render(
);
+ render(
);
const checkboxes = await screen.findAllByRole('checkbox');
await user.click(checkboxes[0]);
@@ -188,6 +188,29 @@ describe('SecurityHistoryView', () => {
expect(last?.currentScanId).toBe(10);
});
+ it('does not fetch when closed', async () => {
+ mockedFetch.mockResolvedValue(listResponse([scan()]));
+ render(
);
+
+ // Flush any microtasks; the fetch guard returns synchronously so no
+ // timer delay is required.
+ await Promise.resolve();
+ expect(mockedFetch).not.toHaveBeenCalled();
+ });
+
+ it('fires onClose when Escape is pressed and does not fetch again', async () => {
+ mockedFetch.mockResolvedValue(listResponse([scan()]));
+ const onClose = vi.fn();
+ const user = userEvent.setup();
+ render(
);
+
+ await waitFor(() => expect(mockedFetch).toHaveBeenCalledTimes(1));
+ await user.keyboard('{Escape}');
+
+ await waitFor(() => expect(onClose).toHaveBeenCalled());
+ expect(mockedFetch).toHaveBeenCalledTimes(1);
+ });
+
it('disables Compare button for community tier', async () => {
licenseState.isPaid = false;
mockedFetch.mockResolvedValue(
@@ -197,7 +220,7 @@ describe('SecurityHistoryView', () => {
]),
);
const user = userEvent.setup();
- render(
);
+ render(
);
const checkboxes = await screen.findAllByRole('checkbox');
await user.click(checkboxes[0]);