mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Converge update modal chrome on shared primitives
This commit is contained in:
@@ -523,6 +523,10 @@ not a replacement status card, CTA band, or page-local nested card.
|
||||
Shared error-boundary fallback actions are also command buttons: reset,
|
||||
reload, and retry controls must compose `Button` so emergency UI does not
|
||||
become a separate local button vocabulary.
|
||||
Update confirmation and progress modal actions are part of the same command
|
||||
boundary: cancel, start, retry, close, history, reload-now, and close-icon
|
||||
controls must compose `Button` or `ActionIconButton` instead of carrying
|
||||
modal-local blue, neutral, or icon-button class strings.
|
||||
Compact icon-only row, inline, and floating action controls belong to
|
||||
`ActionIconButton`. Feature surfaces may own the icon choice, click handler,
|
||||
label text, and layout slot, but icon-button size, tone, focus ring,
|
||||
@@ -1372,6 +1376,10 @@ not a replacement status card, CTA band, or page-local nested card.
|
||||
notices are part of the same settings callout boundary: the editor and
|
||||
credential slots own the source-specific lifecycle or API meaning, while
|
||||
`CalloutCard` owns the warning/success/danger shell and compact density.
|
||||
Update confirmation and progress modal notices share that same primitive
|
||||
boundary. The update flow owns version, prerequisite, root-access, restart,
|
||||
and error copy; `CalloutCard` owns the info/warning/danger shell, spacing,
|
||||
dark-mode tone, and icon layout.
|
||||
8. Keep hosted settings-shell framing imports safe for bundle initialization.
|
||||
Self-hosted billing titles, descriptions, and referral copy used by
|
||||
`settingsHeaderMeta.ts`, `settingsNavCatalog.ts`, and adjacent settings
|
||||
@@ -1786,6 +1794,9 @@ primitive internals such as `Button`, `PulseDataGrid`, and
|
||||
`HistoryChartOverlay`, as well as Login, Settings, Patrol, and AI finding
|
||||
surfaces; local `animate-spin` spinner shells in those consumers are governed
|
||||
by the shared-template registry rather than page-local discretion.
|
||||
Update progress status indicators are included in that loading boundary:
|
||||
progress-stage loading must compose `LoadingSpinner` rather than local spinner
|
||||
SVGs.
|
||||
`DiscoveryLoadingFallback` owns the discovery-tab Suspense fallback row for
|
||||
resource, workload, and Docker host drawers: centered row layout, status
|
||||
semantics, discovery loading copy, and canonical `LoadingSpinner` composition
|
||||
@@ -3398,6 +3409,10 @@ Shared error-boundary fallbacks use the same boundary: the fallback owns error
|
||||
copy and reset/reload handlers, while `CalloutCard` owns danger tone, spacing,
|
||||
dark-mode styling, and alert layout instead of inline red panels or raw SVG
|
||||
alert glyphs.
|
||||
Update confirmation and progress modals use the same shared boundary: the
|
||||
modal flow owns update state and copy, while `CalloutCard`, `Button`,
|
||||
`ActionIconButton`, `LoadingSpinner`, and lucide icons own the colored notice,
|
||||
command, icon-only close, and status indicator chrome.
|
||||
|
||||
Settings loading placeholders must route through the shared
|
||||
`SettingsLoadingSkeleton` primitive instead of local `animate-pulse` blocks.
|
||||
|
||||
@@ -415,6 +415,48 @@
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "update-modal-action-callout-shell",
|
||||
"category": "callout",
|
||||
"summary": "Update confirmation and progress modals must compose CalloutCard, Button, ActionIconButton, LoadingSpinner, and library icons for shared status, warning, restart, and command chrome instead of recreating local colored panels, raw SVG glyphs, or modal-local button classes.",
|
||||
"canonical": {
|
||||
"path": "src/components/shared/CalloutCard.tsx",
|
||||
"export": "CalloutCard"
|
||||
},
|
||||
"requiredConsumers": [
|
||||
{ "path": "src/components/UpdateConfirmationModal.tsx" },
|
||||
{ "path": "src/components/UpdateProgressModal.tsx" }
|
||||
],
|
||||
"forbiddenPatterns": [
|
||||
{
|
||||
"path": "src/components/UpdateConfirmationModal.tsx",
|
||||
"patterns": [
|
||||
"<svg",
|
||||
"bg-blue-50 dark:bg-blue-900 border border-blue-200",
|
||||
"bg-yellow-50 dark:bg-yellow-900 border border-yellow-200",
|
||||
"rounded-md p-4 border",
|
||||
"px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "src/components/UpdateProgressModal.tsx",
|
||||
"patterns": [
|
||||
"<svg",
|
||||
"bg-red-50 dark:bg-red-900 border border-red-200",
|
||||
"bg-blue-50 dark:bg-blue-900 border border-blue-200",
|
||||
"bg-yellow-50 dark:bg-yellow-900 border border-yellow-200",
|
||||
"mt-2 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded"
|
||||
]
|
||||
}
|
||||
],
|
||||
"proof": [
|
||||
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
||||
"src/components/__tests__/UpdateConfirmationModal.test.tsx",
|
||||
"src/components/shared/CalloutCard.test.tsx",
|
||||
"src/components/shared/Button.test.tsx",
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "platform-inline-notice-shell",
|
||||
"category": "callout",
|
||||
@@ -495,6 +537,7 @@
|
||||
{ "path": "src/components/Settings/UpdatesSettingsPanel.tsx" },
|
||||
{ "path": "src/components/Settings/UserAssignmentsDialog.tsx" },
|
||||
{ "path": "src/components/Settings/UserAssignmentsPanel.tsx" },
|
||||
{ "path": "src/components/UpdateProgressModal.tsx" },
|
||||
{ "path": "src/components/shared/Button.tsx" },
|
||||
{ "path": "src/components/shared/DiscoveryLoadingFallback.tsx" },
|
||||
{ "path": "src/components/shared/HistoryChartOverlay.tsx" },
|
||||
@@ -733,6 +776,13 @@
|
||||
{
|
||||
"path": "src/components/Settings/SecurityOverviewPanel.tsx",
|
||||
"patterns": ["animate-pulse", "bg-slate-300"]
|
||||
},
|
||||
{
|
||||
"path": "src/components/UpdateProgressModal.tsx",
|
||||
"patterns": [
|
||||
"<svg class=\"w-12 h-12 text-blue-500 animate-spin\"",
|
||||
"class=\"animate-spin h-4 w-4\""
|
||||
]
|
||||
}
|
||||
],
|
||||
"proof": [
|
||||
@@ -779,6 +829,8 @@
|
||||
{ "path": "src/components/Settings/InfrastructureWorkspace.tsx" },
|
||||
{ "path": "src/components/Settings/ReportingPanel.tsx" },
|
||||
{ "path": "src/components/Settings/ResourcePicker.tsx" },
|
||||
{ "path": "src/components/UpdateConfirmationModal.tsx" },
|
||||
{ "path": "src/components/UpdateProgressModal.tsx" },
|
||||
{ "path": "src/components/Workloads/GuestDrawer.tsx" },
|
||||
{ "path": "src/features/patrol/PatrolIntelligenceWorkspace.tsx" },
|
||||
{ "path": "src/features/standalone/StandalonePageSurface.tsx" }
|
||||
@@ -834,6 +886,20 @@
|
||||
"px-4 py-2 bg-slate-600 text-white rounded hover:bg-slate-700",
|
||||
"text-xs px-2 py-1 bg-red-600 text-white rounded hover:bg-red-700"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "src/components/UpdateConfirmationModal.tsx",
|
||||
"patterns": [
|
||||
"px-4 py-2 text-sm font-medium text-base-content hover:bg-surface-hover rounded-md transition-colors",
|
||||
"px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "src/components/UpdateProgressModal.tsx",
|
||||
"patterns": [
|
||||
"mt-2 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded",
|
||||
"px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
|
||||
]
|
||||
}
|
||||
],
|
||||
"proof": [
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { createEffect, createSignal, Show, For } from 'solid-js';
|
||||
import type { UpdatePlan } from '@/api/updates';
|
||||
import AlertTriangleIcon from 'lucide-solid/icons/alert-triangle';
|
||||
import ArrowRightIcon from 'lucide-solid/icons/arrow-right';
|
||||
import CheckCircleIcon from 'lucide-solid/icons/check-circle';
|
||||
import ClockIcon from 'lucide-solid/icons/clock';
|
||||
import LockIcon from 'lucide-solid/icons/lock';
|
||||
import XIcon from 'lucide-solid/icons/x';
|
||||
import { ActionIconButton, Button } from '@/components/shared/Button';
|
||||
import { CalloutCard } from '@/components/shared/CalloutCard';
|
||||
import { Dialog } from '@/components/shared/Dialog';
|
||||
|
||||
interface UpdateConfirmationModalProps {
|
||||
@@ -34,6 +42,13 @@ export function UpdateConfirmationModal(props: UpdateConfirmationModalProps) {
|
||||
props.onConfirm();
|
||||
}
|
||||
};
|
||||
const warningTone = () => (props.isMajorUpgrade ? 'warning' : 'info');
|
||||
const warningTitle = () =>
|
||||
props.isMajorUpgrade && props.isPrerelease
|
||||
? 'Major Version Pre-Release'
|
||||
: props.isMajorUpgrade
|
||||
? 'Major Version Upgrade'
|
||||
: 'Pre-Release Build';
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -48,121 +63,47 @@ export function UpdateConfirmationModal(props: UpdateConfirmationModalProps) {
|
||||
<div class="px-6 py-4 border-b border-border">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold text-base-content">Confirm Update</h2>
|
||||
<button
|
||||
<ActionIconButton
|
||||
onClick={handleClose}
|
||||
aria-label="Close confirmation"
|
||||
label="Close confirmation"
|
||||
title="Close"
|
||||
class=" hover:text-base-content"
|
||||
tone="muted"
|
||||
size="md"
|
||||
disabled={props.isApplying}
|
||||
type="button"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<XIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</ActionIconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div class="px-6 py-4 space-y-4">
|
||||
{/* Version Jump */}
|
||||
<div class="bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-800 rounded-md p-4">
|
||||
<div class="text-sm font-medium text-blue-900 dark:text-blue-100 mb-2">
|
||||
Version Update
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-blue-800 dark:text-blue-200">
|
||||
<CalloutCard tone="info" scale="compact" padding="md" title="Version Update">
|
||||
<div class="flex items-center gap-3 text-sm text-blue-800 dark:text-blue-200">
|
||||
<span class="font-mono text-sm">{props.currentVersion}</span>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 7l5 5m0 0l-5 5m5-5H6"
|
||||
/>
|
||||
</svg>
|
||||
<ArrowRightIcon class="h-4 w-4" aria-hidden="true" />
|
||||
<span class="font-mono text-sm font-semibold">{props.latestVersion}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CalloutCard>
|
||||
|
||||
{/* Major version / pre-release warning */}
|
||||
<Show when={props.warning}>
|
||||
<div
|
||||
class={`rounded-md p-4 border ${
|
||||
props.isMajorUpgrade && props.isPrerelease
|
||||
? 'bg-orange-50 dark:bg-orange-950 border-orange-300 dark:border-orange-700'
|
||||
: props.isMajorUpgrade
|
||||
? 'bg-amber-50 dark:bg-amber-950 border-amber-300 dark:border-amber-700'
|
||||
: 'bg-blue-50 dark:bg-blue-950 border-blue-300 dark:border-blue-700'
|
||||
}`}
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<svg
|
||||
class={`w-5 h-5 flex-shrink-0 mt-0.5 ${
|
||||
props.isMajorUpgrade && props.isPrerelease
|
||||
? 'text-orange-500 dark:text-orange-400'
|
||||
: props.isMajorUpgrade
|
||||
? 'text-amber-500 dark:text-amber-400'
|
||||
: 'text-blue-500 dark:text-blue-400'
|
||||
}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<div class="text-sm">
|
||||
<p
|
||||
class={`font-medium ${
|
||||
props.isMajorUpgrade && props.isPrerelease
|
||||
? 'text-orange-800 dark:text-orange-200'
|
||||
: props.isMajorUpgrade
|
||||
? 'text-amber-800 dark:text-amber-200'
|
||||
: 'text-blue-800 dark:text-blue-200'
|
||||
}`}
|
||||
>
|
||||
{props.isMajorUpgrade && props.isPrerelease
|
||||
? 'Major Version Pre-Release'
|
||||
: props.isMajorUpgrade
|
||||
? 'Major Version Upgrade'
|
||||
: 'Pre-Release Build'}
|
||||
</p>
|
||||
<p
|
||||
class={`mt-1 ${
|
||||
props.isMajorUpgrade && props.isPrerelease
|
||||
? 'text-orange-700 dark:text-orange-300'
|
||||
: props.isMajorUpgrade
|
||||
? 'text-amber-700 dark:text-amber-300'
|
||||
: 'text-blue-700 dark:text-blue-300'
|
||||
}`}
|
||||
>
|
||||
{props.warning}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CalloutCard
|
||||
tone={warningTone()}
|
||||
scale="compact"
|
||||
padding="md"
|
||||
icon={<AlertTriangleIcon class="h-5 w-5" aria-hidden="true" />}
|
||||
title={warningTitle()}
|
||||
description={<span class="text-sm">{props.warning}</span>}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
{/* Estimated Time */}
|
||||
<Show when={props.plan.estimatedTime}>
|
||||
<div class="flex items-center gap-2 text-sm text-muted">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<ClockIcon class="h-4 w-4" aria-hidden="true" />
|
||||
<span>Estimated time: {props.plan.estimatedTime}</span>
|
||||
</div>
|
||||
</Show>
|
||||
@@ -175,19 +116,10 @@ export function UpdateConfirmationModal(props: UpdateConfirmationModalProps) {
|
||||
<For each={props.plan.prerequisites}>
|
||||
{(prerequisite) => (
|
||||
<li class="flex items-start gap-2 text-sm text-base-content">
|
||||
<svg
|
||||
class="w-4 h-4 mt-0.5 flex-shrink-0 text-orange-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<AlertTriangleIcon
|
||||
class="mt-0.5 h-4 w-4 flex-shrink-0 text-amber-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{prerequisite}</span>
|
||||
</li>
|
||||
)}
|
||||
@@ -198,42 +130,24 @@ export function UpdateConfirmationModal(props: UpdateConfirmationModalProps) {
|
||||
|
||||
{/* Root Required Warning */}
|
||||
<Show when={props.plan.requiresRoot}>
|
||||
<div class="bg-yellow-50 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-800 rounded-md p-3">
|
||||
<div class="flex items-start gap-2">
|
||||
<svg
|
||||
class="w-5 h-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
<div class="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
<div class="font-medium">Root access required</div>
|
||||
<div class="text-yellow-700 dark:text-yellow-300 mt-1">
|
||||
This update requires elevated privileges to modify system files.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CalloutCard
|
||||
tone="warning"
|
||||
scale="compact"
|
||||
padding="md"
|
||||
icon={<LockIcon class="h-5 w-5" aria-hidden="true" />}
|
||||
title="Root access required"
|
||||
description={
|
||||
<span class="text-sm">
|
||||
This update requires elevated privileges to modify system files.
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
{/* Rollback Support */}
|
||||
<Show when={props.plan.rollbackSupport}>
|
||||
<div class="flex items-center gap-2 text-sm text-green-600 dark:text-green-400">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<CheckCircleIcon class="h-4 w-4" aria-hidden="true" />
|
||||
<span>Automatic backup will be created</span>
|
||||
</div>
|
||||
</Show>
|
||||
@@ -258,39 +172,25 @@ export function UpdateConfirmationModal(props: UpdateConfirmationModalProps) {
|
||||
|
||||
{/* Footer */}
|
||||
<div class="px-6 py-4 bg-surface-alt border-t border-border flex items-center justify-end gap-3">
|
||||
<button
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
disabled={props.isApplying}
|
||||
class="px-4 py-2 text-sm font-medium text-base-content hover:bg-surface-hover rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
variant="ghost"
|
||||
size="md"
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
disabled={!acknowledged() || props.isApplying}
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
isLoading={props.isApplying}
|
||||
variant="primary"
|
||||
size="md"
|
||||
type="button"
|
||||
>
|
||||
<Show when={props.isApplying}>
|
||||
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
</Show>
|
||||
<span>{props.isApplying ? 'Starting...' : 'Start Update'}</span>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { createSignal, Show, onCleanup, createEffect } from 'solid-js';
|
||||
import { UpdatesAPI, type UpdateStatus } from '@/api/updates';
|
||||
import AlertTriangleIcon from 'lucide-solid/icons/alert-triangle';
|
||||
import CheckCircleIcon from 'lucide-solid/icons/check-circle';
|
||||
import InfoIcon from 'lucide-solid/icons/info';
|
||||
import { Dialog } from '@/components/shared/Dialog';
|
||||
import { ActionIconButton, Button } from '@/components/shared/Button';
|
||||
import { CalloutCard } from '@/components/shared/CalloutCard';
|
||||
import { LoadingSpinner } from '@/components/shared/LoadingSpinner';
|
||||
import { ProgressBar } from '@/components/shared/ProgressBar';
|
||||
import { apiFetch } from '@/utils/apiClient';
|
||||
import { logger } from '@/utils/logger';
|
||||
@@ -310,48 +316,14 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
|
||||
if (!currentStatus) return null;
|
||||
|
||||
if (hasError()) {
|
||||
return (
|
||||
<svg class="w-12 h-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
return <AlertTriangleIcon class="h-12 w-12 text-red-500" aria-hidden="true" />;
|
||||
}
|
||||
|
||||
if (isComplete() && !hasError()) {
|
||||
return (
|
||||
<svg class="w-12 h-12 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
return <CheckCircleIcon class="h-12 w-12 text-emerald-500" aria-hidden="true" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<svg class="w-12 h-12 text-blue-500 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
return <LoadingSpinner size="lg" tone="info" label="Update in progress" />;
|
||||
};
|
||||
|
||||
const getStatusText = () => {
|
||||
@@ -391,23 +363,24 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
|
||||
<div class="px-6 py-4 border-b border-border">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold text-base-content">Updating Pulse</h2>
|
||||
<button
|
||||
<ActionIconButton
|
||||
onClick={handleClose}
|
||||
class="text-muted hover:text-base-content"
|
||||
type="button"
|
||||
aria-label={
|
||||
label={
|
||||
isComplete()
|
||||
? 'Close update progress'
|
||||
: 'Hide update progress. The update continues server-side.'
|
||||
}
|
||||
tone="muted"
|
||||
size="md"
|
||||
type="button"
|
||||
title={
|
||||
isComplete()
|
||||
? 'Close update progress'
|
||||
: 'Hide update progress. GlobalUpdateProgressWatcher keeps tracking the server-side update.'
|
||||
}
|
||||
>
|
||||
<XIcon class="w-5 h-5" />
|
||||
</button>
|
||||
<XIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</ActionIconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -443,78 +416,65 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
|
||||
|
||||
{/* Error Message */}
|
||||
<Show when={hasError() && status()?.error}>
|
||||
<div class="mt-6 bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-800 rounded-md p-4">
|
||||
<div class="text-sm text-red-800 dark:text-red-200">
|
||||
<div class="font-medium mb-1">Error Details:</div>
|
||||
<div class="text-red-700 dark:text-red-300">{status()!.error}</div>
|
||||
</div>
|
||||
</div>
|
||||
<CalloutCard
|
||||
tone="danger"
|
||||
scale="compact"
|
||||
padding="md"
|
||||
class="mt-6"
|
||||
icon={<AlertTriangleIcon class="h-5 w-5" aria-hidden="true" />}
|
||||
title="Error Details"
|
||||
description={<span class="text-sm">{status()!.error}</span>}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
{/* Warning / Info */}
|
||||
<Show when={!isComplete()}>
|
||||
<Show when={isRestarting()}>
|
||||
<div class="mt-6 bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-800 rounded-md p-3">
|
||||
<div class="flex items-start gap-2">
|
||||
<svg
|
||||
class="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
<CalloutCard
|
||||
tone="info"
|
||||
scale="compact"
|
||||
padding="md"
|
||||
class="mt-6"
|
||||
icon={<InfoIcon class="h-5 w-5" aria-hidden="true" />}
|
||||
description={
|
||||
<Show
|
||||
when={wsDisconnected()}
|
||||
fallback={
|
||||
<span class="text-sm">Pulse is restarting with the new version...</span>
|
||||
}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<div class="flex-1">
|
||||
<div class="text-sm text-blue-800 dark:text-blue-200">
|
||||
<Show
|
||||
when={wsDisconnected()}
|
||||
fallback={<span>Pulse is restarting with the new version...</span>}
|
||||
>
|
||||
<span>
|
||||
Waiting for Pulse to complete restart. This page will reload
|
||||
automatically.
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={wsDisconnected() && healthCheckAttempts() > 5}>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
class="mt-2 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded transition-colors"
|
||||
type="button"
|
||||
>
|
||||
Reload Now
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-sm">
|
||||
Waiting for Pulse to complete restart. This page will reload automatically.
|
||||
</span>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<Show when={wsDisconnected() && healthCheckAttempts() > 5}>
|
||||
<Button
|
||||
onClick={() => window.location.reload()}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="mt-2"
|
||||
type="button"
|
||||
>
|
||||
Reload Now
|
||||
</Button>
|
||||
</Show>
|
||||
</CalloutCard>
|
||||
</Show>
|
||||
<Show when={!isRestarting()}>
|
||||
<div class="mt-6 bg-yellow-50 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-800 rounded-md p-3">
|
||||
<div class="flex items-start gap-2">
|
||||
<svg
|
||||
class="w-5 h-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<div class="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
<CalloutCard
|
||||
tone="warning"
|
||||
scale="compact"
|
||||
padding="md"
|
||||
class="mt-6"
|
||||
icon={<AlertTriangleIcon class="h-5 w-5" aria-hidden="true" />}
|
||||
description={
|
||||
<span class="text-sm">
|
||||
Please do not close this window or refresh the page during the update.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
@@ -523,30 +483,23 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
|
||||
<Show when={isComplete()}>
|
||||
<div class="px-6 py-4 bg-surface-alt border-t border-border flex items-center justify-end gap-3">
|
||||
<Show when={!hasError()}>
|
||||
<button
|
||||
onClick={props.onViewHistory}
|
||||
class="px-4 py-2 text-sm font-medium text-base-content hover:bg-surface-hover rounded-md transition-colors"
|
||||
type="button"
|
||||
>
|
||||
<Button onClick={props.onViewHistory} variant="ghost" size="md" type="button">
|
||||
View History
|
||||
</button>
|
||||
</Button>
|
||||
</Show>
|
||||
<Show when={hasError()}>
|
||||
<button
|
||||
<Button
|
||||
onClick={() => window.location.reload()}
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
|
||||
variant="primary"
|
||||
size="md"
|
||||
type="button"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</Button>
|
||||
</Show>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
|
||||
type="button"
|
||||
>
|
||||
<Button onClick={handleClose} variant="primary" size="md" type="button">
|
||||
Close
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
|
||||
import { createSignal } from 'solid-js';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { UpdateConfirmationModal } from '@/components/UpdateConfirmationModal';
|
||||
import updateConfirmationModalSource from '@/components/UpdateConfirmationModal.tsx?raw';
|
||||
|
||||
describe('UpdateConfirmationModal', () => {
|
||||
afterEach(() => {
|
||||
@@ -44,4 +45,21 @@ describe('UpdateConfirmationModal', () => {
|
||||
expect((reopenedCheckbox as HTMLInputElement).checked).toBe(false);
|
||||
expect(await screen.findByRole('button', { name: 'Start Update' })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('keeps update confirmation chrome on shared primitives', () => {
|
||||
expect(updateConfirmationModalSource).toContain('CalloutCard');
|
||||
expect(updateConfirmationModalSource).toContain('Button');
|
||||
expect(updateConfirmationModalSource).toContain('ActionIconButton');
|
||||
expect(updateConfirmationModalSource).toContain('lucide-solid/icons/arrow-right');
|
||||
expect(updateConfirmationModalSource).not.toContain('<svg');
|
||||
expect(updateConfirmationModalSource).not.toContain(
|
||||
'bg-blue-50 dark:bg-blue-900 border border-blue-200',
|
||||
);
|
||||
expect(updateConfirmationModalSource).not.toContain(
|
||||
'bg-yellow-50 dark:bg-yellow-900 border border-yellow-200',
|
||||
);
|
||||
expect(updateConfirmationModalSource).not.toContain(
|
||||
'px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,6 +116,8 @@ import sharedTemplateRegistrySource from '../../../scripts/shared-template-regis
|
||||
import discoveryTabSource from '@/components/Discovery/DiscoveryTab.tsx?raw';
|
||||
import emailProviderSelectSource from '@/components/Alerts/EmailProviderSelect.tsx?raw';
|
||||
import errorBoundarySource from '@/components/ErrorBoundary.tsx?raw';
|
||||
import updateConfirmationModalSource from '@/components/UpdateConfirmationModal.tsx?raw';
|
||||
import updateProgressModalSource from '@/components/UpdateProgressModal.tsx?raw';
|
||||
import incidentTimelinePanelSource from '@/components/Alerts/IncidentTimelinePanel.tsx?raw';
|
||||
import alertDetailPresentationSource from '@/utils/alertDetailPresentation.ts?raw';
|
||||
import alertSeverityPresentationSource from '@/utils/alertSeverityPresentation.ts?raw';
|
||||
@@ -2520,6 +2522,7 @@ describe('shared primitive guardrails', () => {
|
||||
'src/components/Settings/UpdatesSettingsPanel.tsx',
|
||||
'src/components/Settings/UserAssignmentsDialog.tsx',
|
||||
'src/components/Settings/UserAssignmentsPanel.tsx',
|
||||
'src/components/UpdateProgressModal.tsx',
|
||||
'src/components/shared/Button.tsx',
|
||||
'src/components/shared/DiscoveryLoadingFallback.tsx',
|
||||
'src/components/shared/HistoryChartOverlay.tsx',
|
||||
@@ -2616,6 +2619,7 @@ describe('shared primitive guardrails', () => {
|
||||
discoveryLoadingFallbackSource,
|
||||
historyChartOverlaySource,
|
||||
pulseDataGridSource,
|
||||
updateProgressModalSource,
|
||||
]) {
|
||||
expect(source).toContain('LoadingSpinner');
|
||||
expect(source).not.toContain('animate-spin');
|
||||
@@ -2904,6 +2908,8 @@ describe('shared primitive guardrails', () => {
|
||||
'src/components/Settings/InfrastructureWorkspace.tsx',
|
||||
'src/components/Settings/ReportingPanel.tsx',
|
||||
'src/components/Settings/ResourcePicker.tsx',
|
||||
'src/components/UpdateConfirmationModal.tsx',
|
||||
'src/components/UpdateProgressModal.tsx',
|
||||
'src/components/Workloads/GuestDrawer.tsx',
|
||||
'src/features/patrol/PatrolIntelligenceWorkspace.tsx',
|
||||
'src/features/standalone/StandalonePageSurface.tsx',
|
||||
@@ -2961,6 +2967,20 @@ describe('shared primitive guardrails', () => {
|
||||
'text-xs px-2 py-1 bg-red-600 text-white rounded hover:bg-red-700',
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
path: 'src/components/UpdateConfirmationModal.tsx',
|
||||
patterns: expect.arrayContaining([
|
||||
'px-4 py-2 text-sm font-medium text-base-content hover:bg-surface-hover rounded-md transition-colors',
|
||||
'px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors',
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
path: 'src/components/UpdateProgressModal.tsx',
|
||||
patterns: expect.arrayContaining([
|
||||
'mt-2 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded',
|
||||
'px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors',
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(errorBoundarySource).toContain('Button');
|
||||
@@ -2973,6 +2993,22 @@ describe('shared primitive guardrails', () => {
|
||||
expect(errorBoundarySource).not.toContain(
|
||||
'text-xs px-2 py-1 bg-red-600 text-white rounded hover:bg-red-700',
|
||||
);
|
||||
expect(updateConfirmationModalSource).toContain('Button');
|
||||
expect(updateConfirmationModalSource).toContain('ActionIconButton');
|
||||
expect(updateConfirmationModalSource).not.toContain(
|
||||
'px-4 py-2 text-sm font-medium text-base-content hover:bg-surface-hover rounded-md transition-colors',
|
||||
);
|
||||
expect(updateConfirmationModalSource).not.toContain(
|
||||
'px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors',
|
||||
);
|
||||
expect(updateProgressModalSource).toContain('Button');
|
||||
expect(updateProgressModalSource).toContain('ActionIconButton');
|
||||
expect(updateProgressModalSource).not.toContain(
|
||||
'mt-2 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded',
|
||||
);
|
||||
expect(updateProgressModalSource).not.toContain(
|
||||
'px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors',
|
||||
);
|
||||
expect(registeredGuard?.canonical?.path).toBe('src/components/shared/buttonModel.ts');
|
||||
expect(registeredGuard?.canonical?.export).toBe('getButtonClass');
|
||||
expect(registeredGuard?.allPatterns).toEqual([
|
||||
@@ -3735,6 +3771,74 @@ describe('shared primitive guardrails', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('routes update modal callouts, actions, and status indicators through shared primitives', () => {
|
||||
const registry = JSON.parse(sharedTemplateRegistrySource) as {
|
||||
rules?: Array<{
|
||||
id: string;
|
||||
canonical?: { path?: string; export?: string };
|
||||
requiredConsumers?: Array<{ path?: string }>;
|
||||
forbiddenPatterns?: Array<{ path?: string; patterns?: string[] }>;
|
||||
}>;
|
||||
};
|
||||
const registeredRule = registry.rules?.find(
|
||||
(rule) => rule.id === 'update-modal-action-callout-shell',
|
||||
);
|
||||
|
||||
expect(registeredRule?.canonical?.path).toBe('src/components/shared/CalloutCard.tsx');
|
||||
expect(registeredRule?.canonical?.export).toBe('CalloutCard');
|
||||
expect(registeredRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual([
|
||||
'src/components/UpdateConfirmationModal.tsx',
|
||||
'src/components/UpdateProgressModal.tsx',
|
||||
]);
|
||||
expect(registeredRule?.forbiddenPatterns).toEqual([
|
||||
{
|
||||
path: 'src/components/UpdateConfirmationModal.tsx',
|
||||
patterns: [
|
||||
'<svg',
|
||||
'bg-blue-50 dark:bg-blue-900 border border-blue-200',
|
||||
'bg-yellow-50 dark:bg-yellow-900 border border-yellow-200',
|
||||
'rounded-md p-4 border',
|
||||
'px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors',
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'src/components/UpdateProgressModal.tsx',
|
||||
patterns: [
|
||||
'<svg',
|
||||
'bg-red-50 dark:bg-red-900 border border-red-200',
|
||||
'bg-blue-50 dark:bg-blue-900 border border-blue-200',
|
||||
'bg-yellow-50 dark:bg-yellow-900 border border-yellow-200',
|
||||
'mt-2 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded',
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(updateConfirmationModalSource).toContain('CalloutCard');
|
||||
expect(updateConfirmationModalSource).toContain('Button');
|
||||
expect(updateConfirmationModalSource).toContain('ActionIconButton');
|
||||
expect(updateConfirmationModalSource).toContain('lucide-solid/icons/arrow-right');
|
||||
expect(updateProgressModalSource).toContain('CalloutCard');
|
||||
expect(updateProgressModalSource).toContain('Button');
|
||||
expect(updateProgressModalSource).toContain('ActionIconButton');
|
||||
expect(updateProgressModalSource).toContain('LoadingSpinner');
|
||||
|
||||
for (const source of [updateConfirmationModalSource, updateProgressModalSource]) {
|
||||
expect(source).not.toContain('<svg');
|
||||
expect(source).not.toContain('bg-blue-50 dark:bg-blue-900 border border-blue-200');
|
||||
expect(source).not.toContain('bg-yellow-50 dark:bg-yellow-900 border border-yellow-200');
|
||||
expect(source).not.toContain(
|
||||
'px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors',
|
||||
);
|
||||
}
|
||||
expect(updateConfirmationModalSource).not.toContain('rounded-md p-4 border');
|
||||
expect(updateProgressModalSource).not.toContain(
|
||||
'bg-red-50 dark:bg-red-900 border border-red-200',
|
||||
);
|
||||
expect(updateProgressModalSource).not.toContain(
|
||||
'mt-2 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded',
|
||||
);
|
||||
});
|
||||
|
||||
it('routes platform inline notices through InlineNotice', () => {
|
||||
const registry = JSON.parse(sharedTemplateRegistrySource) as {
|
||||
rules?: Array<{
|
||||
|
||||
Reference in New Issue
Block a user