mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Make Patrol finding outcomes discoverable
This commit is contained in:
@@ -1,28 +1,19 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "87fda2a235a34bf62071458795237856ac16650d",
|
||||
"verified_at": "2026-08-30T08:55:33Z",
|
||||
"base_sha": "1e805afc4b54c25ed253497e4818037476cbce23",
|
||||
"verified_at": "2026-08-30T10:22:47Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/api/resources.ts",
|
||||
"frontend-modern/src/components/Settings/DataHandlingPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/ResourcePicker.tsx",
|
||||
"frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts",
|
||||
"frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts",
|
||||
"frontend-modern/src/hooks/useResourceStats.ts",
|
||||
"frontend-modern/src/hooks/useResources.ts"
|
||||
"frontend-modern/src/components/AI/FindingsPanel.tsx",
|
||||
"frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx",
|
||||
"frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/api/resources.ts": "08a29f4efc054188e378f5eecb94c2cad4884c8a20fc31f97c3242a72099cdde",
|
||||
"frontend-modern/src/components/Settings/DataHandlingPanel.tsx": "ce04993e9a017890a7098cf15ee518701d3ecbbbd6eac91520c0c7674a86c98c",
|
||||
"frontend-modern/src/components/Settings/ResourcePicker.tsx": "2e33b23cfd257222996a2adb9102b80f36ed26d0039c449c3111162ed7da13b7",
|
||||
"frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts": "f9fca9e1b66f7d2f020a52a0e873da7371f649c4f37c8a7df08e627b2a1e77f2",
|
||||
"frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts": "744ffef0c69a2113d12192698258e4296fd8a03fdf9a227bc9c0ab0ed250b5d7",
|
||||
"frontend-modern/src/hooks/useResourceStats.ts": "b3d4e3777096dd1933a2e2c53af822052ddddb4563a9ccd1bc4c16677d191ec6",
|
||||
"frontend-modern/src/hooks/useResources.ts": "142ee968fdfdeb1281feb6b04beee23c1556db42a118e47e8063b6f151d1c24a"
|
||||
"frontend-modern/src/components/AI/FindingsPanel.tsx": "bddd32ceb519ad2d4deeb464c47e96789b12e512ab127617c6d7f01d3e32bbd8",
|
||||
"frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx": "34540499fb66c378bba88acae22eaf48f0444e6bd0dd5c4c00a05ae93271d48a",
|
||||
"frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx": "36711b7bc41597a240109c37e50174011c3b3fb5b77aa65179e1bc6cf7165524"
|
||||
},
|
||||
"routes": [
|
||||
"/settings/security-data-handling",
|
||||
"/patrol"
|
||||
],
|
||||
"viewports": [
|
||||
@@ -36,16 +27,15 @@
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Resource Privacy cold direct load settled with 1,855 governed resources and rendered the aggregate posture without a false empty state",
|
||||
"Resource Privacy desktop and narrow layouts retained the complete policy summary, posture cards, scrolling, and viewport fit without horizontal overflow",
|
||||
"Patrol Protection objective dialog opened the resource scope picker with 1,503 populated resources instead of an empty placeholder",
|
||||
"resource picker desktop and narrow layouts retained search, type filters, bounded list scrolling, selection state, dialog controls, and viewport fit"
|
||||
"Patrol Inbox selected-decision lifecycle showed the lasting finding outcome explanation and visible Open finding options handoff",
|
||||
"Patrol Activity explained the finding outcome workflow and rendered a visible Finding options control on each active finding",
|
||||
"expanded active finding showed Resolve or dismiss with Dismiss: Not an issue, Remember as expected, Dismiss: Later, and Create rule from this",
|
||||
"desktop and narrow layouts retained readable controls, scrolling, and exact viewport fit without horizontal overflow"
|
||||
],
|
||||
"interactions": [
|
||||
"loaded Resource Privacy as a cold direct route against the exact isolated frontend and backend build",
|
||||
"traced the cold Resource Privacy request set and confirmed only the compact /api/resources/stats read plus one type=agent settings-status page, with no unfiltered paginated resource hydration",
|
||||
"opened Patrol Protection, opened Add objective, expanded Limit to specific resources, searched for esxi-07.lab.local, and selected the matching resource",
|
||||
"dismissed the objective dialog and confirmed focus returned to Add objective",
|
||||
"inspected Resource Privacy and the expanded resource picker at 1440 by 900 and 390 by 844 for populated state, responsive fit, clipping, scrolling, and control availability"
|
||||
"opened the first mock-backed Patrol Inbox decision and followed Open finding options into Activity",
|
||||
"opened an active finding's outcome controls by pointer at desktop width and by keyboard Enter at narrow width",
|
||||
"closed Resolve or dismiss and confirmed the Finding options trigger returned to aria-expanded false",
|
||||
"inspected the Inbox handoff, Activity rows, expanded finding details, management disclosure, control placement, clipping, and scrolling at 1440 by 900 and 390 by 844"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -189,6 +189,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
const [filter, setFilter] = createSignal<FindingsPanelFilter>(props.filterOverride ?? 'active');
|
||||
const [sortBy, setSortBy] = createSignal<'severity' | 'time'>('severity');
|
||||
const [expandedId, setExpandedId] = createSignal<string | null>(null);
|
||||
const [manageOpenId, setManageOpenId] = createSignal<string | null>(null);
|
||||
const [actionLoading, setActionLoading] = createSignal<string | null>(null);
|
||||
const [lastHashScrolled, setLastHashScrolled] = createSignal<string | null>(null);
|
||||
const [editingNoteId, setEditingNoteId] = createSignal<string | null>(null);
|
||||
@@ -501,6 +502,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
createEffect(() => {
|
||||
if (isPatrolFindingsSource() && expandedId() && !selectedPatrolFinding()) {
|
||||
setExpandedId(null);
|
||||
setManageOpenId(null);
|
||||
}
|
||||
});
|
||||
const filterOptions = createMemo(() => buildFindingFilterOptions(filterCounts()));
|
||||
@@ -979,12 +981,21 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
const toggleExpanded = () => {
|
||||
if (expandedId() === finding.id) {
|
||||
setExpandedId(null);
|
||||
setManageOpenId(null);
|
||||
} else {
|
||||
setExpandedId(finding.id);
|
||||
setManageOpenId(null);
|
||||
}
|
||||
props.onFindingClick?.(finding);
|
||||
};
|
||||
|
||||
const openFindingOptions = (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
setExpandedId(finding.id);
|
||||
setManageOpenId(finding.id);
|
||||
props.onFindingClick?.(finding);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`finding-${finding.id}`}
|
||||
@@ -1240,7 +1251,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
</Show>
|
||||
</div>
|
||||
{/* Actions */}
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<div class="flex shrink-0 flex-col items-end gap-1 sm:flex-row sm:items-center">
|
||||
<Show
|
||||
when={
|
||||
!isPatrolFindingsSource() && expandedId() !== finding.id
|
||||
@@ -1326,6 +1337,26 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
</button>
|
||||
</Show>
|
||||
</Show>
|
||||
<Show
|
||||
when={
|
||||
isPatrolFindingsSource() &&
|
||||
props.showControls !== false &&
|
||||
finding.status === 'active' &&
|
||||
manualControls.dismiss
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Open resolve and dismiss options for ${title.label}`}
|
||||
aria-expanded={manageOpenId() === finding.id}
|
||||
aria-controls={`finding-${finding.id}-manage`}
|
||||
onClick={openFindingOptions}
|
||||
class="inline-flex min-h-11 items-center rounded border border-border bg-surface px-2 py-1 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 sm:min-h-0"
|
||||
title="Resolve, dismiss, remember as expected, or create a rule"
|
||||
>
|
||||
Finding options
|
||||
</button>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={
|
||||
@@ -1693,9 +1724,14 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
</Show>
|
||||
|
||||
<Show when={shouldShowExpandedManageMenu}>
|
||||
<details onClick={(e) => e.stopPropagation()}>
|
||||
<summary class="list-none cursor-pointer rounded border border-border bg-surface px-3 py-1.5 font-medium text-base-content hover:bg-surface-hover">
|
||||
Manage
|
||||
<details
|
||||
id={`finding-${finding.id}-manage`}
|
||||
open={manageOpenId() === finding.id}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onToggle={(event) => setManageOpenId(event.currentTarget.open ? finding.id : null)}
|
||||
>
|
||||
<summary class="list-none cursor-pointer rounded border border-border bg-surface px-3 py-1.5 font-medium text-base-content hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40">
|
||||
Resolve or dismiss
|
||||
</summary>
|
||||
<div class="mt-1 flex min-w-48 flex-col gap-1 rounded border border-border bg-surface p-1 shadow-sm">
|
||||
<Show when={shouldShowAssistantManageAction}>
|
||||
@@ -2199,7 +2235,10 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Close review panel for ${title().label}`}
|
||||
onClick={() => setExpandedId(null)}
|
||||
onClick={() => {
|
||||
setExpandedId(null);
|
||||
setManageOpenId(null);
|
||||
}}
|
||||
class="rounded p-1.5 text-muted transition-colors hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
|
||||
>
|
||||
<XIcon class="h-4 w-4" />
|
||||
|
||||
@@ -329,7 +329,7 @@ describe('FindingsPanel resource links', () => {
|
||||
expect(
|
||||
screen.getByRole('complementary', { name: 'Review Provider connection issue' }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Manage')).toBeInTheDocument();
|
||||
expect(screen.getByText('Resolve or dismiss')).toBeInTheDocument();
|
||||
const assistantHandoff = screen.getByRole('button', { name: 'Open in Assistant' });
|
||||
expect(assistantHandoff.closest('details')).not.toBeNull();
|
||||
expect(screen.getByRole('button', { name: 'Acknowledge' })).toBeInTheDocument();
|
||||
@@ -565,6 +565,31 @@ describe('FindingsPanel resource links', () => {
|
||||
expectCollapsedFinding('Provider connection issue');
|
||||
});
|
||||
|
||||
it('opens durable finding outcomes directly from an active Patrol row', async () => {
|
||||
render(() => <FindingsPanel findingsSource="patrol" />);
|
||||
|
||||
await waitFor(() => expect(mockState.loadPatrolFindings).toHaveBeenCalled());
|
||||
|
||||
const options = screen.getByRole('button', {
|
||||
name: 'Open resolve and dismiss options for Provider connection issue',
|
||||
});
|
||||
expect(options).toHaveAttribute('aria-expanded', 'false');
|
||||
|
||||
fireEvent.click(options);
|
||||
|
||||
expect(options).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(screen.getByText('Resolve or dismiss')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Dismiss: Not an issue' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Remember as expected' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Dismiss: Later' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Create rule from this' })).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: 'Close review panel for Provider connection issue' }),
|
||||
);
|
||||
expect(options).toHaveAttribute('aria-expanded', 'false');
|
||||
});
|
||||
|
||||
it('renders Patrol findings while the unified findings request is still loading', async () => {
|
||||
mockState.findingsLoading = true;
|
||||
mockState.patrolFindingsLoading = false;
|
||||
|
||||
@@ -223,7 +223,7 @@ describe('FindingsPanel assistant handoff', () => {
|
||||
expect(findingsPanelSource).toMatch(/>\s*Copy summary\s*</);
|
||||
});
|
||||
|
||||
it('keeps expanded finding actions behind a canonical primary action and compact Manage menu', () => {
|
||||
it('keeps expanded finding actions behind a canonical primary action and explicit outcome menu', () => {
|
||||
expect(findingsPanelSource).toContain('getFindingPrimaryActionPresentation(finding)');
|
||||
expect(findingsPanelSource).toContain('getPrimaryAssistantFindingAction');
|
||||
expect(findingsPanelSource).toContain('const shouldShowAssistantPrimaryAction =');
|
||||
@@ -235,7 +235,7 @@ describe('FindingsPanel assistant handoff', () => {
|
||||
expect(findingsPanelSource).not.toContain(
|
||||
'fallback={\n <button\n type="button"\n onClick={(e) => {\n e.stopPropagation();\n void openFindingInAssistant(finding);',
|
||||
);
|
||||
expect(findingsPanelSource).toContain('Manage');
|
||||
expect(findingsPanelSource).toContain('Resolve or dismiss');
|
||||
expect(findingsPanelSource).toContain('min-w-48');
|
||||
expect(findingsPanelSource).not.toContain('min-w-40');
|
||||
});
|
||||
@@ -243,7 +243,7 @@ describe('FindingsPanel assistant handoff', () => {
|
||||
it('keeps primary-action Patrol runtime findings out of secondary action clutter', () => {
|
||||
// Runtime/setup findings already have one canonical recovery path:
|
||||
// provider settings. The expanded card must not add the Assistant
|
||||
// secondary button or generic Manage menu beside that primary action.
|
||||
// secondary button or generic outcome menu beside that primary action.
|
||||
expect(findingsPanelSource).toContain('const shouldShowExpandedManageMenu =');
|
||||
expect(findingsPanelSource).toContain('!primaryAction ||');
|
||||
expect(findingsPanelSource).toContain('<Show when={shouldShowExpandedManageMenu}>');
|
||||
|
||||
@@ -1343,6 +1343,22 @@ function AttentionLifecycleControls(props: {
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={props.onOpenFindings}>
|
||||
<div class="mt-3 flex flex-col gap-3 rounded-md border border-border-subtle bg-surface px-3 py-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p class="text-xs leading-5 text-muted">
|
||||
Need a lasting Patrol finding outcome? Resolve it, dismiss it as not an issue,
|
||||
remember expected behavior, or create a suppression rule.
|
||||
</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
class="min-h-11 shrink-0 sm:min-h-0"
|
||||
onClick={() => props.onOpenFindings?.()}
|
||||
>
|
||||
Open finding options
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={showSuppression()}>
|
||||
<form
|
||||
class="mt-3 space-y-3 border-t border-border-subtle pt-3"
|
||||
@@ -1421,18 +1437,7 @@ function AttentionLifecycleControls(props: {
|
||||
>
|
||||
adjust alert thresholds
|
||||
</A>{' '}
|
||||
to change or turn off this alert for the affected resource
|
||||
<Show when={props.onOpenFindings} fallback=".">
|
||||
, or{' '}
|
||||
<button
|
||||
type="button"
|
||||
class="font-medium text-blue-700 hover:underline dark:text-blue-300"
|
||||
onClick={() => props.onOpenFindings?.()}
|
||||
>
|
||||
review Patrol findings
|
||||
</button>{' '}
|
||||
to mark a finding as expected so Patrol stops raising it.
|
||||
</Show>
|
||||
to change or turn off this alert for the affected resource.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -126,7 +126,7 @@ export function PatrolIntelligenceSurface() {
|
||||
Review and history
|
||||
</h2>
|
||||
<p class="mt-1 max-w-3xl text-sm leading-5 text-muted">
|
||||
Audit governed operations or inspect Patrol's underlying records.
|
||||
Audit governed operations, manage finding outcomes, or inspect Patrol history.
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid divide-y divide-border sm:grid-cols-2 sm:divide-x sm:divide-y-0">
|
||||
@@ -177,9 +177,12 @@ export function PatrolIntelligenceSurface() {
|
||||
<HistoryIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
<span class="min-w-0">
|
||||
<span class="text-sm font-semibold text-base-content">Patrol records</span>
|
||||
<span class="text-sm font-semibold text-base-content">
|
||||
Finding options and history
|
||||
</span>
|
||||
<span class="mt-1 block text-xs font-normal leading-5 text-muted">
|
||||
Inspect findings and check history when you need the forensic trail.
|
||||
Resolve or dismiss findings, remember expected behavior, create rules, and
|
||||
inspect check history.
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
@@ -199,13 +202,14 @@ export function PatrolIntelligenceSurface() {
|
||||
open={findingsOpen()}
|
||||
onToggle={(event) => setFindingsOpen(event.currentTarget.open)}
|
||||
>
|
||||
<summary class="sr-only">Patrol records</summary>
|
||||
<summary class="sr-only">Finding options and history</summary>
|
||||
<div
|
||||
class={`space-y-4 border-t border-border p-4 sm:p-5 ${!state.patrolEnabledLocal() ? 'opacity-50 pointer-events-none' : ''}`}
|
||||
>
|
||||
<p class="text-xs leading-5 text-muted">
|
||||
Patrol records keep raw check findings and run history. The Inbox separately lists
|
||||
items that currently need a decision.
|
||||
Choose Finding options on an active finding to resolve it, dismiss it, remember it
|
||||
as expected, or create a suppression rule. Check history remains available here for
|
||||
the forensic trail.
|
||||
</p>
|
||||
<PatrolIntelligenceWorkspace state={state} />
|
||||
</div>
|
||||
|
||||
@@ -686,7 +686,7 @@ describe('PatrolAttentionWorkbench', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('points lifecycle users at the durable threshold and finding controls', async () => {
|
||||
it('keeps durable Patrol finding outcomes visible beside alert lifecycle controls', async () => {
|
||||
const active = item();
|
||||
apiMocks.getList.mockResolvedValue(
|
||||
listResponse([active], summary({ activeCount: 1, openCount: 1, calm: false })),
|
||||
@@ -708,16 +708,23 @@ describe('PatrolAttentionWorkbench', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByText(/Mark reviewed removes this occurrence from the decision inbox/i),
|
||||
).toHaveTextContent(/until it resolves or you return it to open/i);
|
||||
expect(screen.queryByText(/today's decision inbox/i)).not.toBeInTheDocument();
|
||||
expect(await screen.findByText(/Need a lasting Patrol finding outcome/i)).toHaveTextContent(
|
||||
/remember expected behavior/i,
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open finding options' }));
|
||||
expect(onOpenFindings).toHaveBeenCalledTimes(1);
|
||||
|
||||
fireEvent.click(screen.getByText('More ways to manage this issue'));
|
||||
expect(screen.getByText(/Mark reviewed removes this occurrence/i)).toHaveTextContent(
|
||||
/until it resolves or you return it to open/i,
|
||||
);
|
||||
expect(screen.getByRole('link', { name: 'adjust alert thresholds' })).toHaveAttribute(
|
||||
'href',
|
||||
'/alerts/thresholds',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'review Patrol findings' }));
|
||||
expect(onOpenFindings).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'review Patrol findings' }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('omits the findings pointer when no findings surface is wired', async () => {
|
||||
|
||||
Reference in New Issue
Block a user