Delete remaining orphans surfaced by final cleanup sweep

Final tail of the post-IA-migration cleanup. None of these files have
non-test consumers; most were last touched by the retired aggregate
chrome:

- components/shared/SummaryPanel.tsx — old summary panel primitive,
  superseded by the per-platform table cards that replaced the
  Infrastructure/Workloads aggregate summaries.
- components/shared/MigrationNoticeBanner.tsx — one-shot upgrade
  banner with no live consumer.
- components/Infrastructure/AgentDeployModal.tsx — replaced by the
  per-platform deploy flows in features/standalone (Machines). Drops
  its unit test and the AgentDeployWizardE2E test that exercised it.
- utils/storageSummaryTrendCache.ts — small in-memory cache only
  consumed by the deleted Storage summary chart pipeline.

Also drops the leftover dead STORAGE_KEYS entries: STORAGE_SHOW_FILTERS,
STORAGE_VIEW_MODE, STORAGE_SOURCE_FILTER, RECOVERY_SHOW_FILTERS,
RECOVERY_USE_RELATIVE_TIME, WORKLOADS_SHOW_FILTERS, WORKLOADS_CARD_VIEW,
WORKLOADS_AUTO_REFRESH. Persisted user-side values under those key
strings will simply stop being read; Pulse never read them back after
the standalone surfaces went away. WORKLOADS_SEARCH_HISTORY,
AGENTS_SEARCH_HISTORY, etc. remain — they back live filter-bar search
history.

Baseline test failures (10) unchanged.
This commit is contained in:
rcourtman
2026-05-26 17:06:21 +01:00
parent 7b924bcc73
commit b91dc8dd4c
7 changed files with 0 additions and 2106 deletions
@@ -1,208 +0,0 @@
import { Component, Show, Switch, Match, createMemo } from 'solid-js';
import { Dialog } from '@/components/shared/Dialog';
import { StepIndicator } from '@/components/SetupWizard/StepIndicator';
import { useDeployWizard, type WizardStep } from '@/hooks/useDeployWizard';
import { CandidatesStep } from './deploy/CandidatesStep';
import { PreflightStep } from './deploy/PreflightStep';
import { ConfirmStep } from './deploy/ConfirmStep';
import { DeployingStep } from './deploy/DeployingStep';
import { ResultsStep } from './deploy/ResultsStep';
import XIcon from 'lucide-solid/icons/x';
interface AgentDeployModalProps {
isOpen: boolean;
clusterId: string;
clusterName: string;
onClose: () => void;
}
const STEP_LABELS = ['Select Hosts', 'Preflight', 'Confirm', 'Deploy', 'Results'];
const stepToIndex: Record<WizardStep, number> = {
candidates: 0,
preflight: 1,
confirm: 2,
deploying: 3,
results: 4,
};
export const AgentDeployModal: Component<AgentDeployModalProps> = (props) => {
const wizard = useDeployWizard({
clusterId: props.clusterId,
clusterName: props.clusterName,
});
const currentStepIndex = createMemo(() => stepToIndex[wizard.step()]);
const canRunPreflight = createMemo(
() =>
wizard.selectedNodeIds().size > 0 &&
wizard.selectedSourceAgent() !== '' &&
!wizard.startingPreflight(),
);
const handleClose = () => {
if (wizard.isOperationActive()) {
// Don't block close — just warn (closing won't cancel).
// The deployment continues in the background.
}
props.onClose();
};
return (
<Dialog
isOpen={props.isOpen}
onClose={handleClose}
panelClass="max-w-4xl"
closeOnBackdrop={!wizard.isOperationActive()}
ariaLabel={`Deploy Agents — ${props.clusterName}`}
>
{/* Header */}
<div class="flex items-center justify-between border-b border-border px-4 py-3">
<h2 class="text-sm font-semibold text-base-content">Deploy Agents — {props.clusterName}</h2>
<button
type="button"
onClick={handleClose}
class="rounded-md p-1 text-muted hover:text-base-content hover:bg-surface-hover transition-colors"
aria-label="Close"
>
<XIcon class="w-4 h-4" />
</button>
</div>
{/* Step indicator */}
<nav aria-label="Deploy progress" class="border-b border-border px-4 py-2">
<StepIndicator steps={STEP_LABELS} currentStep={currentStepIndex()} />
</nav>
{/* Body */}
<div class="overflow-y-auto p-4 max-h-[60vh]" aria-live="polite">
<Switch>
<Match when={wizard.step() === 'candidates'}>
<CandidatesStep wizard={wizard} />
</Match>
<Match when={wizard.step() === 'preflight'}>
<PreflightStep wizard={wizard} />
</Match>
<Match when={wizard.step() === 'confirm'}>
<ConfirmStep wizard={wizard} />
</Match>
<Match when={wizard.step() === 'deploying'}>
<DeployingStep wizard={wizard} />
</Match>
<Match when={wizard.step() === 'results'}>
<ResultsStep wizard={wizard} />
</Match>
</Switch>
</div>
{/* Footer */}
<div class="flex items-center justify-between border-t border-border px-4 py-3">
<div class="flex gap-2">
<Switch>
<Match when={wizard.step() === 'candidates'}>
<button
type="button"
onClick={props.onClose}
class="rounded-md border border-border px-3 py-1.5 text-xs font-medium text-muted hover:bg-surface-hover transition-colors"
>
Cancel
</button>
</Match>
<Match when={wizard.step() === 'confirm'}>
<button
type="button"
onClick={() => wizard.setStep('candidates')}
class="rounded-md border border-border px-3 py-1.5 text-xs font-medium text-muted hover:bg-surface-hover transition-colors"
>
Back
</button>
</Match>
<Match when={wizard.step() === 'preflight'}>
<span class="text-xs text-muted">Preflight in progress...</span>
</Match>
<Match when={wizard.step() === 'deploying'}>
<span class="text-xs text-muted">
<Show when={wizard.isOperationActive()}>Closing won't cancel the deployment.</Show>
</span>
</Match>
</Switch>
</div>
<div class="flex gap-2">
<Switch>
<Match when={wizard.step() === 'candidates'}>
<button
type="button"
onClick={() => wizard.startPreflight()}
disabled={!canRunPreflight()}
class="rounded-md bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<Show when={wizard.startingPreflight()} fallback="Run Preflight">
<span class="flex items-center gap-1.5">
<span class="h-3 w-3 animate-spin rounded-full border-2 border-white border-t-transparent" />
Starting...
</span>
</Show>
</button>
</Match>
<Match when={wizard.step() === 'confirm'}>
<button
type="button"
onClick={() => wizard.startDeploy()}
disabled={wizard.confirmSelectedNodeIds().size === 0 || wizard.startingDeploy()}
class="rounded-md bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<Show
when={wizard.startingDeploy()}
fallback={`Deploy ${wizard.confirmSelectedNodeIds().size} Host${wizard.confirmSelectedNodeIds().size !== 1 ? 's' : ''}`}
>
<span class="flex items-center gap-1.5">
<span class="h-3 w-3 animate-spin rounded-full border-2 border-white border-t-transparent" />
Starting...
</span>
</Show>
</button>
</Match>
<Match when={wizard.step() === 'deploying'}>
<button
type="button"
onClick={() => wizard.cancelDeploy()}
disabled={wizard.canceling()}
class="rounded-md bg-red-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-red-700 disabled:opacity-50 transition-colors"
>
<Show when={wizard.canceling()} fallback="Cancel Deployment">
Canceling...
</Show>
</button>
</Match>
<Match when={wizard.step() === 'results'}>
<Show when={wizard.retryableTargets().length > 0}>
<button
type="button"
onClick={() => wizard.retryFailed()}
disabled={wizard.retrying()}
class="rounded-md bg-amber-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-amber-700 disabled:opacity-50 transition-colors"
>
<Show
when={wizard.retrying()}
fallback={`Retry ${wizard.retryableTargets().length} Failed`}
>
Retrying...
</Show>
</button>
</Show>
<button
type="button"
onClick={props.onClose}
class="rounded-md border border-border px-3 py-1.5 text-xs font-medium text-base-content hover:bg-surface-hover transition-colors"
>
Close
</button>
</Match>
</Switch>
</div>
</div>
</Dialog>
);
};
@@ -1,682 +0,0 @@
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, fireEvent, cleanup } from '@solidjs/testing-library';
import { createSignal } from 'solid-js';
import type { Accessor } from 'solid-js';
/* ── hoisted mocks ────────────────────────────────────────────── */
// Build a controllable mock wizard factory.
// Each test can override individual fields before render.
const { useDeployWizardMock } = vi.hoisted(() => ({
useDeployWizardMock: vi.fn(),
}));
vi.mock('@/hooks/useDeployWizard', () => ({
useDeployWizard: useDeployWizardMock,
}));
// Track ConfirmStep props to verify parent wiring.
const { confirmStepPropsSpy } = vi.hoisted(() => ({
confirmStepPropsSpy: vi.fn(),
}));
// Stub child step components — we don't test their internals here.
vi.mock('../deploy/CandidatesStep', () => ({
CandidatesStep: () => <div data-testid="candidates-step">CandidatesStep</div>,
}));
vi.mock('../deploy/PreflightStep', () => ({
PreflightStep: () => <div data-testid="preflight-step">PreflightStep</div>,
}));
vi.mock('../deploy/ConfirmStep', () => ({
ConfirmStep: (props: Record<string, unknown>) => {
confirmStepPropsSpy(props);
return <div data-testid="confirm-step">ConfirmStep</div>;
},
}));
vi.mock('../deploy/DeployingStep', () => ({
DeployingStep: () => <div data-testid="deploying-step">DeployingStep</div>,
}));
vi.mock('../deploy/ResultsStep', () => ({
ResultsStep: () => <div data-testid="results-step">ResultsStep</div>,
}));
// Stub StepIndicator to expose the currentStep prop for assertion.
vi.mock('@/components/SetupWizard/StepIndicator', () => ({
StepIndicator: (props: { steps: string[]; currentStep: number }) => (
<div data-testid="step-indicator" data-current-step={props.currentStep}>
{props.steps.join(' | ')}
</div>
),
}));
/* ── component import (after mocks) ──────────────────────────── */
import { AgentDeployModal } from '../AgentDeployModal';
/* ── helpers ──────────────────────────────────────────────────── */
type WizardStep = 'candidates' | 'preflight' | 'confirm' | 'deploying' | 'results';
/** Creates a full mock wizard with sensible defaults. */
function createMockWizard(overrides: Partial<Record<string, unknown>> = {}) {
const [step, setStep] = createSignal<WizardStep>((overrides.step as WizardStep) ?? 'candidates');
const [selectedNodeIds] = createSignal<Set<string>>(
(overrides.selectedNodeIds as Set<string>) ?? new Set<string>(),
);
const [selectedSourceAgent] = createSignal<string>(
(overrides.selectedSourceAgent as string) ?? '',
);
const [startingPreflight] = createSignal<boolean>(
(overrides.startingPreflight as boolean) ?? false,
);
const [startingDeploy] = createSignal<boolean>((overrides.startingDeploy as boolean) ?? false);
const [canceling] = createSignal<boolean>((overrides.canceling as boolean) ?? false);
const [retrying] = createSignal<boolean>((overrides.retrying as boolean) ?? false);
const [confirmSelectedNodeIds] = createSignal<Set<string>>(
(overrides.confirmSelectedNodeIds as Set<string>) ?? new Set<string>(),
);
const readyNodesData = (overrides.readyNodes as unknown[]) ?? [];
const readyNodes: Accessor<unknown[]> = () => readyNodesData;
const retryableTargetsData = (overrides.retryableTargets as unknown[]) ?? [];
const retryableTargets: Accessor<unknown[]> = () => retryableTargetsData;
const isOperationActive: Accessor<boolean> =
(overrides.isOperationActive as Accessor<boolean>) ??
(() => step() === 'preflight' || step() === 'deploying');
return {
step,
setStep,
selectedNodeIds,
selectedSourceAgent,
confirmSelectedNodeIds,
startingPreflight,
startingDeploy,
canceling,
retrying,
readyNodes,
retryableTargets,
isOperationActive,
startPreflight: overrides.startPreflight ?? vi.fn(),
startDeploy: overrides.startDeploy ?? vi.fn(),
cancelDeploy: overrides.cancelDeploy ?? vi.fn(),
retryFailed: overrides.retryFailed ?? vi.fn(),
};
}
const defaultProps = () => ({
isOpen: true,
clusterId: 'cluster-1',
clusterName: 'My Cluster',
onClose: vi.fn(),
});
/* ── lifecycle ────────────────────────────────────────────────── */
beforeEach(() => {
vi.clearAllMocks();
// Default: return a basic wizard in 'candidates' step.
useDeployWizardMock.mockReturnValue(createMockWizard());
});
afterEach(() => {
cleanup();
document.body.style.overflow = '';
});
/* ================================================================
Tests
================================================================ */
describe('AgentDeployModal', () => {
/* ── Rendering ─────────────────────────────────────────────── */
describe('rendering', () => {
it('renders the dialog with cluster name in the title', () => {
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByText('Deploy Agents — My Cluster')).toBeInTheDocument();
});
it('sets aria-label on the dialog with cluster name', () => {
render(() => <AgentDeployModal {...defaultProps()} />);
const dialog = screen.getByRole('dialog');
expect(dialog).toHaveAttribute('aria-label', 'Deploy Agents — My Cluster');
});
it('renders the StepIndicator with step labels', () => {
render(() => <AgentDeployModal {...defaultProps()} />);
const indicator = screen.getByTestId('step-indicator');
expect(indicator).toHaveTextContent('Select Hosts | Preflight | Confirm | Deploy | Results');
});
it('has a close X button with aria-label', () => {
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByLabelText('Close')).toBeInTheDocument();
});
it('does not render when isOpen is false', () => {
const props = defaultProps();
props.isOpen = false;
render(() => <AgentDeployModal {...props} />);
expect(screen.queryByText('Deploy Agents — My Cluster')).not.toBeInTheDocument();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(document.querySelector('[data-dialog-backdrop]')).toBeNull();
});
});
/* ── Step routing ──────────────────────────────────────────── */
describe('step routing', () => {
const allStepTestIds = [
'candidates-step',
'preflight-step',
'confirm-step',
'deploying-step',
'results-step',
];
function expectOnlyStep(activeTestId: string) {
expect(screen.getByTestId(activeTestId)).toBeInTheDocument();
for (const id of allStepTestIds) {
if (id !== activeTestId) {
expect(screen.queryByTestId(id)).not.toBeInTheDocument();
}
}
}
it('shows only CandidatesStep when step is candidates', () => {
useDeployWizardMock.mockReturnValue(createMockWizard({ step: 'candidates' }));
render(() => <AgentDeployModal {...defaultProps()} />);
expectOnlyStep('candidates-step');
});
it('shows only PreflightStep when step is preflight', () => {
useDeployWizardMock.mockReturnValue(createMockWizard({ step: 'preflight' }));
render(() => <AgentDeployModal {...defaultProps()} />);
expectOnlyStep('preflight-step');
});
it('shows only ConfirmStep when step is confirm', () => {
useDeployWizardMock.mockReturnValue(createMockWizard({ step: 'confirm' }));
render(() => <AgentDeployModal {...defaultProps()} />);
expectOnlyStep('confirm-step');
});
it('shows only DeployingStep when step is deploying', () => {
useDeployWizardMock.mockReturnValue(createMockWizard({ step: 'deploying' }));
render(() => <AgentDeployModal {...defaultProps()} />);
expectOnlyStep('deploying-step');
});
it('shows only ResultsStep when step is results', () => {
useDeployWizardMock.mockReturnValue(createMockWizard({ step: 'results' }));
render(() => <AgentDeployModal {...defaultProps()} />);
expectOnlyStep('results-step');
});
});
/* ── Step indicator index ──────────────────────────────────── */
describe('step indicator index', () => {
const cases: [WizardStep, number][] = [
['candidates', 0],
['preflight', 1],
['confirm', 2],
['deploying', 3],
['results', 4],
];
it.each(cases)('maps wizard step "%s" to indicator index %d', (wizardStep, expectedIndex) => {
useDeployWizardMock.mockReturnValue(createMockWizard({ step: wizardStep }));
render(() => <AgentDeployModal {...defaultProps()} />);
const indicator = screen.getByTestId('step-indicator');
expect(indicator.dataset.currentStep).toBe(String(expectedIndex));
});
});
/* ── Candidates step footer ────────────────────────────────── */
describe('candidates step footer', () => {
it('shows Cancel and Run Preflight buttons', () => {
useDeployWizardMock.mockReturnValue(createMockWizard({ step: 'candidates' }));
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByText('Cancel')).toBeInTheDocument();
expect(screen.getByText('Run Preflight')).toBeInTheDocument();
});
it('Cancel calls onClose', () => {
useDeployWizardMock.mockReturnValue(createMockWizard({ step: 'candidates' }));
const props = defaultProps();
render(() => <AgentDeployModal {...props} />);
fireEvent.click(screen.getByText('Cancel'));
expect(props.onClose).toHaveBeenCalledTimes(1);
});
it('disables Run Preflight when no nodes selected', () => {
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'candidates',
selectedNodeIds: new Set<string>(),
selectedSourceAgent: 'agent-1',
}),
);
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByText('Run Preflight')).toBeDisabled();
});
it('disables Run Preflight when no source agent selected', () => {
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'candidates',
selectedNodeIds: new Set(['node-1']),
selectedSourceAgent: '',
}),
);
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByText('Run Preflight')).toBeDisabled();
});
it('enables Run Preflight when nodes and source agent are selected', () => {
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'candidates',
selectedNodeIds: new Set(['node-1']),
selectedSourceAgent: 'agent-1',
}),
);
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByText('Run Preflight')).not.toBeDisabled();
});
it('disables and shows Starting... when startingPreflight is true', () => {
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'candidates',
selectedNodeIds: new Set(['node-1']),
selectedSourceAgent: 'agent-1',
startingPreflight: true,
}),
);
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByText('Starting...')).toBeInTheDocument();
// The button containing "Starting..." should be disabled.
const btn = screen.getByText('Starting...').closest('button');
expect(btn).toBeDisabled();
});
it('calls startPreflight when Run Preflight is clicked', () => {
const startPreflightMock = vi.fn();
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'candidates',
selectedNodeIds: new Set(['node-1']),
selectedSourceAgent: 'agent-1',
startPreflight: startPreflightMock,
}),
);
render(() => <AgentDeployModal {...defaultProps()} />);
fireEvent.click(screen.getByText('Run Preflight'));
expect(startPreflightMock).toHaveBeenCalledTimes(1);
});
});
/* ── Preflight step footer ─────────────────────────────────── */
describe('preflight step footer', () => {
it('shows "Preflight in progress..." message', () => {
useDeployWizardMock.mockReturnValue(createMockWizard({ step: 'preflight' }));
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByText('Preflight in progress...')).toBeInTheDocument();
});
});
/* ── Confirm step footer ───────────────────────────────────── */
describe('confirm step footer', () => {
it('shows Back button that returns to candidates step', () => {
const wizard = createMockWizard({ step: 'confirm' });
useDeployWizardMock.mockReturnValue(wizard);
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByText('Back')).toBeInTheDocument();
fireEvent.click(screen.getByText('Back'));
expect(wizard.step()).toBe('candidates');
});
it('shows Deploy button with confirmed node count', () => {
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'confirm',
confirmSelectedNodeIds: new Set(['n1', 'n2', 'n3']),
}),
);
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByText('Deploy 3 Hosts')).toBeInTheDocument();
});
it('uses singular "Node" when exactly 1 confirmed node', () => {
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'confirm',
confirmSelectedNodeIds: new Set(['n1']),
}),
);
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByText('Deploy 1 Host')).toBeInTheDocument();
});
it('disables Deploy button when no confirmed nodes', () => {
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'confirm',
confirmSelectedNodeIds: new Set<string>(),
}),
);
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByText('Deploy 0 Hosts')).toBeDisabled();
});
it('disables and shows Starting... when startingDeploy is true', () => {
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'confirm',
confirmSelectedNodeIds: new Set(['n1']),
startingDeploy: true,
}),
);
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByText('Starting...')).toBeInTheDocument();
const btn = screen.getByText('Starting...').closest('button');
expect(btn).toBeDisabled();
});
it('calls startDeploy when Deploy button is clicked', () => {
const startDeployMock = vi.fn();
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'confirm',
confirmSelectedNodeIds: new Set(['n1']),
startDeploy: startDeployMock,
}),
);
render(() => <AgentDeployModal {...defaultProps()} />);
fireEvent.click(screen.getByText('Deploy 1 Host'));
expect(startDeployMock).toHaveBeenCalledTimes(1);
});
});
/* ── Deploying step footer ─────────────────────────────────── */
describe('deploying step footer', () => {
it('shows Cancel Deployment button', () => {
useDeployWizardMock.mockReturnValue(createMockWizard({ step: 'deploying' }));
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByText('Cancel Deployment')).toBeInTheDocument();
});
it('calls cancelDeploy when Cancel Deployment is clicked', () => {
const cancelDeployMock = vi.fn();
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'deploying',
cancelDeploy: cancelDeployMock,
}),
);
render(() => <AgentDeployModal {...defaultProps()} />);
fireEvent.click(screen.getByText('Cancel Deployment'));
expect(cancelDeployMock).toHaveBeenCalledTimes(1);
});
it('shows Canceling... when canceling is true', () => {
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'deploying',
canceling: true,
}),
);
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByText('Canceling...')).toBeInTheDocument();
const btn = screen.getByText('Canceling...').closest('button');
expect(btn).toBeDisabled();
});
it('shows warning about closing during active operation', () => {
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'deploying',
isOperationActive: () => true,
}),
);
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByText("Closing won't cancel the deployment.")).toBeInTheDocument();
});
it('hides close-warning when operation is not active', () => {
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'deploying',
isOperationActive: () => false,
}),
);
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.queryByText("Closing won't cancel the deployment.")).not.toBeInTheDocument();
});
});
/* ── Results step footer ───────────────────────────────────── */
describe('results step footer', () => {
it('shows Close button', () => {
useDeployWizardMock.mockReturnValue(createMockWizard({ step: 'results' }));
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByText('Close')).toBeInTheDocument();
});
it('Close button calls onClose', () => {
useDeployWizardMock.mockReturnValue(createMockWizard({ step: 'results' }));
const props = defaultProps();
render(() => <AgentDeployModal {...props} />);
fireEvent.click(screen.getByText('Close'));
expect(props.onClose).toHaveBeenCalledTimes(1);
});
it('shows Retry button when retryable targets exist', () => {
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'results',
retryableTargets: [{ id: 't1' }, { id: 't2' }],
}),
);
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByText('Retry 2 Failed')).toBeInTheDocument();
});
it('hides Retry button when no retryable targets', () => {
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'results',
retryableTargets: [],
}),
);
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.queryByText(/Retry/)).not.toBeInTheDocument();
});
it('calls retryFailed when Retry button is clicked', () => {
const retryFailedMock = vi.fn();
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'results',
retryableTargets: [{ id: 't1' }],
retryFailed: retryFailedMock,
}),
);
render(() => <AgentDeployModal {...defaultProps()} />);
fireEvent.click(screen.getByText('Retry 1 Failed'));
expect(retryFailedMock).toHaveBeenCalledTimes(1);
});
it('disables and shows Retrying... when retrying is true', () => {
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'results',
retryableTargets: [{ id: 't1' }],
retrying: true,
}),
);
render(() => <AgentDeployModal {...defaultProps()} />);
expect(screen.getByText('Retrying...')).toBeInTheDocument();
const btn = screen.getByText('Retrying...').closest('button');
expect(btn).toBeDisabled();
});
});
/* ── Close behavior ────────────────────────────────────────── */
describe('close behavior', () => {
it('X button calls onClose', () => {
const props = defaultProps();
render(() => <AgentDeployModal {...props} />);
fireEvent.click(screen.getByLabelText('Close'));
expect(props.onClose).toHaveBeenCalledTimes(1);
});
it('X button still calls onClose when operation is active', () => {
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'deploying',
isOperationActive: () => true,
}),
);
const props = defaultProps();
render(() => <AgentDeployModal {...props} />);
fireEvent.click(screen.getByLabelText('Close'));
expect(props.onClose).toHaveBeenCalledTimes(1);
});
it('disables backdrop close when operation is active', () => {
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'deploying',
isOperationActive: () => true,
}),
);
const props = defaultProps();
render(() => <AgentDeployModal {...props} />);
// Click on backdrop — the Dialog component receives closeOnBackdrop=false.
// We verify this by clicking the backdrop and seeing that onClose is NOT called.
const backdrop = document.querySelector('[data-dialog-backdrop]');
expect(backdrop).not.toBeNull();
fireEvent.click(backdrop!);
expect(props.onClose).not.toHaveBeenCalled();
});
it('allows backdrop close when operation is not active', () => {
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'candidates',
isOperationActive: () => false,
}),
);
const props = defaultProps();
render(() => <AgentDeployModal {...props} />);
const backdrop = document.querySelector('[data-dialog-backdrop]');
expect(backdrop).not.toBeNull();
fireEvent.click(backdrop!);
expect(props.onClose).toHaveBeenCalledTimes(1);
});
});
/* ── Wizard initialization ─────────────────────────────────── */
describe('wizard initialization', () => {
it('passes clusterId and clusterName to useDeployWizard', () => {
render(() => <AgentDeployModal {...defaultProps()} />);
expect(useDeployWizardMock).toHaveBeenCalledWith({
clusterId: 'cluster-1',
clusterName: 'My Cluster',
});
});
});
/* ── Keyboard close (Escape) ───────────────────────────────── */
describe('keyboard close', () => {
it('calls onClose when Escape key is pressed', () => {
const props = defaultProps();
render(() => <AgentDeployModal {...props} />);
fireEvent.keyDown(document, { key: 'Escape' });
expect(props.onClose).toHaveBeenCalledTimes(1);
});
it('calls onClose on Escape even when operation is active', () => {
useDeployWizardMock.mockReturnValue(
createMockWizard({
step: 'deploying',
isOperationActive: () => true,
}),
);
const props = defaultProps();
render(() => <AgentDeployModal {...props} />);
fireEvent.keyDown(document, { key: 'Escape' });
expect(props.onClose).toHaveBeenCalledTimes(1);
});
});
/* ── ConfirmStep prop wiring ───────────────────────────────── */
describe('ConfirmStep prop wiring', () => {
it('passes wizard to ConfirmStep', () => {
const wizard = createMockWizard({ step: 'confirm' });
useDeployWizardMock.mockReturnValue(wizard);
render(() => <AgentDeployModal {...defaultProps()} />);
expect(confirmStepPropsSpy).toHaveBeenCalled();
const receivedProps = confirmStepPropsSpy.mock.calls[0][0];
expect(receivedProps.wizard).toBe(wizard);
});
});
});
@@ -1,44 +0,0 @@
import { Show, type Component } from 'solid-js';
import XIcon from 'lucide-solid/icons/x';
interface MigrationNoticeBannerProps {
title: string;
message: string;
learnMoreHref?: string;
onDismiss: () => void;
}
export const MigrationNoticeBanner: Component<MigrationNoticeBannerProps> = (props) => {
return (
<div class="rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-blue-900 shadow-sm dark:border-blue-800 dark:bg-blue-950 dark:text-blue-100">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<div class="text-xs font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300">
Navigation update
</div>
<div class="mt-0.5 text-sm font-medium">{props.title}</div>
<div class="mt-0.5 text-xs text-blue-800 dark:text-blue-200">{props.message}</div>
<Show when={props.learnMoreHref}>
<a
href={props.learnMoreHref}
class="mt-1 inline-flex text-xs font-medium text-blue-700 underline underline-offset-2 hover:text-blue-900 dark:text-blue-300 dark:hover:text-blue-100"
>
See full migration guide
</a>
</Show>
</div>
<button
type="button"
onClick={props.onDismiss}
class="inline-flex h-6 w-6 items-center justify-center rounded-md text-blue-700 hover:bg-blue-100 hover:text-blue-900 dark:text-blue-300 dark:hover:bg-blue-900 dark:hover:text-blue-100"
title="Dismiss"
aria-label="Dismiss navigation notice"
>
<XIcon class="h-3.5 w-3.5" />
</button>
</div>
</div>
);
};
export default MigrationNoticeBanner;
@@ -1,76 +0,0 @@
import { For, Show, type JSX } from 'solid-js';
import {
SUMMARY_TIME_RANGES,
SUMMARY_TIME_RANGE_LABEL,
type SummaryTimeRange,
} from './summaryTimeRange';
export interface SummaryPanelProps {
/** Left-side header content (resource counts, badges). */
headerLeft: JSX.Element;
/** Currently selected time range (highlights the active button). */
timeRange?: string;
/** Called when the user clicks a time range button. Omit to hide the buttons. */
onTimeRangeChange?: (range: SummaryTimeRange) => void;
/** Custom time range options. Defaults to SUMMARY_TIME_RANGES. */
timeRanges?: readonly string[];
/** Custom labels for time range buttons. Defaults to SUMMARY_TIME_RANGE_LABEL. */
timeRangeLabels?: Record<string, string>;
/** Card grid content. */
children: JSX.Element;
/** data-testid for the outer wrapper. */
testId?: string;
/** Extra CSS classes on the outer container. */
class?: string;
/** Optional shared density override for compact monitoring surfaces. */
density?: 'default' | 'compact';
}
export function SummaryPanel(props: SummaryPanelProps) {
const ranges = () => (props.timeRanges ?? SUMMARY_TIME_RANGES) as readonly string[];
const labels = () =>
props.timeRangeLabels ?? (SUMMARY_TIME_RANGE_LABEL as Record<string, string>);
const isCompact = () => props.density === 'compact';
return (
<div
data-testid={props.testId}
class={`rounded-md border border-border bg-surface shadow-sm ${
isCompact() ? 'p-1.5 sm:p-2.5' : 'p-2 sm:p-3'
} ${props.class ?? ''}`}
>
<div
class={`flex flex-wrap items-center justify-between border-b border-border-subtle text-[11px] ${
isCompact()
? 'mb-1.5 gap-1.5 px-0.5 pb-1.5'
: 'mb-2 gap-2 px-1 pb-2'
}`}
>
<div class="flex items-center gap-3">{props.headerLeft}</div>
<Show when={props.onTimeRangeChange}>
<div class="inline-flex shrink-0 rounded border border-border bg-surface p-0.5 text-xs">
<For each={[...ranges()]}>
{(range) => (
<button
type="button"
onClick={() => props.onTimeRangeChange?.(range as SummaryTimeRange)}
class={`rounded px-2 py-1 ${
props.timeRange === range
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200'
: 'text-muted hover:bg-surface-hover'
}`}
>
{labels()[range] ?? range}
</button>
)}
</For>
</div>
</Show>
</div>
<div class={`grid grid-cols-2 lg:grid-cols-4 ${isCompact() ? 'gap-2' : 'gap-2 sm:gap-3'}`}>
{props.children}
</div>
</div>
);
}
-10
View File
@@ -167,20 +167,10 @@ export const STORAGE_KEYS = {
ALERT_HISTORY_TIME_FILTER: 'alertHistoryTimeFilter',
ALERT_HISTORY_SEVERITY_FILTER: 'alertHistorySeverityFilter',
// Storage settings
STORAGE_SHOW_FILTERS: 'storageShowFilters',
STORAGE_VIEW_MODE: 'storageViewMode',
STORAGE_SOURCE_FILTER: 'storageSourceFilter',
// Recovery settings
RECOVERY_SHOW_FILTERS: 'backupsShowFilters',
RECOVERY_USE_RELATIVE_TIME: 'backupsUseRelativeTime',
RECOVERY_SEARCH_HISTORY: 'backupsSearchHistory',
// Workloads settings
WORKLOADS_SHOW_FILTERS: 'workloadsShowFilters',
WORKLOADS_CARD_VIEW: 'workloadsCardView',
WORKLOADS_AUTO_REFRESH: 'workloadsAutoRefresh',
WORKLOADS_SEARCH_HISTORY: 'workloadsSearchHistory',
WORKLOADS_METRIC_DISPLAY_MODE: 'workloadsMetricDisplayMode',
WORKLOADS_METRIC_HISTORY_RANGE: 'workloadsMetricHistoryRange',
@@ -1,59 +0,0 @@
import { ChartsAPI, type StorageSummaryTrendResponse, type TimeRange } from '@/api/charts';
import { eventBus } from '@/stores/events';
import { getOrgID } from '@/utils/apiClient';
import { normalizeOrgScope } from '@/utils/orgScope';
const STORAGE_SUMMARY_TREND_CACHE_VERSION = 1;
function cacheKeyFor(
range: TimeRange,
orgScope: string = normalizeOrgScope(getOrgID()),
): string {
return `${STORAGE_SUMMARY_TREND_CACHE_VERSION}::${orgScope}::${range}`;
}
const inMemoryCache = new Map<string, StorageSummaryTrendResponse>();
const inFlightFetches = new Map<string, Promise<StorageSummaryTrendResponse>>();
export function readStorageSummaryTrendCache(range: TimeRange): StorageSummaryTrendResponse | null {
return inMemoryCache.get(cacheKeyFor(range)) ?? null;
}
export function fetchStorageSummaryTrendAndCache(
range: TimeRange,
_options?: { caller?: string },
): Promise<StorageSummaryTrendResponse> {
const inFlightKey = cacheKeyFor(range);
const existing = inFlightFetches.get(inFlightKey);
if (existing) {
return existing;
}
const request = ChartsAPI.getStorageSummaryTrend(range)
.then((response) => {
inMemoryCache.set(inFlightKey, response);
return response;
})
.finally(() => {
inFlightFetches.delete(inFlightKey);
});
inFlightFetches.set(inFlightKey, request);
return request;
}
export function __resetStorageSummaryTrendCacheForTests(): void {
inMemoryCache.clear();
inFlightFetches.clear();
}
const unsubscribeStorageOrgSwitch = eventBus.on('org_switched', () => {
inMemoryCache.clear();
inFlightFetches.clear();
});
if (import.meta.hot) {
import.meta.hot.dispose(() => {
unsubscribeStorageOrgSwitch();
});
}