mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-10 03:51:54 +00:00
Scope Patrol findings badge to run snapshots
This commit is contained in:
parent
7354453f73
commit
624d25ba4f
5 changed files with 72 additions and 20 deletions
|
|
@ -255,6 +255,10 @@ That same snapshot scoping must apply to the findings control bar too. When the
|
|||
user is looking at an explicit run snapshot, filter pills and their attention
|
||||
or approval counts must derive from that snapshot-scoped finding set rather
|
||||
than borrowing global Patrol finding counts from outside the selected run.
|
||||
That same snapshot scoping must also apply to the `Findings` tab badge itself.
|
||||
When the selected run carries an explicit empty `finding_ids` snapshot, or when
|
||||
the run lacks snapshot ids entirely, the tab must fail closed instead of
|
||||
borrowing global active-finding counts and tones from outside the selected run.
|
||||
That same findings surface should keep its section chrome functional rather
|
||||
than promotional. Inside the Patrol findings tab, the selected tab already
|
||||
names the surface, so the findings card should not add another in-card product
|
||||
|
|
|
|||
|
|
@ -481,12 +481,13 @@ describe('aiFindingPresentation', () => {
|
|||
it('uses explicit textual separators for patrol tab badges instead of css-only spacing', () => {
|
||||
expect(patrolWorkspaceSource).toContain("aria-hidden=\"true\"");
|
||||
expect(patrolWorkspaceSource).toContain("{' '}");
|
||||
expect(patrolWorkspaceSource).toContain('{state.summaryStats().totalActive}');
|
||||
expect(patrolWorkspaceSource).toContain('{state.findingsTabBadgeCount()}');
|
||||
expect(patrolWorkspaceSource).toContain('{state.displayRunHistory().length}');
|
||||
});
|
||||
|
||||
it('routes the findings tab badge tone through the shared patrol findings badge helper', () => {
|
||||
expect(patrolWorkspaceSource).toContain('getPatrolFindingsBadgePresentation');
|
||||
expect(patrolWorkspaceSource).toContain('state.findingsTabBadgeFindings()');
|
||||
expect(patrolWorkspaceSource).toContain('findingsBadgePresentation().toneClasses');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import type { PatrolIntelligenceState } from './usePatrolIntelligenceState';
|
|||
|
||||
export function PatrolIntelligenceWorkspace(props: { state: PatrolIntelligenceState }) {
|
||||
const state = props.state;
|
||||
const findingsBadgePresentation = () => getPatrolFindingsBadgePresentation(state.activePatrolFindings());
|
||||
const findingsBadgePresentation = () =>
|
||||
getPatrolFindingsBadgePresentation(state.findingsTabBadgeFindings());
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -49,13 +50,13 @@ export function PatrolIntelligenceWorkspace(props: { state: PatrolIntelligenceSt
|
|||
}`}
|
||||
>
|
||||
Findings
|
||||
<Show when={state.summaryStats().totalActive > 0}>
|
||||
<Show when={(state.findingsTabBadgeCount() ?? 0) > 0}>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class={`ml-1.5 px-1.5 py-0.5 text-xs rounded-full ${findingsBadgePresentation().toneClasses}`}
|
||||
>
|
||||
{' '}
|
||||
{state.summaryStats().totalActive}
|
||||
{state.findingsTabBadgeCount()}
|
||||
</span>
|
||||
</Show>
|
||||
</button>
|
||||
|
|
@ -108,11 +109,11 @@ export function PatrolIntelligenceWorkspace(props: { state: PatrolIntelligenceSt
|
|||
filterFindingIds={state.selectedRunFindingIds()}
|
||||
scopeResourceIds={state.selectedRunScopeResourceIds()}
|
||||
scopeResourceTypes={state.selectedRun()?.scope_resource_types}
|
||||
runSnapshot={state.selectedRun() ?? undefined}
|
||||
showScopeWarnings={Boolean(state.selectedRun())}
|
||||
runtimeState={state.runtimeState()}
|
||||
blockedReason={state.blockedReason()}
|
||||
overallHealth={state.intelligenceSummary()?.overall_health}
|
||||
runSnapshot={state.selectedRun() ?? undefined}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
|
|
|
|||
|
|
@ -437,6 +437,19 @@ export function usePatrolIntelligenceState() {
|
|||
});
|
||||
|
||||
const selectedRunScopeResourceIds = createMemo(() => getCanonicalScopeResourceIds(selectedRun()));
|
||||
const allPatrolFindings = createMemo(() =>
|
||||
aiIntelligenceStore.findings.filter(
|
||||
(finding) =>
|
||||
finding.source !== 'threshold' && !finding.isThreshold && !hasTriggeringAlert(finding),
|
||||
),
|
||||
);
|
||||
const selectedRunPatrolFindings = createMemo(() => {
|
||||
const run = selectedRun();
|
||||
if (!run) return null;
|
||||
if (run.finding_ids === undefined) return undefined;
|
||||
const snapshotFindingIds = new Set(run.finding_ids);
|
||||
return allPatrolFindings().filter((finding) => snapshotFindingIds.has(finding.id));
|
||||
});
|
||||
|
||||
const intelligenceSummary = createMemo(() => aiIntelligenceStore.intelligenceSummary);
|
||||
const policyPosture = createMemo(() => intelligenceSummary()?.policy_posture);
|
||||
|
|
@ -584,11 +597,7 @@ export function usePatrolIntelligenceState() {
|
|||
}
|
||||
|
||||
const summaryStats = () => {
|
||||
const allFindings = aiIntelligenceStore.findings;
|
||||
const patrolFindings = allFindings.filter(
|
||||
(finding) =>
|
||||
finding.source !== 'threshold' && !finding.isThreshold && !hasTriggeringAlert(finding),
|
||||
);
|
||||
const patrolFindings = allPatrolFindings();
|
||||
const activeFindings = patrolFindings.filter((finding) => finding.status === 'active');
|
||||
|
||||
return {
|
||||
|
|
@ -606,13 +615,24 @@ export function usePatrolIntelligenceState() {
|
|||
};
|
||||
|
||||
const activePatrolFindings = () =>
|
||||
aiIntelligenceStore.findings.filter(
|
||||
(finding) =>
|
||||
finding.status === 'active' &&
|
||||
finding.source !== 'threshold' &&
|
||||
!finding.isThreshold &&
|
||||
!hasTriggeringAlert(finding),
|
||||
);
|
||||
allPatrolFindings().filter((finding) => finding.status === 'active');
|
||||
const findingsTabBadgeFindings = createMemo(() => {
|
||||
const snapshotFindings = selectedRunPatrolFindings();
|
||||
if (snapshotFindings === null) {
|
||||
return activePatrolFindings();
|
||||
}
|
||||
return snapshotFindings ?? [];
|
||||
});
|
||||
const findingsTabBadgeCount = createMemo(() => {
|
||||
const snapshotFindings = selectedRunPatrolFindings();
|
||||
if (snapshotFindings === null) {
|
||||
return activePatrolFindings().length;
|
||||
}
|
||||
if (snapshotFindings === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return snapshotFindings.length;
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
await Promise.all([
|
||||
|
|
@ -670,6 +690,8 @@ export function usePatrolIntelligenceState() {
|
|||
clearScrollToFindingTimer,
|
||||
defaultModel,
|
||||
displayRunHistory,
|
||||
findingsTabBadgeCount,
|
||||
findingsTabBadgeFindings,
|
||||
findingsFilterOverride,
|
||||
fullModeUnlocked,
|
||||
handleAlertTriggeredAnalysisChange,
|
||||
|
|
|
|||
|
|
@ -983,6 +983,16 @@ describe('AIIntelligence entitlement gating', () => {
|
|||
hasFeatureMock.mockReturnValue(true);
|
||||
licenseStatusMock.mockReturnValue({ subscription_state: 'active' });
|
||||
getPatrolStatusMock.mockResolvedValue(defaultPatrolStatus({ license_required: false }));
|
||||
intelligenceState.findings = [
|
||||
{
|
||||
id: 'finding-runtime',
|
||||
status: 'active',
|
||||
severity: 'warning',
|
||||
resourceId: 'ai-service',
|
||||
resourceName: 'Pulse Patrol Service',
|
||||
title: 'Pulse Patrol: Insufficient API credits',
|
||||
},
|
||||
];
|
||||
runHistoryState.selection = {
|
||||
id: 'run-empty',
|
||||
started_at: '2026-03-12T10:00:00Z',
|
||||
|
|
@ -1019,7 +1029,6 @@ describe('AIIntelligence entitlement gating', () => {
|
|||
await waitFor(() => {
|
||||
expect(getPatrolStatusMock).toHaveBeenCalled();
|
||||
expect(findingsPanelState.latestProps).not.toBeNull();
|
||||
runSnapshotId: 'run-empty',
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Runs' }));
|
||||
|
|
@ -1030,12 +1039,15 @@ describe('AIIntelligence entitlement gating', () => {
|
|||
expect(screen.getByText(/Filtered to run/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Findings' }).textContent).toBe('Findings');
|
||||
|
||||
expect(findingsPanelState.latestProps).toMatchObject({
|
||||
filterOverride: 'all',
|
||||
filterFindingIds: [],
|
||||
scopeResourceIds: ['expanded-a', 'expanded-b'],
|
||||
scopeResourceTypes: ['vm'],
|
||||
showScopeWarnings: true,
|
||||
runSnapshotId: 'run-empty',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1079,7 +1091,6 @@ describe('AIIntelligence entitlement gating', () => {
|
|||
await waitFor(() => {
|
||||
expect(getPatrolStatusMock).toHaveBeenCalled();
|
||||
expect(findingsPanelState.latestProps).not.toBeNull();
|
||||
runSnapshotId: 'run-empty-effective-scope',
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Runs' }));
|
||||
|
|
@ -1096,6 +1107,7 @@ describe('AIIntelligence entitlement gating', () => {
|
|||
scopeResourceIds: [],
|
||||
scopeResourceTypes: ['vm'],
|
||||
showScopeWarnings: true,
|
||||
runSnapshotId: 'run-empty-effective-scope',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1103,6 +1115,16 @@ describe('AIIntelligence entitlement gating', () => {
|
|||
hasFeatureMock.mockReturnValue(true);
|
||||
licenseStatusMock.mockReturnValue({ subscription_state: 'active' });
|
||||
getPatrolStatusMock.mockResolvedValue(defaultPatrolStatus({ license_required: false }));
|
||||
intelligenceState.findings = [
|
||||
{
|
||||
id: 'finding-runtime',
|
||||
status: 'active',
|
||||
severity: 'warning',
|
||||
resourceId: 'ai-service',
|
||||
resourceName: 'Pulse Patrol Service',
|
||||
title: 'Pulse Patrol: Insufficient API credits',
|
||||
},
|
||||
];
|
||||
runHistoryState.selection = {
|
||||
id: 'run-missing-snapshot',
|
||||
started_at: '2026-03-12T10:10:00Z',
|
||||
|
|
@ -1138,7 +1160,6 @@ describe('AIIntelligence entitlement gating', () => {
|
|||
await waitFor(() => {
|
||||
expect(getPatrolStatusMock).toHaveBeenCalled();
|
||||
expect(findingsPanelState.latestProps).not.toBeNull();
|
||||
runSnapshotId: 'run-missing-snapshot',
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Runs' }));
|
||||
|
|
@ -1149,12 +1170,15 @@ describe('AIIntelligence entitlement gating', () => {
|
|||
expect(screen.getByText(/Filtered to run/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Findings' }).textContent).toBe('Findings');
|
||||
|
||||
expect(findingsPanelState.latestProps).toMatchObject({
|
||||
filterOverride: 'all',
|
||||
filterFindingIds: undefined,
|
||||
scopeResourceIds: ['expanded-a'],
|
||||
scopeResourceTypes: ['vm'],
|
||||
showScopeWarnings: true,
|
||||
runSnapshotId: 'run-missing-snapshot',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue