Label Patrol configuration toggles

This commit is contained in:
rcourtman
2026-05-20 14:50:49 +01:00
parent 69263ff68b
commit 7b56ef17d1
4 changed files with 69 additions and 11 deletions
@@ -62,4 +62,23 @@ describe('Toggle', () => {
expect(button).toHaveAttribute('aria-describedby');
expect(button).toHaveAccessibleDescription('Turns it on');
});
it('supports an external visible label when the layout owns label placement', () => {
render(() => (
<>
<span id="external-toggle-label">Alert-triggered patrols</span>
<span id="external-toggle-description">Run when alerts fire.</span>
<Toggle
checked={true}
ariaLabelledBy="external-toggle-label"
ariaDescribedBy="external-toggle-description"
/>
</>
));
const button = screen.getByRole('button', { name: 'Alert-triggered patrols' });
expect(button).toHaveAttribute('aria-labelledby', 'external-toggle-label');
expect(button).toHaveAttribute('aria-describedby', 'external-toggle-description');
expect(button).toHaveAccessibleDescription('Run when alerts fire.');
});
});
@@ -59,8 +59,8 @@ export function Toggle(props: LabeledToggleProps) {
<TogglePrimitive
{...props}
size={size}
ariaLabelledBy={props.label ? labelId : undefined}
ariaDescribedBy={props.description ? descriptionId : undefined}
ariaLabelledBy={props.label ? labelId : props.ariaLabelledBy}
ariaDescribedBy={props.description ? descriptionId : props.ariaDescribedBy}
/>
{(props.label || props.description) && (
<span class={getToggleLabelClass()}>
@@ -1,4 +1,4 @@
import { createMemo, For, Show } from 'solid-js';
import { createMemo, createUniqueId, For, Show } from 'solid-js';
import RefreshCwIcon from 'lucide-solid/icons/refresh-cw';
import PlayIcon from 'lucide-solid/icons/play';
import CircleHelpIcon from 'lucide-solid/icons/circle-help';
@@ -42,6 +42,13 @@ export function getPatrolConfigurationFailureInlineDetails(
export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState }) {
const state = props.state;
const fieldIdPrefix = `patrol-config-${createUniqueId()}`;
const fieldIds = {
alertTriggeredAnalysis: `${fieldIdPrefix}-alert-triggered-analysis`,
patrolAlertTriggers: `${fieldIdPrefix}-alert-triggered-patrols`,
patrolAnomalyTriggers: `${fieldIdPrefix}-anomaly-triggered-patrols`,
autonomousCriticalRemediation: `${fieldIdPrefix}-autonomous-critical-remediation`,
};
const headerMeta = getPatrolPageHeaderMeta();
const scheduleOptions = createMemo(() => buildPatrolScheduleOptions(state.patrolInterval()));
const selectedScheduleLabel = createMemo(
@@ -325,9 +332,12 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
<div class="space-y-4 pt-4 border-t border-border-subtle">
<div class="flex items-start justify-between gap-3">
<div class="flex-1">
<label class="text-sm font-medium text-base-content">
<span
id={fieldIds.alertTriggeredAnalysis}
class="text-sm font-medium text-base-content"
>
Alert-Triggered Analysis
</label>
</span>
<p class="text-[11px] text-muted mt-0.5 leading-tight">
Analyze infrastructure automatically when critical alerts fire.
</p>
@@ -338,6 +348,7 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
state.handleAlertTriggeredAnalysisChange(e.currentTarget.checked)
}
disabled={state.isUpdatingSettings() || state.alertAnalysisLocked()}
ariaLabelledBy={fieldIds.alertTriggeredAnalysis}
/>
</div>
@@ -365,9 +376,12 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
<div class="flex items-start justify-between gap-3">
<div class="flex-1">
<label class="text-sm font-medium text-base-content">
<span
id={fieldIds.patrolAlertTriggers}
class="text-sm font-medium text-base-content"
>
Alert-Triggered Patrols
</label>
</span>
<p class="text-[11px] text-muted mt-0.5 leading-tight">
Run scoped Patrol checks when alerts fire or clear.
</p>
@@ -378,14 +392,18 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
state.handlePatrolAlertTriggersChange(e.currentTarget.checked)
}
disabled={state.isUpdatingSettings() || !state.patrolEnabledLocal()}
ariaLabelledBy={fieldIds.patrolAlertTriggers}
/>
</div>
<div class="flex items-start justify-between gap-3">
<div class="flex-1">
<label class="text-sm font-medium text-base-content">
<span
id={fieldIds.patrolAnomalyTriggers}
class="text-sm font-medium text-base-content"
>
Anomaly-Triggered Patrols
</label>
</span>
<p class="text-[11px] text-muted mt-0.5 leading-tight">
Run scoped Patrol checks when learned baselines detect high-signal
anomalies.
@@ -397,14 +415,18 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
state.handlePatrolAnomalyTriggersChange(e.currentTarget.checked)
}
disabled={state.isUpdatingSettings() || !state.patrolEnabledLocal()}
ariaLabelledBy={fieldIds.patrolAnomalyTriggers}
/>
</div>
<div class="flex items-start justify-between gap-3">
<div class="flex-1">
<label class="text-sm font-medium text-red-600 dark:text-red-400">
<span
id={fieldIds.autonomousCriticalRemediation}
class="text-sm font-medium text-red-600 dark:text-red-400"
>
Autonomous critical remediation
</label>
</span>
<p class="text-[11px] text-muted mt-0.5 leading-tight">
Permit Patrol to execute critical remediation actions without approval.
</p>
@@ -418,6 +440,7 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
state.autonomyLevel() === 'assisted' || state.autonomyLevel() === 'full'
)
}
ariaLabelledBy={fieldIds.autonomousCriticalRemediation}
/>
</div>
</div>
@@ -274,10 +274,14 @@ vi.mock('@/components/shared/Toggle', () => ({
disabled?: boolean;
onToggle?: (value: boolean) => void;
ariaLabel?: string;
ariaLabelledBy?: string;
ariaDescribedBy?: string;
}) => (
<button
type="button"
aria-label={props.ariaLabel}
aria-labelledby={props.ariaLabel ? undefined : props.ariaLabelledBy}
aria-describedby={props.ariaDescribedBy}
aria-pressed={props.checked}
disabled={props.disabled}
onClick={() => props.onToggle?.(!props.checked)}
@@ -287,11 +291,17 @@ vi.mock('@/components/shared/Toggle', () => ({
checked?: boolean;
disabled?: boolean;
onChange?: (event: Event & { currentTarget: HTMLInputElement }) => void;
ariaLabel?: string;
ariaLabelledBy?: string;
ariaDescribedBy?: string;
}) => (
<input
type="checkbox"
checked={props.checked}
disabled={props.disabled}
aria-label={props.ariaLabel}
aria-labelledby={props.ariaLabel ? undefined : props.ariaLabelledBy}
aria-describedby={props.ariaDescribedBy}
onChange={(event) => props.onChange?.(event as Event & { currentTarget: HTMLInputElement })}
/>
),
@@ -789,6 +799,12 @@ describe('AIIntelligence entitlement gating', () => {
expect(screen.getByRole('button', { name: 'Investigate' })).not.toBeDisabled();
expect(screen.getByRole('button', { name: 'Remediate' })).not.toBeDisabled();
expect(screen.getByRole('checkbox', { name: 'Alert-Triggered Analysis' })).toBeInTheDocument();
expect(screen.getByRole('checkbox', { name: 'Alert-Triggered Patrols' })).toBeInTheDocument();
expect(screen.getByRole('checkbox', { name: 'Anomaly-Triggered Patrols' })).toBeInTheDocument();
expect(
screen.getByRole('checkbox', { name: 'Autonomous critical remediation' }),
).toBeInTheDocument();
expect(screen.queryByRole('link', { name: 'Upgrade to Pro' })).not.toBeInTheDocument();
expect(screen.queryByRole('link', { name: 'Upgrade' })).not.toBeInTheDocument();
});