mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 12:42:49 +00:00
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
-1027
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user