fix(security): convert scan history from full page to sheet overlay (#720)

Scan history is now a right-side sheet that layers over the current view
(typically Resources Hub) instead of a full-page activeView branch. The
sheet opens via the existing navigation event, fetches only when open,
dismisses on Escape or overlay click, and preserves the nested scan-details
and scan-compare sheets intact via Radix portal stacking.

The fetch effect now resets selection and page state on active-node change
exactly once, avoiding a double-fetch on node switches.
This commit is contained in:
Anso
2026-04-21 08:01:31 -04:00
committed by GitHub
parent 661b9c638b
commit e4fdb1cd6c
5 changed files with 104 additions and 48 deletions
@@ -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(<SecurityHistoryView />);
render(<SecurityHistoryView open onClose={vi.fn()} />);
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(<SecurityHistoryView />);
render(<SecurityHistoryView open onClose={vi.fn()} />);
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(<SecurityHistoryView />);
const { rerender } = render(<SecurityHistoryView open onClose={vi.fn()} />);
await waitFor(() => expect(mockedFetch).toHaveBeenCalledTimes(1));
nodesState.activeNode = { id: 2 };
rerender(<SecurityHistoryView key="remount-signal" />);
rerender(<SecurityHistoryView key="remount-signal" open onClose={vi.fn()} />);
await waitFor(() => expect(mockedFetch).toHaveBeenCalledTimes(2));
});
@@ -152,7 +152,7 @@ describe('SecurityHistoryView', () => {
]),
);
const user = userEvent.setup();
render(<SecurityHistoryView />);
render(<SecurityHistoryView open onClose={vi.fn()} />);
const checkboxes = await screen.findAllByRole('checkbox');
expect(checkboxes).toHaveLength(3);
@@ -175,7 +175,7 @@ describe('SecurityHistoryView', () => {
]),
);
const user = userEvent.setup();
render(<SecurityHistoryView />);
render(<SecurityHistoryView open onClose={vi.fn()} />);
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(<SecurityHistoryView open={false} onClose={vi.fn()} />);
// 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(<SecurityHistoryView open onClose={onClose} />);
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(<SecurityHistoryView />);
render(<SecurityHistoryView open onClose={vi.fn()} />);
const checkboxes = await screen.findAllByRole('checkbox');
await user.click(checkboxes[0]);