mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-15 09:49:48 +00:00
Normalize alerts and patrol page headers
This commit is contained in:
parent
50a7d73293
commit
df06fe84b2
9 changed files with 447 additions and 236 deletions
|
|
@ -12,6 +12,7 @@ import { CountdownTimer } from '@/components/patrol';
|
|||
import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy';
|
||||
import { formatRelativeTime } from '@/utils/format';
|
||||
import { groupModelsByProvider } from '@/utils/patrolFormat';
|
||||
import { getPatrolPageHeaderMeta } from '@/utils/patrolPagePresentation';
|
||||
import {
|
||||
getAIQuickstartCreditsPresentation,
|
||||
isPatrolQuickstartExhaustedReason,
|
||||
|
|
@ -23,6 +24,7 @@ import type { PatrolIntelligenceState } from './usePatrolIntelligenceState';
|
|||
|
||||
export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState }) {
|
||||
const state = props.state;
|
||||
const headerMeta = getPatrolPageHeaderMeta();
|
||||
const quickstartPresentation = createMemo(() =>
|
||||
getAIQuickstartCreditsPresentation(
|
||||
state.patrolStatus()?.quickstart_credits_remaining ?? 0,
|
||||
|
|
@ -49,7 +51,9 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
|
|||
const patrolStatus = state.patrolStatus();
|
||||
if (!patrolStatus) return false;
|
||||
if (patrolStatus.using_quickstart) return true;
|
||||
return state.runtimeState() === 'blocked' && isPatrolQuickstartExhaustedReason(state.blockedReason());
|
||||
return (
|
||||
state.runtimeState() === 'blocked' && isPatrolQuickstartExhaustedReason(state.blockedReason())
|
||||
);
|
||||
});
|
||||
const patrolModelStale = createMemo(() => {
|
||||
const model = state.patrolModel();
|
||||
|
|
@ -62,13 +66,14 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
|
|||
<div class="flex-shrink-0 bg-surface border-b border-border px-4 py-3">
|
||||
<PageHeader
|
||||
id="patrol-title"
|
||||
description={headerMeta.description}
|
||||
title={
|
||||
<span
|
||||
class="inline-flex items-center gap-3"
|
||||
title="Pulse Patrol continuously verifies your infrastructure, surfaces actionable findings, and can automatically fix issues based on your autonomy settings."
|
||||
>
|
||||
<PulsePatrolLogo class="w-6 h-6 text-base-content" />
|
||||
<span>Patrol</span>
|
||||
<span>{headerMeta.title}</span>
|
||||
</span>
|
||||
}
|
||||
class="mb-3"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
import { useLocation, useNavigate } from '@solidjs/router';
|
||||
import { logger } from '@/utils/logger';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { SectionHeader } from '@/components/shared/SectionHeader';
|
||||
import { PageHeader } from '@/components/shared/PageHeader';
|
||||
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
import Calendar from 'lucide-solid/icons/calendar';
|
||||
|
|
@ -34,12 +34,8 @@ import {
|
|||
getAlertsTabTitle,
|
||||
isAlertsConfigurationTab,
|
||||
} from '@/utils/alertTabsPresentation';
|
||||
import {
|
||||
getAlertsPageHeaderMeta,
|
||||
} from '@/utils/alertOverviewPresentation';
|
||||
import {
|
||||
getAlertConfigLeaveConfirmation,
|
||||
} from '@/utils/alertConfigPresentation';
|
||||
import { getAlertsPageHeaderMeta } from '@/utils/alertOverviewPresentation';
|
||||
import { getAlertConfigLeaveConfirmation } from '@/utils/alertConfigPresentation';
|
||||
import { useAlertsActivation } from '@/stores/alertsActivation';
|
||||
import LayoutDashboard from 'lucide-solid/icons/layout-dashboard';
|
||||
import History from 'lucide-solid/icons/history';
|
||||
|
|
@ -49,12 +45,7 @@ import { presentationPolicyIsReadOnly } from '@/stores/sessionPresentationPolicy
|
|||
import { AlertsConfigurationSurface } from '@/features/alerts/AlertsConfigurationSurface';
|
||||
import { OverviewTab } from '@/features/alerts/OverviewTab';
|
||||
import { HistoryTab } from '@/features/alerts/tabs/HistoryTab';
|
||||
import {
|
||||
pathForTab,
|
||||
tabFromPath,
|
||||
type AlertTab,
|
||||
type Override,
|
||||
} from '@/features/alerts/types';
|
||||
import { pathForTab, tabFromPath, type AlertTab, type Override } from '@/features/alerts/types';
|
||||
|
||||
export function Alerts() {
|
||||
const { activeAlerts, updateAlert, removeAlerts } = useWebSocket();
|
||||
|
|
@ -66,9 +57,7 @@ export function Alerts() {
|
|||
const readOnlySession = createMemo(() => presentationPolicyIsReadOnly());
|
||||
const isAlertsActive = createMemo(() => alertsActivation.activationState() === 'active');
|
||||
const areAlertsDisabled = createMemo(() => !isAlertsActive());
|
||||
const alertsConfigurationLocked = createMemo(
|
||||
() => !readOnlySession() && areAlertsDisabled(),
|
||||
);
|
||||
const alertsConfigurationLocked = createMemo(() => !readOnlySession() && areAlertsDisabled());
|
||||
const alertActivationPresentation = createMemo(() =>
|
||||
getAlertActivationPresentation({
|
||||
isActive: isAlertsActive(),
|
||||
|
|
@ -127,8 +116,7 @@ export function Alerts() {
|
|||
const [overviewOverrides, setOverviewOverrides] = createSignal<Override[]>([]);
|
||||
const alertsPageHeaderMeta = getAlertsPageHeaderMeta();
|
||||
|
||||
const headerMeta = () =>
|
||||
alertsPageHeaderMeta[activeTab()] ?? alertsPageHeaderMeta.default;
|
||||
const headerMeta = () => alertsPageHeaderMeta[activeTab()] ?? alertsPageHeaderMeta.default;
|
||||
|
||||
createEffect(() => {
|
||||
const currentPath = location.pathname;
|
||||
|
|
@ -175,8 +163,12 @@ export function Alerts() {
|
|||
localStorage.getItem('hideAlertsQuickTip') !== 'true',
|
||||
);
|
||||
|
||||
const runtimeCapabilitiesLoading = createMemo(() => !runtimeCapabilitiesLoaded() || entitlementsLoading());
|
||||
const hasAIAlertsFeature = createMemo(() => !runtimeCapabilitiesLoaded() || hasFeature('ai_alerts'));
|
||||
const runtimeCapabilitiesLoading = createMemo(
|
||||
() => !runtimeCapabilitiesLoaded() || entitlementsLoading(),
|
||||
);
|
||||
const hasAIAlertsFeature = createMemo(
|
||||
() => !runtimeCapabilitiesLoaded() || hasFeature('ai_alerts'),
|
||||
);
|
||||
|
||||
createEffect((wasPaywallVisible) => {
|
||||
const isPaywallVisible =
|
||||
|
|
@ -253,11 +245,11 @@ export function Alerts() {
|
|||
const [sidebarCollapsed, setSidebarCollapsed] = createSignal(false);
|
||||
|
||||
return (
|
||||
<div class="space-y-4">
|
||||
{/* Header with better styling */}
|
||||
<Card padding="md">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<SectionHeader title={headerMeta().title} size="lg" />
|
||||
<div class="space-y-6">
|
||||
<PageHeader
|
||||
title={headerMeta().title}
|
||||
description={headerMeta().description}
|
||||
actions={
|
||||
<Show when={activeTab() === 'overview' && !readOnlySession()}>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class={`text-sm font-medium ${alertActivationPresentation().labelClass}`}>
|
||||
|
|
@ -284,180 +276,172 @@ export function Alerts() {
|
|||
</label>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Card padding="none" class="relative lg:flex overflow-hidden">
|
||||
<Card padding="none" class="relative lg:flex overflow-hidden">
|
||||
<div
|
||||
class={`hidden lg:flex lg:flex-col ${sidebarCollapsed() ? 'w-16' : 'w-72'} ${sidebarCollapsed() ? 'lg:min-w-[4rem] lg:max-w-[4rem] lg:basis-[4rem]' : 'lg:min-w-[18rem] lg:max-w-[18rem] lg:basis-[18rem]'} relative border-b border-border lg:border-b-0 lg:border-r lg:align-top flex-shrink-0 transition-all duration-200`}
|
||||
aria-label="Alerts navigation"
|
||||
aria-expanded={!sidebarCollapsed()}
|
||||
>
|
||||
<div
|
||||
class={`hidden lg:flex lg:flex-col ${sidebarCollapsed() ? 'w-16' : 'w-72'} ${sidebarCollapsed() ? 'lg:min-w-[4rem] lg:max-w-[4rem] lg:basis-[4rem]' : 'lg:min-w-[18rem] lg:max-w-[18rem] lg:basis-[18rem]'} relative border-b border-border lg:border-b-0 lg:border-r lg:align-top flex-shrink-0 transition-all duration-200`}
|
||||
aria-label="Alerts navigation"
|
||||
aria-expanded={!sidebarCollapsed()}
|
||||
class={`sticky top-0 ${sidebarCollapsed() ? 'px-2' : 'px-4'} py-5 space-y-5 transition-all duration-200`}
|
||||
>
|
||||
<div
|
||||
class={`sticky top-0 ${sidebarCollapsed() ? 'px-2' : 'px-4'} py-5 space-y-5 transition-all duration-200`}
|
||||
>
|
||||
<Show when={!sidebarCollapsed()}>
|
||||
<div class="flex items-center justify-between pb-2 border-b border-border">
|
||||
<h2 class="text-sm font-semibold text-base-content">Alerts</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSidebarCollapsed(true)}
|
||||
class="p-1 rounded-md hover:bg-surface-hover transition-colors"
|
||||
aria-label="Collapse sidebar"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 19l-7-7 7-7m8 14l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={sidebarCollapsed()}>
|
||||
<Show when={!sidebarCollapsed()}>
|
||||
<div class="flex items-center justify-between pb-2 border-b border-border">
|
||||
<h2 class="text-sm font-semibold text-base-content">Alerts</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSidebarCollapsed(false)}
|
||||
class="w-full p-2 rounded-md hover:bg-surface-hover transition-colors"
|
||||
aria-label="Expand sidebar"
|
||||
onClick={() => setSidebarCollapsed(true)}
|
||||
class="p-1 rounded-md hover:bg-surface-hover transition-colors"
|
||||
aria-label="Collapse sidebar"
|
||||
>
|
||||
<svg
|
||||
class="w-5 h-5 mx-auto"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 5l7 7-7 7M5 5l7 7-7 7"
|
||||
d="M11 19l-7-7 7-7m8 14l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Show>
|
||||
<div id="alerts-sidebar-menu" class="space-y-5">
|
||||
<For each={tabGroups()}>
|
||||
{(group) => (
|
||||
<div class="space-y-2">
|
||||
<Show when={!sidebarCollapsed()}>
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
{group.label}
|
||||
</p>
|
||||
</Show>
|
||||
<div class="space-y-1.5">
|
||||
<For each={group.items}>
|
||||
{(item) => (
|
||||
<button
|
||||
type="button"
|
||||
aria-current={activeTab() === item.id ? 'page' : undefined}
|
||||
aria-disabled={alertsConfigurationLocked()}
|
||||
disabled={alertsConfigurationLocked()}
|
||||
class={getAlertsSidebarTabClass({
|
||||
isActive: activeTab() === item.id,
|
||||
isDisabled: alertsConfigurationLocked(),
|
||||
collapsed: sidebarCollapsed(),
|
||||
})}
|
||||
onClick={() => handleTabChange(item.id)}
|
||||
title={getAlertsTabTitle({
|
||||
isDisabled: alertsConfigurationLocked(),
|
||||
collapsed: sidebarCollapsed(),
|
||||
label: item.label,
|
||||
})}
|
||||
>
|
||||
{item.icon}
|
||||
<Show when={!sidebarCollapsed()}>
|
||||
<span class="truncate">{item.label}</span>
|
||||
</Show>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<Show when={flatTabs().length > 0}>
|
||||
<div class="lg:hidden border-b border-border">
|
||||
<div class="p-1">
|
||||
<div class="flex w-full overflow-x-auto rounded-md bg-surface-hover p-0.5 touch-scroll scrollbar-hide">
|
||||
<For each={flatTabs()}>
|
||||
{(tab) => (
|
||||
<button
|
||||
type="button"
|
||||
aria-disabled={alertsConfigurationLocked()}
|
||||
disabled={alertsConfigurationLocked()}
|
||||
class={getAlertsMobileTabClass({
|
||||
isActive: activeTab() === tab.id,
|
||||
isDisabled: alertsConfigurationLocked(),
|
||||
})}
|
||||
onClick={() => handleTabChange(tab.id)}
|
||||
title={getAlertsTabTitle({
|
||||
isDisabled: alertsConfigurationLocked(),
|
||||
label: tab.label,
|
||||
})}
|
||||
>
|
||||
<span class="w-full text-center truncate block">{tab.label}</span>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div class="p-2 sm:p-6">
|
||||
<Show when={activeTab() === 'overview'}>
|
||||
<OverviewTab
|
||||
overrides={overviewOverrides()}
|
||||
activeAlerts={activeAlerts}
|
||||
updateAlert={updateAlert}
|
||||
showQuickTip={showQuickTip}
|
||||
dismissQuickTip={dismissQuickTip}
|
||||
showAcknowledged={showAcknowledged}
|
||||
setShowAcknowledged={setShowAcknowledged}
|
||||
alertsDisabled={alertsConfigurationLocked}
|
||||
hasAIAlertsFeature={hasAIAlertsFeature}
|
||||
runtimeCapabilitiesLoading={runtimeCapabilitiesLoading}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={!readOnlySession()}>
|
||||
<AlertsConfigurationSurface
|
||||
activeTab={activeTab}
|
||||
allResources={allResources}
|
||||
byType={byType}
|
||||
children={children}
|
||||
activeAlerts={activeAlerts}
|
||||
removeAlerts={removeAlerts}
|
||||
setOverviewOverrides={setOverviewOverrides}
|
||||
hasUnsavedChanges={hasUnsavedChanges}
|
||||
setHasUnsavedChanges={setHasUnsavedChanges}
|
||||
alertsActivationState={alertsActivation.activationState}
|
||||
alertsActivationConfig={alertsActivation.config}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={activeTab() === 'history'}>
|
||||
<HistoryTab
|
||||
hasAIAlertsFeature={hasAIAlertsFeature}
|
||||
runtimeCapabilitiesLoading={runtimeCapabilitiesLoading}
|
||||
getResource={getResource}
|
||||
allResources={allResources}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={sidebarCollapsed()}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSidebarCollapsed(false)}
|
||||
class="w-full p-2 rounded-md hover:bg-surface-hover transition-colors"
|
||||
aria-label="Expand sidebar"
|
||||
>
|
||||
<svg class="w-5 h-5 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 5l7 7-7 7M5 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Show>
|
||||
<div id="alerts-sidebar-menu" class="space-y-5">
|
||||
<For each={tabGroups()}>
|
||||
{(group) => (
|
||||
<div class="space-y-2">
|
||||
<Show when={!sidebarCollapsed()}>
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
{group.label}
|
||||
</p>
|
||||
</Show>
|
||||
<div class="space-y-1.5">
|
||||
<For each={group.items}>
|
||||
{(item) => (
|
||||
<button
|
||||
type="button"
|
||||
aria-current={activeTab() === item.id ? 'page' : undefined}
|
||||
aria-disabled={alertsConfigurationLocked()}
|
||||
disabled={alertsConfigurationLocked()}
|
||||
class={getAlertsSidebarTabClass({
|
||||
isActive: activeTab() === item.id,
|
||||
isDisabled: alertsConfigurationLocked(),
|
||||
collapsed: sidebarCollapsed(),
|
||||
})}
|
||||
onClick={() => handleTabChange(item.id)}
|
||||
title={getAlertsTabTitle({
|
||||
isDisabled: alertsConfigurationLocked(),
|
||||
collapsed: sidebarCollapsed(),
|
||||
label: item.label,
|
||||
})}
|
||||
>
|
||||
{item.icon}
|
||||
<Show when={!sidebarCollapsed()}>
|
||||
<span class="truncate">{item.label}</span>
|
||||
</Show>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<Show when={flatTabs().length > 0}>
|
||||
<div class="lg:hidden border-b border-border">
|
||||
<div class="p-1">
|
||||
<div class="flex w-full overflow-x-auto rounded-md bg-surface-hover p-0.5 touch-scroll scrollbar-hide">
|
||||
<For each={flatTabs()}>
|
||||
{(tab) => (
|
||||
<button
|
||||
type="button"
|
||||
aria-disabled={alertsConfigurationLocked()}
|
||||
disabled={alertsConfigurationLocked()}
|
||||
class={getAlertsMobileTabClass({
|
||||
isActive: activeTab() === tab.id,
|
||||
isDisabled: alertsConfigurationLocked(),
|
||||
})}
|
||||
onClick={() => handleTabChange(tab.id)}
|
||||
title={getAlertsTabTitle({
|
||||
isDisabled: alertsConfigurationLocked(),
|
||||
label: tab.label,
|
||||
})}
|
||||
>
|
||||
<span class="w-full text-center truncate block">{tab.label}</span>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="p-2 sm:p-6">
|
||||
<Show when={activeTab() === 'overview'}>
|
||||
<OverviewTab
|
||||
overrides={overviewOverrides()}
|
||||
activeAlerts={activeAlerts}
|
||||
updateAlert={updateAlert}
|
||||
showQuickTip={showQuickTip}
|
||||
dismissQuickTip={dismissQuickTip}
|
||||
showAcknowledged={showAcknowledged}
|
||||
setShowAcknowledged={setShowAcknowledged}
|
||||
alertsDisabled={alertsConfigurationLocked}
|
||||
hasAIAlertsFeature={hasAIAlertsFeature}
|
||||
runtimeCapabilitiesLoading={runtimeCapabilitiesLoading}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={!readOnlySession()}>
|
||||
<AlertsConfigurationSurface
|
||||
activeTab={activeTab}
|
||||
allResources={allResources}
|
||||
byType={byType}
|
||||
children={children}
|
||||
activeAlerts={activeAlerts}
|
||||
removeAlerts={removeAlerts}
|
||||
setOverviewOverrides={setOverviewOverrides}
|
||||
hasUnsavedChanges={hasUnsavedChanges}
|
||||
setHasUnsavedChanges={setHasUnsavedChanges}
|
||||
alertsActivationState={alertsActivation.activationState}
|
||||
alertsActivationConfig={alertsActivation.config}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={activeTab() === 'history'}>
|
||||
<HistoryTab
|
||||
hasAIAlertsFeature={hasAIAlertsFeature}
|
||||
runtimeCapabilitiesLoading={runtimeCapabilitiesLoading}
|
||||
getResource={getResource}
|
||||
allResources={allResources}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -256,9 +256,10 @@ vi.mock('@/hooks/usePatrolStream', () => ({
|
|||
}));
|
||||
|
||||
vi.mock('@/components/shared/PageHeader', () => ({
|
||||
PageHeader: (props: { title?: string; actions?: unknown }) => (
|
||||
PageHeader: (props: { title?: string; description?: string; actions?: unknown }) => (
|
||||
<div>
|
||||
<h1>{props.title}</h1>
|
||||
<p>{props.description}</p>
|
||||
<div>{props.actions as any}</div>
|
||||
</div>
|
||||
),
|
||||
|
|
@ -387,8 +388,8 @@ describe('AIIntelligence entitlement gating', () => {
|
|||
href: getPublicPricingUrl(feature),
|
||||
external: true,
|
||||
}));
|
||||
getUpgradeActionUrlOrFallbackMock.mockImplementation(
|
||||
(feature?: string) => getPublicPricingUrl(feature),
|
||||
getUpgradeActionUrlOrFallbackMock.mockImplementation((feature?: string) =>
|
||||
getPublicPricingUrl(feature),
|
||||
);
|
||||
getCorrelationsMock.mockResolvedValue({
|
||||
correlations: [],
|
||||
|
|
@ -659,7 +660,9 @@ describe('AIIntelligence entitlement gating', () => {
|
|||
|
||||
expect(screen.getByText(/Health A · 91\/100/)).toBeInTheDocument();
|
||||
expect(screen.queryByText('Supporting context')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('1 recent change · 4 policy-covered resources')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText('1 recent change · 4 policy-covered resources'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Policy posture')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -217,6 +217,11 @@ describe('tab path helpers', () => {
|
|||
});
|
||||
|
||||
it('keeps alerts configuration owned by a feature surface instead of the page shell', () => {
|
||||
expect(alertsPageSource).toContain(
|
||||
"import { PageHeader } from '@/components/shared/PageHeader';",
|
||||
);
|
||||
expect(alertsPageSource).toContain('<PageHeader');
|
||||
expect(alertsPageSource).toContain('description={headerMeta().description}');
|
||||
expect(alertsPageSource).toContain(
|
||||
"import { AlertsConfigurationSurface } from '@/features/alerts/AlertsConfigurationSurface';",
|
||||
);
|
||||
|
|
@ -312,7 +317,9 @@ describe('tab path helpers', () => {
|
|||
expect(alertDestinationsModelSource).toContain('export function buildAppriseConfigPayload');
|
||||
expect(alertDestinationsModelSource).toContain('formatAppriseTargets');
|
||||
expect(alertDestinationsModelSource).toContain('parseAppriseTargets');
|
||||
expect(alertDestinationsTabStateSource).toContain('export function useAlertDestinationsTabState');
|
||||
expect(alertDestinationsTabStateSource).toContain(
|
||||
'export function useAlertDestinationsTabState',
|
||||
);
|
||||
expect(alertDestinationsTabStateSource).toContain('NotificationsAPI.testNotification');
|
||||
expect(alertDestinationsTabStateSource).toContain('useAlertWebhookDestinationsState');
|
||||
expect(alertDestinationsTabStateSource).not.toContain('NotificationsAPI.getWebhooks');
|
||||
|
|
@ -359,21 +366,21 @@ describe('tab path helpers', () => {
|
|||
expect(alertHistoryTabSource).not.toContain('const loadIncidentTimeline = async');
|
||||
expect(alertHistoryTabSource).not.toContain('const saveIncidentNote = async');
|
||||
expect(alertHistoryTabSource).not.toContain('usePersistentSignal(');
|
||||
expect(alertHistoryTabSource).not.toContain("const [searchTerm, setSearchTerm] = createSignal");
|
||||
expect(alertHistoryTabSource).not.toContain('const [searchTerm, setSearchTerm] = createSignal');
|
||||
expect(alertHistoryFrequencyCardSource).toContain('export function AlertHistoryFrequencyCard');
|
||||
expect(alertHistoryFrequencyCardSource).toContain('getAlertFrequencySelectionPresentation');
|
||||
expect(alertHistoryFrequencyCardSource).toContain('getAlertBucketCountLabel');
|
||||
expect(alertHistoryFiltersCardSource).toContain('export function AlertHistoryFiltersCard');
|
||||
expect(alertHistoryFiltersCardSource).toContain('getAlertHistorySearchPlaceholder');
|
||||
expect(alertResourceIncidentsPanelSource).toContain('export function AlertResourceIncidentsPanel');
|
||||
expect(alertResourceIncidentsPanelSource).toContain(
|
||||
'export function AlertResourceIncidentsPanel',
|
||||
);
|
||||
expect(alertResourceIncidentsPanelSource).toContain('IncidentEventFilters');
|
||||
expect(alertResourceIncidentsPanelSource).toContain('IncidentTimelineEventCard');
|
||||
expect(alertResourceIncidentsPanelSource).toContain('buildResolvedResourceSurfaceLinks');
|
||||
expect(alertResourceIncidentsPanelSource).toContain('allowInfrastructureFallback: true');
|
||||
expect(alertResourceIncidentsPanelSource).not.toContain('buildInfrastructureResourceLink');
|
||||
expect(alertResourceIncidentsPanelSource).not.toContain(
|
||||
'buildResourceSurfaceLinksForResource',
|
||||
);
|
||||
expect(alertResourceIncidentsPanelSource).not.toContain('buildResourceSurfaceLinksForResource');
|
||||
expect(alertResourceIncidentsPanelSource).toContain('{link.compactLabel}');
|
||||
expect(alertHistoryTableSectionSource).toContain('export function AlertHistoryTableSection');
|
||||
expect(alertHistoryTableSectionSource).toContain('AlertHistoryTableGroupRow');
|
||||
|
|
@ -441,7 +448,9 @@ describe('tab path helpers', () => {
|
|||
expect(alertOverviewAlertCardSource).toContain('getAlertOverviewStartedAtClass');
|
||||
expect(alertOverviewAlertCardSource).toContain('getAlertOverviewPrimaryActionClass');
|
||||
expect(alertOverviewAlertCardSource).toContain('getAlertOverviewSecondaryActionClass');
|
||||
expect(alertAcknowledgementStateSource).toContain('export function useAlertAcknowledgementState');
|
||||
expect(alertAcknowledgementStateSource).toContain(
|
||||
'export function useAlertAcknowledgementState',
|
||||
);
|
||||
expect(alertAcknowledgementStateSource).toContain('AlertsAPI.bulkAcknowledge');
|
||||
expect(alertAcknowledgementStateSource).toContain('AlertsAPI.acknowledge');
|
||||
expect(alertAcknowledgementStateSource).toContain('AlertsAPI.unacknowledge');
|
||||
|
|
@ -483,28 +492,46 @@ describe('tab path helpers', () => {
|
|||
expect(thresholdsTableSource).toContain(
|
||||
"import { useThresholdsTableState } from '@/features/alerts/thresholds/hooks/useThresholdsTableState';",
|
||||
);
|
||||
expect(thresholdsTableSource).toContain("import { ThresholdsTableProxmoxTab } from './ThresholdsTableProxmoxTab';");
|
||||
expect(thresholdsTableSource).toContain("import { ThresholdsTablePMGTab } from './ThresholdsTablePMGTab';");
|
||||
expect(thresholdsTableSource).toContain("import { ThresholdsTableAgentsTab } from './ThresholdsTableAgentsTab';");
|
||||
expect(thresholdsTableSource).toContain("import { ThresholdsTableDockerTab } from './ThresholdsTableDockerTab';");
|
||||
expect(thresholdsTableSource).toContain(
|
||||
"import { ThresholdsTableProxmoxTab } from './ThresholdsTableProxmoxTab';",
|
||||
);
|
||||
expect(thresholdsTableSource).toContain(
|
||||
"import { ThresholdsTablePMGTab } from './ThresholdsTablePMGTab';",
|
||||
);
|
||||
expect(thresholdsTableSource).toContain(
|
||||
"import { ThresholdsTableAgentsTab } from './ThresholdsTableAgentsTab';",
|
||||
);
|
||||
expect(thresholdsTableSource).toContain(
|
||||
"import { ThresholdsTableDockerTab } from './ThresholdsTableDockerTab';",
|
||||
);
|
||||
expect(thresholdsTableSource).not.toContain('const [searchTerm, setSearchTerm] = createSignal');
|
||||
expect(thresholdsTableSource).not.toContain('const handleTabClick =');
|
||||
expect(thresholdsTableSource).not.toContain("groupedResources={state.guestsGroupedByNode()}");
|
||||
expect(thresholdsTableSource).not.toContain('groupedResources={state.guestsGroupedByNode()}');
|
||||
expect(thresholdsTableSource).not.toContain('dockerIgnoredPrefixesPresentation.title');
|
||||
expect(thresholdsTableProxmoxTabSource).toContain('export function ThresholdsTableProxmoxTab');
|
||||
expect(thresholdsTableProxmoxTabSource).toContain('ThresholdsTableProxmoxNodesSection');
|
||||
expect(thresholdsTableProxmoxTabSource).toContain('ThresholdsTableProxmoxPBSSection');
|
||||
expect(thresholdsTableProxmoxTabSource).toContain('ThresholdsTableProxmoxGuestsSection');
|
||||
expect(thresholdsTableProxmoxTabSource).toContain('ThresholdsTableProxmoxGuestFilteringSection');
|
||||
expect(thresholdsTableProxmoxTabSource).toContain(
|
||||
'ThresholdsTableProxmoxGuestFilteringSection',
|
||||
);
|
||||
expect(thresholdsTableProxmoxTabSource).toContain('ThresholdsTableProxmoxBackupsSection');
|
||||
expect(thresholdsTableProxmoxTabSource).toContain('ThresholdsTableProxmoxSnapshotsSection');
|
||||
expect(thresholdsTableProxmoxTabSource).toContain('ThresholdsTableProxmoxStorageSection');
|
||||
expect(thresholdsTableProxmoxTabSource).not.toContain('backupOrphanedPresentation');
|
||||
expect(thresholdsTableProxmoxTabSource).not.toContain("sectionTitles.guestFiltering");
|
||||
expect(thresholdsTableSectionPropsSource).toContain('export interface ThresholdsTableSectionProps');
|
||||
expect(thresholdsTableProxmoxNodesSectionSource).toContain('export function ThresholdsTableProxmoxNodesSection');
|
||||
expect(thresholdsTableProxmoxPBSSectionSource).toContain('export function ThresholdsTableProxmoxPBSSection');
|
||||
expect(thresholdsTableProxmoxGuestsSectionSource).toContain('export function ThresholdsTableProxmoxGuestsSection');
|
||||
expect(thresholdsTableProxmoxTabSource).not.toContain('sectionTitles.guestFiltering');
|
||||
expect(thresholdsTableSectionPropsSource).toContain(
|
||||
'export interface ThresholdsTableSectionProps',
|
||||
);
|
||||
expect(thresholdsTableProxmoxNodesSectionSource).toContain(
|
||||
'export function ThresholdsTableProxmoxNodesSection',
|
||||
);
|
||||
expect(thresholdsTableProxmoxPBSSectionSource).toContain(
|
||||
'export function ThresholdsTableProxmoxPBSSection',
|
||||
);
|
||||
expect(thresholdsTableProxmoxGuestsSectionSource).toContain(
|
||||
'export function ThresholdsTableProxmoxGuestsSection',
|
||||
);
|
||||
expect(thresholdsTableProxmoxGuestFilteringSectionSource).toContain(
|
||||
'export function ThresholdsTableProxmoxGuestFilteringSection',
|
||||
);
|
||||
|
|
@ -560,7 +587,9 @@ describe('tab path helpers', () => {
|
|||
expect(thresholdsDataHookSource).toContain('useThresholdsGuestData(inputs)');
|
||||
expect(thresholdsDataHookSource).toContain('useThresholdsInfrastructureData(inputs)');
|
||||
expect(thresholdsDataHookSource).not.toContain('const hostOverrideIdCandidates =');
|
||||
expect(thresholdsDataHookSource).not.toContain('const dockerContainersGroupedByHost = createMemo');
|
||||
expect(thresholdsDataHookSource).not.toContain(
|
||||
'const dockerContainersGroupedByHost = createMemo',
|
||||
);
|
||||
expect(thresholdsHostDataHookSource).toContain('export function useThresholdsHostData');
|
||||
expect(thresholdsHostDataHookSource).toContain('hostOverrideIdCandidates(agentResource)');
|
||||
expect(thresholdsDockerDataHookSource).toContain('export function useThresholdsDockerData');
|
||||
|
|
@ -573,11 +602,15 @@ describe('tab path helpers', () => {
|
|||
expect(thresholdsResourceModelSource).toContain('export function buildNodeHeaderMeta');
|
||||
expect(thresholdsResourceModelSource).toContain('export const normalizeStorageStatus');
|
||||
expect(thresholdsTableStateHookSource).toContain('export function useThresholdsTableState');
|
||||
expect(thresholdsTableStateHookSource).toContain('useThresholdsData(props, editingId, searchTerm)');
|
||||
expect(thresholdsTableStateHookSource).toContain(
|
||||
'useThresholdsData(props, editingId, searchTerm)',
|
||||
);
|
||||
expect(thresholdsTableStateHookSource).toContain('useThresholdsRecoveryDefaultsState(props)');
|
||||
expect(thresholdsTableStateHookSource).toContain('useThresholdsOverrideMutations');
|
||||
expect(thresholdsTableStateHookSource).toContain('useThresholdsAvailabilityMutations');
|
||||
expect(thresholdsTableStateHookSource).not.toContain('const saveEdit = (resourceId: string) => {');
|
||||
expect(thresholdsTableStateHookSource).not.toContain(
|
||||
'const saveEdit = (resourceId: string) => {',
|
||||
);
|
||||
expect(thresholdsTableStateHookSource).not.toContain(
|
||||
'const toggleNodeConnectivity = (resourceId: string, forceState?: boolean) => {',
|
||||
);
|
||||
|
|
@ -591,7 +624,9 @@ describe('tab path helpers', () => {
|
|||
expect(thresholdsOverrideMutationsHookSource).toContain(
|
||||
'export function useThresholdsOverrideMutations',
|
||||
);
|
||||
expect(thresholdsOverrideMutationsHookSource).toContain('const saveEdit = (resourceId: string) => {');
|
||||
expect(thresholdsOverrideMutationsHookSource).toContain(
|
||||
'const saveEdit = (resourceId: string) => {',
|
||||
);
|
||||
expect(thresholdsOverrideMutationsHookSource).toContain(
|
||||
'const handleSaveBulkEdit = (thresholds: Record<string, number | undefined>) => {',
|
||||
);
|
||||
|
|
@ -610,9 +645,7 @@ describe('tab path helpers', () => {
|
|||
'const setOfflineState = (resourceId: string, state: OfflineState) => {',
|
||||
);
|
||||
expect(thresholdsOverrideMutationModelSource).toContain('export const upsertOverride =');
|
||||
expect(thresholdsOverrideMutationModelSource).toContain(
|
||||
'export const withThresholdEntries =',
|
||||
);
|
||||
expect(thresholdsOverrideMutationModelSource).toContain('export const withThresholdEntries =');
|
||||
expect(thresholdsOverrideMutationModelSource).toContain('export const stripStateKeys =');
|
||||
});
|
||||
|
||||
|
|
@ -738,16 +771,12 @@ describe('incident timeline presentation helpers', () => {
|
|||
);
|
||||
expect(getAlertIncidentTimelineHeadingClass()).toBe('font-medium text-base-content');
|
||||
expect(getAlertIncidentTimelineDetailClass()).toBe('mt-1 text-xs text-base-content');
|
||||
expect(getAlertIncidentTimelineCommandClass()).toBe(
|
||||
'mt-1 font-mono text-xs text-base-content',
|
||||
);
|
||||
expect(getAlertIncidentTimelineCommandClass()).toBe('mt-1 font-mono text-xs text-base-content');
|
||||
expect(getAlertIncidentTimelineOutputClass()).toBe('mt-1 text-xs text-muted');
|
||||
});
|
||||
|
||||
it('returns the resource incident panel presentation', () => {
|
||||
expect(getAlertResourceIncidentCardClass()).toBe(
|
||||
'rounded border border-border bg-surface p-3',
|
||||
);
|
||||
expect(getAlertResourceIncidentCardClass()).toBe('rounded border border-border bg-surface p-3');
|
||||
expect(getAlertResourceIncidentSummaryRowClass()).toBe(
|
||||
'mt-2 flex flex-wrap items-center justify-between gap-2 text-xs text-muted',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,10 +7,16 @@ import {
|
|||
ALERT_HISTORY_SEARCH_PLACEHOLDER,
|
||||
ALERTS_EMPTY_STATE,
|
||||
ALERTS_PAGE_DEFAULT_TITLE,
|
||||
ALERTS_PAGE_DEFAULT_DESCRIPTION,
|
||||
ALERTS_PAGE_DESTINATIONS_DESCRIPTION,
|
||||
ALERTS_PAGE_DESTINATIONS_TITLE,
|
||||
ALERTS_PAGE_HISTORY_DESCRIPTION,
|
||||
ALERTS_PAGE_HISTORY_TITLE,
|
||||
ALERTS_PAGE_OVERVIEW_DESCRIPTION,
|
||||
ALERTS_PAGE_OVERVIEW_TITLE,
|
||||
ALERTS_PAGE_SCHEDULE_DESCRIPTION,
|
||||
ALERTS_PAGE_SCHEDULE_TITLE,
|
||||
ALERTS_PAGE_THRESHOLDS_DESCRIPTION,
|
||||
ALERTS_PAGE_THRESHOLDS_TITLE,
|
||||
ALERT_TIMELINE_EMPTY_STATE,
|
||||
ALERT_TIMELINE_FAILURE_STATE,
|
||||
|
|
@ -60,9 +66,7 @@ describe('alertOverviewPresentation', () => {
|
|||
expect(ALERT_HISTORY_SEARCH_PLACEHOLDER).toBe('Search alerts...');
|
||||
expect(getAlertHistorySearchPlaceholder()).toBe('Search alerts...');
|
||||
expect(ALERT_HISTORY_EMPTY_STATE).toBe('No alerts found');
|
||||
expect(ALERT_HISTORY_EMPTY_DESCRIPTION).toBe(
|
||||
'Try adjusting your filters or check back later',
|
||||
);
|
||||
expect(ALERT_HISTORY_EMPTY_DESCRIPTION).toBe('Try adjusting your filters or check back later');
|
||||
expect(getAlertHistoryEmptyState()).toEqual({
|
||||
title: 'No alerts found',
|
||||
description: 'Try adjusting your filters or check back later',
|
||||
|
|
@ -80,13 +84,54 @@ describe('alertOverviewPresentation', () => {
|
|||
expect(ALERTS_PAGE_DESTINATIONS_TITLE).toBe('Notification Destinations');
|
||||
expect(ALERTS_PAGE_SCHEDULE_TITLE).toBe('Maintenance Schedule');
|
||||
expect(ALERTS_PAGE_HISTORY_TITLE).toBe('Alert History');
|
||||
expect(ALERTS_PAGE_DEFAULT_DESCRIPTION).toBe(
|
||||
'Review active incidents, inspect alert history, and manage thresholds, notifications, and schedules.',
|
||||
);
|
||||
expect(ALERTS_PAGE_OVERVIEW_DESCRIPTION).toBe(
|
||||
'Review active incidents, confirm alert coverage, and control whether alerts are actively monitoring this install.',
|
||||
);
|
||||
expect(ALERTS_PAGE_THRESHOLDS_DESCRIPTION).toBe(
|
||||
'Tune thresholds and scoped overrides for infrastructure, workloads, and integrations.',
|
||||
);
|
||||
expect(ALERTS_PAGE_DESTINATIONS_DESCRIPTION).toBe(
|
||||
'Route alert notifications to email, Apprise, and webhook destinations.',
|
||||
);
|
||||
expect(ALERTS_PAGE_SCHEDULE_DESCRIPTION).toBe(
|
||||
'Define quiet hours, grouping, cooldowns, recovery, and escalation behavior for alert delivery.',
|
||||
);
|
||||
expect(ALERTS_PAGE_HISTORY_DESCRIPTION).toBe(
|
||||
'Search prior alerts, review incident timelines, and inspect alert frequency over time.',
|
||||
);
|
||||
expect(getAlertsPageHeaderMeta()).toEqual({
|
||||
overview: { title: 'Alerts Overview' },
|
||||
thresholds: { title: 'Alert Thresholds' },
|
||||
destinations: { title: 'Notification Destinations' },
|
||||
schedule: { title: 'Maintenance Schedule' },
|
||||
history: { title: 'Alert History' },
|
||||
default: { title: 'Alerts' },
|
||||
overview: {
|
||||
title: 'Alerts Overview',
|
||||
description:
|
||||
'Review active incidents, confirm alert coverage, and control whether alerts are actively monitoring this install.',
|
||||
},
|
||||
thresholds: {
|
||||
title: 'Alert Thresholds',
|
||||
description:
|
||||
'Tune thresholds and scoped overrides for infrastructure, workloads, and integrations.',
|
||||
},
|
||||
destinations: {
|
||||
title: 'Notification Destinations',
|
||||
description: 'Route alert notifications to email, Apprise, and webhook destinations.',
|
||||
},
|
||||
schedule: {
|
||||
title: 'Maintenance Schedule',
|
||||
description:
|
||||
'Define quiet hours, grouping, cooldowns, recovery, and escalation behavior for alert delivery.',
|
||||
},
|
||||
history: {
|
||||
title: 'Alert History',
|
||||
description:
|
||||
'Search prior alerts, review incident timelines, and inspect alert frequency over time.',
|
||||
},
|
||||
default: {
|
||||
title: 'Alerts',
|
||||
description:
|
||||
'Review active incidents, inspect alert history, and manage thresholds, notifications, and schedules.',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
PATROL_PAGE_DESCRIPTION,
|
||||
PATROL_PAGE_TITLE,
|
||||
getPatrolPageHeaderMeta,
|
||||
} from '@/utils/patrolPagePresentation';
|
||||
|
||||
describe('patrolPagePresentation', () => {
|
||||
it('returns canonical Patrol page header metadata', () => {
|
||||
expect(PATROL_PAGE_TITLE).toBe('Patrol');
|
||||
expect(PATROL_PAGE_DESCRIPTION).toBe(
|
||||
'Continuously verify infrastructure health, review findings, and control Patrol runtime behavior.',
|
||||
);
|
||||
expect(getPatrolPageHeaderMeta()).toEqual({
|
||||
title: 'Patrol',
|
||||
description:
|
||||
'Continuously verify infrastructure health, review findings, and control Patrol runtime behavior.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
export const ALERTS_EMPTY_STATE = 'No active alerts';
|
||||
export const ALERTS_THRESHOLD_HINT = 'Alerts will appear here when thresholds are exceeded';
|
||||
export const ALERT_TIMELINE_LOADING_STATE = 'Loading timeline...';
|
||||
export const ALERT_TIMELINE_FILTER_EMPTY_STATE =
|
||||
'No timeline events match the selected filters.';
|
||||
export const ALERT_TIMELINE_FILTER_EMPTY_STATE = 'No timeline events match the selected filters.';
|
||||
export const ALERT_TIMELINE_EMPTY_STATE = 'No timeline events yet.';
|
||||
export const ALERT_TIMELINE_UNAVAILABLE_STATE = 'No incident timeline available.';
|
||||
export const ALERT_TIMELINE_FAILURE_STATE = 'Failed to load timeline.';
|
||||
|
|
@ -18,6 +17,18 @@ export const ALERTS_PAGE_THRESHOLDS_TITLE = 'Alert Thresholds';
|
|||
export const ALERTS_PAGE_DESTINATIONS_TITLE = 'Notification Destinations';
|
||||
export const ALERTS_PAGE_SCHEDULE_TITLE = 'Maintenance Schedule';
|
||||
export const ALERTS_PAGE_HISTORY_TITLE = 'Alert History';
|
||||
export const ALERTS_PAGE_DEFAULT_DESCRIPTION =
|
||||
'Review active incidents, inspect alert history, and manage thresholds, notifications, and schedules.';
|
||||
export const ALERTS_PAGE_OVERVIEW_DESCRIPTION =
|
||||
'Review active incidents, confirm alert coverage, and control whether alerts are actively monitoring this install.';
|
||||
export const ALERTS_PAGE_THRESHOLDS_DESCRIPTION =
|
||||
'Tune thresholds and scoped overrides for infrastructure, workloads, and integrations.';
|
||||
export const ALERTS_PAGE_DESTINATIONS_DESCRIPTION =
|
||||
'Route alert notifications to email, Apprise, and webhook destinations.';
|
||||
export const ALERTS_PAGE_SCHEDULE_DESCRIPTION =
|
||||
'Define quiet hours, grouping, cooldowns, recovery, and escalation behavior for alert delivery.';
|
||||
export const ALERTS_PAGE_HISTORY_DESCRIPTION =
|
||||
'Search prior alerts, review incident timelines, and inspect alert frequency over time.';
|
||||
|
||||
export type DashboardAlertTone = 'default' | 'warning' | 'danger';
|
||||
|
||||
|
|
@ -34,12 +45,30 @@ export interface AlertOverviewCardPresentation {
|
|||
|
||||
export function getAlertsPageHeaderMeta() {
|
||||
return {
|
||||
overview: { title: ALERTS_PAGE_OVERVIEW_TITLE },
|
||||
thresholds: { title: ALERTS_PAGE_THRESHOLDS_TITLE },
|
||||
destinations: { title: ALERTS_PAGE_DESTINATIONS_TITLE },
|
||||
schedule: { title: ALERTS_PAGE_SCHEDULE_TITLE },
|
||||
history: { title: ALERTS_PAGE_HISTORY_TITLE },
|
||||
default: { title: ALERTS_PAGE_DEFAULT_TITLE },
|
||||
overview: {
|
||||
title: ALERTS_PAGE_OVERVIEW_TITLE,
|
||||
description: ALERTS_PAGE_OVERVIEW_DESCRIPTION,
|
||||
},
|
||||
thresholds: {
|
||||
title: ALERTS_PAGE_THRESHOLDS_TITLE,
|
||||
description: ALERTS_PAGE_THRESHOLDS_DESCRIPTION,
|
||||
},
|
||||
destinations: {
|
||||
title: ALERTS_PAGE_DESTINATIONS_TITLE,
|
||||
description: ALERTS_PAGE_DESTINATIONS_DESCRIPTION,
|
||||
},
|
||||
schedule: {
|
||||
title: ALERTS_PAGE_SCHEDULE_TITLE,
|
||||
description: ALERTS_PAGE_SCHEDULE_DESCRIPTION,
|
||||
},
|
||||
history: {
|
||||
title: ALERTS_PAGE_HISTORY_TITLE,
|
||||
description: ALERTS_PAGE_HISTORY_DESCRIPTION,
|
||||
},
|
||||
default: {
|
||||
title: ALERTS_PAGE_DEFAULT_TITLE,
|
||||
description: ALERTS_PAGE_DEFAULT_DESCRIPTION,
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
|
|
|
|||
10
frontend-modern/src/utils/patrolPagePresentation.ts
Normal file
10
frontend-modern/src/utils/patrolPagePresentation.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export const PATROL_PAGE_TITLE = 'Patrol';
|
||||
export const PATROL_PAGE_DESCRIPTION =
|
||||
'Continuously verify infrastructure health, review findings, and control Patrol runtime behavior.';
|
||||
|
||||
export function getPatrolPageHeaderMeta() {
|
||||
return {
|
||||
title: PATROL_PAGE_TITLE,
|
||||
description: PATROL_PAGE_DESCRIPTION,
|
||||
} as const;
|
||||
}
|
||||
86
tests/integration/tests/60-page-header-consistency.spec.ts
Normal file
86
tests/integration/tests/60-page-header-consistency.spec.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { readRuntimeState } from "./runtime-defaults";
|
||||
|
||||
const PAGE_HEADER_ROUTES = [
|
||||
{
|
||||
slug: "operations",
|
||||
route: "/operations",
|
||||
title: "Operations",
|
||||
description:
|
||||
"Run diagnostics, review generated reports, and inspect system logs without leaving the app.",
|
||||
},
|
||||
{
|
||||
slug: "alerts",
|
||||
route: "/alerts/overview",
|
||||
title: "Alerts Overview",
|
||||
description:
|
||||
"Review active incidents, confirm alert coverage, and control whether alerts are actively monitoring this install.",
|
||||
},
|
||||
{
|
||||
slug: "patrol",
|
||||
route: "/patrol",
|
||||
title: "Patrol",
|
||||
description:
|
||||
"Continuously verify infrastructure health, review findings, and control Patrol runtime behavior.",
|
||||
},
|
||||
] as const;
|
||||
|
||||
const PRIMARY_API_TOKEN =
|
||||
process.env.PULSE_E2E_PRIMARY_API_TOKEN?.trim() ||
|
||||
(typeof readRuntimeState()?.primaryAPIToken === "string"
|
||||
? readRuntimeState()!.primaryAPIToken!.trim()
|
||||
: "");
|
||||
|
||||
test.skip(
|
||||
PRIMARY_API_TOKEN === "",
|
||||
"Top-level header browser proof requires a runtime API token.",
|
||||
);
|
||||
|
||||
test.describe("Top-level page header consistency", () => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript((token: string) => {
|
||||
sessionStorage.setItem(
|
||||
"pulse_auth",
|
||||
JSON.stringify({
|
||||
type: "token",
|
||||
value: token,
|
||||
}),
|
||||
);
|
||||
sessionStorage.setItem("pulse_auth_user", "admin");
|
||||
localStorage.setItem("pulse_whats_new_v2_shown", "true");
|
||||
}, PRIMARY_API_TOKEN);
|
||||
});
|
||||
|
||||
for (const surface of PAGE_HEADER_ROUTES) {
|
||||
test(`renders the canonical header framing on ${surface.route}`, async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
await page.goto(surface.route, { waitUntil: "domcontentloaded" });
|
||||
|
||||
const pageHeading = page.getByRole("heading", {
|
||||
level: 1,
|
||||
name: surface.title,
|
||||
});
|
||||
await expect(
|
||||
pageHeading,
|
||||
`${surface.route} should render a single top-level heading`,
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(surface.description, { exact: true }).first(),
|
||||
`${surface.route} should render the canonical subheader copy`,
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator("h1"),
|
||||
`${surface.route} should not render duplicate page headings`,
|
||||
).toHaveCount(1);
|
||||
|
||||
const screenshotPath = testInfo.outputPath(`${surface.slug}.png`);
|
||||
await page.screenshot({ path: screenshotPath });
|
||||
console.log(
|
||||
`[page-header-consistency] screenshot ${surface.slug}: ${screenshotPath}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue