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
28 changes: 24 additions & 4 deletions ui/packages/plugins/atw/src/IncidentWorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 }>();
Expand Down Expand Up @@ -135,21 +142,34 @@ export function IncidentWorkspacePage() {
</Typography>
)}

{!canMutate && (
<Box sx={{ mt: 1, mb: 2 }}>
<ReadOnlyNotice
variant="inline"
action="collect diagnostics for this incident"
testId="atw-collect-read-only"
/>
</Box>
)}

<Box
sx={{
mt: 1,
display: 'grid',
gap: 2,
gridTemplateColumns: {
xs: 'minmax(0, 1fr)',
md: 'repeat(2, minmax(0, 1fr))',
md: canMutate ? 'repeat(2, minmax(0, 1fr))' : 'minmax(0, 1fr)',
},
alignItems: 'start',
}}
>
<Paper variant="outlined" sx={{ p: 2 }}>
<CollectPane incidentId={incidentId} isClosed={isClosed} />
</Paper>
{/* PMM divergence from upstream SEP — keep on the next sync. */}
{canMutate && (
<Paper variant="outlined" sx={{ p: 2 }}>
<CollectPane incidentId={incidentId} isClosed={isClosed} />
</Paper>
)}
<Paper variant="outlined" sx={{ p: 2 }}>
<ResultsPane incidentId={incidentId} />
</Paper>
Expand Down
5 changes: 3 additions & 2 deletions ui/packages/plugins/atw/src/ResultsPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,9 @@ export function ResultsPane({ incidentId }: ResultsPaneProps) {

{!isLoading && !error && (!rows || rows.length === 0) && (
<Alert severity="info">
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.'}
</Alert>
)}

Expand Down
33 changes: 33 additions & 0 deletions ui/packages/plugins/atw/tests/IncidentWorkspacePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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/');
});
});
14 changes: 14 additions & 0 deletions ui/packages/plugins/atw/tests/ResultsPane.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,23 @@ describe('ResultsPane', () => {

renderPane(<ResultsPane incidentId="inc-1" />);

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(<ResultsPane incidentId="inc-1" />);

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 () => {
Expand Down
Loading