diff --git a/ui/packages/plugins/atw/src/IncidentWorkspacePage.tsx b/ui/packages/plugins/atw/src/IncidentWorkspacePage.tsx index 19f691a4c7..7599258e20 100644 --- a/ui/packages/plugins/atw/src/IncidentWorkspacePage.tsx +++ b/ui/packages/plugins/atw/src/IncidentWorkspacePage.tsx @@ -30,6 +30,7 @@ import LockOpenOutlinedIcon from '@mui/icons-material/LockOpenOutlined'; import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; import { useNavigate, useParams } from 'react-router-dom'; import { useAuth } from '@sep/api'; +import { ReadOnlyNotice } from '@sep/framework'; import { CollectPane } from './CollectPane'; import { ResultsPane } from './ResultsPane'; import { useAtwIncident, useAtwIncidentLifecycle } from './hooks'; @@ -38,6 +39,12 @@ import { useAtwIncident, useAtwIncidentLifecycle } from './hooks'; * Incident workspace rendered at ``/atw/:incidentId``. Two side-by-side panes — * Collect (browse, select, and batch-execute snippets) and Results (each * execution's status, logs, and file listing) — stacked on narrow screens. + * + * A read-only session gets Results alone, full width. Collect exists only to + * start an execution, and every unsafe ATW route requires an administrator, so + * leaving the pane mounted with its execute form withheld offered a snippet + * picker that could never run anything. Withheld rather than disabled because a + * role, unlike a closed incident, is not something the viewer can undo. */ export function IncidentWorkspacePage() { const { incidentId } = useParams<{ incidentId: string }>(); @@ -135,6 +142,16 @@ export function IncidentWorkspacePage() { )} + {!canMutate && ( + + + + )} + - - - + {/* PMM divergence from upstream SEP — keep on the next sync. */} + {canMutate && ( + + + + )} diff --git a/ui/packages/plugins/atw/src/ResultsPane.tsx b/ui/packages/plugins/atw/src/ResultsPane.tsx index eb48c7620b..2c8d2df3c5 100644 --- a/ui/packages/plugins/atw/src/ResultsPane.tsx +++ b/ui/packages/plugins/atw/src/ResultsPane.tsx @@ -273,8 +273,9 @@ export function ResultsPane({ incidentId }: ResultsPaneProps) { {!isLoading && !error && (!rows || rows.length === 0) && ( - No executions yet. Run snippets from the Collect pane to see results - here. + {canMutate + ? 'No executions yet. Run snippets from the Collect pane to see results here.' + : 'No executions yet.'} )} diff --git a/ui/packages/plugins/atw/tests/IncidentWorkspacePage.test.tsx b/ui/packages/plugins/atw/tests/IncidentWorkspacePage.test.tsx index 1d365ac519..9eefa87d46 100644 --- a/ui/packages/plugins/atw/tests/IncidentWorkspacePage.test.tsx +++ b/ui/packages/plugins/atw/tests/IncidentWorkspacePage.test.tsx @@ -210,4 +210,37 @@ describe('IncidentWorkspacePage — write access', () => { screen.queryByRole('button', { name: /Reopen incident/i }) ).not.toBeInTheDocument(); }); + + it('shows the Collect pane for a session that may mutate', async () => { + renderWorkspace(); + + await waitFor(() => expect(screen.getByText('Collect')).toBeTruthy()); + expect(screen.getByText('Results')).toBeTruthy(); + expect( + screen.queryByTestId('atw-collect-read-only') + ).not.toBeInTheDocument(); + }); + + it('withholds the Collect pane from a non-admin, leaving Results', async () => { + mockCanMutate = false; + renderWorkspace(); + + await waitFor(() => expect(screen.getByText('Results')).toBeTruthy()); + expect(screen.queryByText('Collect')).not.toBeInTheDocument(); + expect( + screen.queryByRole('combobox', { name: 'Snippets' }) + ).not.toBeInTheDocument(); + expect(screen.getByTestId('atw-collect-read-only')).toBeTruthy(); + expect( + screen.getByText(/permission to collect diagnostics for this incident/i) + ).toBeTruthy(); + }); + + it('fetches no snippet categories for a non-admin', async () => { + mockCanMutate = false; + renderWorkspace(); + + await waitFor(() => expect(screen.getByText('Results')).toBeTruthy()); + expect(mockedApi.get).not.toHaveBeenCalledWith('/apps/atw/'); + }); }); diff --git a/ui/packages/plugins/atw/tests/ResultsPane.test.tsx b/ui/packages/plugins/atw/tests/ResultsPane.test.tsx index 3b1314b4e4..dba53a44c8 100644 --- a/ui/packages/plugins/atw/tests/ResultsPane.test.tsx +++ b/ui/packages/plugins/atw/tests/ResultsPane.test.tsx @@ -100,9 +100,23 @@ describe('ResultsPane', () => { renderPane(); + await waitFor(() => { + expect( + screen.getByText(/Run snippets from the Collect pane/i) + ).toBeTruthy(); + }); + }); + + it('does not point a read-only session at the withheld Collect pane', async () => { + mockCanMutate = false; + mockedApi.get.mockResolvedValue(paginated([])); + + renderPane(); + await waitFor(() => { expect(screen.getByText(/No executions yet/i)).toBeTruthy(); }); + expect(screen.queryByText(/Collect pane/i)).not.toBeInTheDocument(); }); it('renders an Unknown chip when the task status could not be hydrated', async () => {