refactor(decnet_web/DeckyFleet): move IntervalEditor out

Verbatim move of the per-decky mutation-interval modal (~60 LOC)
into its own file. Saves null when the toggle is off, minutes
otherwise.

- New DeckyFleet/IntervalEditor.tsx
- IntervalEditor.test.tsx covers null-current disabled path,
  numeric-current enabled path, and CANCEL not firing onSave.
- src/test/fixtures/decky.ts now derives DeckyFixture from the
  canonical Decky type (the fixture's loose swarm shape was
  missing host_address/host_status; aligning to Decky catches
  that statically).
This commit is contained in:
2026-05-09 04:55:30 -04:00
parent 032ffbb4eb
commit b6ff288dcf
4 changed files with 137 additions and 79 deletions

View File

@@ -1004,67 +1004,7 @@ const DeployWizard: React.FC<DeployWizardProps> = ({
);
};
// ─── Interval editor modal ───────────────────────────────────────────────
interface IntervalEditorProps {
open: boolean;
deckyName: string;
current: number | null;
onClose: () => void;
onSave: (minutes: number | null) => void;
}
const IntervalEditor: React.FC<IntervalEditorProps> = ({ open, deckyName, current, onClose, onSave }) => {
const [enabled, setEnabled] = useState<boolean>(current !== null);
const [minutes, setMinutes] = useState<number>(current ?? 30);
return (
<Modal
open={open}
onClose={onClose}
title={`MUTATION INTERVAL · ${deckyName}`}
icon={RefreshCw}
accent="violet"
footer={
<>
<button className="btn ghost" onClick={onClose}>CANCEL</button>
<button className="btn violet" onClick={() => onSave(enabled ? minutes : null)}>SAVE</button>
</>
}
>
<div className="modal-body">
<div style={{ display: 'flex', gap: 10, alignItems: 'center', padding: 14, border: '1px solid var(--border)' }}>
<input
id="interval-enable"
type="checkbox"
checked={enabled}
onChange={(e) => setEnabled(e.target.checked)}
style={{ accentColor: 'var(--matrix)' }}
/>
<label htmlFor="interval-enable" style={{ fontSize: '0.8rem', letterSpacing: 1 }}>
ENABLE PERIODIC MUTATION
</label>
</div>
{enabled && (
<div className="tweak-group">
<label>INTERVAL ({minutes} minutes)</label>
<input
type="range"
min={5}
max={240}
step={5}
value={minutes}
onChange={(e) => setMinutes(parseInt(e.target.value, 10))}
/>
<div className="dim" style={{ fontSize: '0.65rem', letterSpacing: 1 }}>
Applied on the next mutation cycle.
</div>
</div>
)}
</div>
</Modal>
);
};
import { IntervalEditor } from './DeckyFleet/IntervalEditor';
// ─── Fleet page ──────────────────────────────────────────────────────────

View File

@@ -0,0 +1,62 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntervalEditor } from './IntervalEditor';
describe('IntervalEditor', () => {
it('starts disabled when current is null and saves null', async () => {
const onSave = vi.fn();
const user = userEvent.setup();
render(
<IntervalEditor
open
deckyName="decoy-01"
current={null}
onClose={() => {}}
onSave={onSave}
/>,
);
const checkbox = screen.getByLabelText('ENABLE PERIODIC MUTATION') as HTMLInputElement;
expect(checkbox.checked).toBe(false);
expect(screen.queryByText(/INTERVAL \(/)).not.toBeInTheDocument();
await user.click(screen.getByText('SAVE'));
expect(onSave).toHaveBeenCalledWith(null);
});
it('starts enabled when current is a number and saves the slider value', async () => {
const onSave = vi.fn();
const user = userEvent.setup();
render(
<IntervalEditor
open
deckyName="decoy-02"
current={45}
onClose={() => {}}
onSave={onSave}
/>,
);
expect(screen.getByText(/INTERVAL \(45 minutes\)/)).toBeInTheDocument();
await user.click(screen.getByText('SAVE'));
expect(onSave).toHaveBeenCalledWith(45);
});
it('CANCEL invokes onClose without onSave', async () => {
const onClose = vi.fn();
const onSave = vi.fn();
const user = userEvent.setup();
render(
<IntervalEditor
open
deckyName="decoy-03"
current={30}
onClose={onClose}
onSave={onSave}
/>,
);
await user.click(screen.getByText('CANCEL'));
expect(onClose).toHaveBeenCalledTimes(1);
expect(onSave).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,71 @@
import React, { useState } from 'react';
import { RefreshCw } from '../../icons';
import Modal from '../Modal/Modal';
interface Props {
open: boolean;
deckyName: string;
current: number | null;
onClose: () => void;
onSave: (minutes: number | null) => void;
}
/** Modal that toggles + slider-edits per-decky mutation intervals.
* Saves null when disabled, minutes otherwise. */
export const IntervalEditor: React.FC<Props> = ({
open,
deckyName,
current,
onClose,
onSave,
}) => {
const [enabled, setEnabled] = useState<boolean>(current !== null);
const [minutes, setMinutes] = useState<number>(current ?? 30);
return (
<Modal
open={open}
onClose={onClose}
title={`MUTATION INTERVAL · ${deckyName}`}
icon={RefreshCw}
accent="violet"
footer={
<>
<button className="btn ghost" onClick={onClose}>CANCEL</button>
<button className="btn violet" onClick={() => onSave(enabled ? minutes : null)}>SAVE</button>
</>
}
>
<div className="modal-body">
<div style={{ display: 'flex', gap: 10, alignItems: 'center', padding: 14, border: '1px solid var(--border)' }}>
<input
id="interval-enable"
type="checkbox"
checked={enabled}
onChange={(e) => setEnabled(e.target.checked)}
style={{ accentColor: 'var(--matrix)' }}
/>
<label htmlFor="interval-enable" style={{ fontSize: '0.8rem', letterSpacing: 1 }}>
ENABLE PERIODIC MUTATION
</label>
</div>
{enabled && (
<div className="tweak-group">
<label>INTERVAL ({minutes} minutes)</label>
<input
type="range"
min={5}
max={240}
step={5}
value={minutes}
onChange={(e) => setMinutes(parseInt(e.target.value, 10))}
/>
<div className="dim" style={{ fontSize: '0.65rem', letterSpacing: 1 }}>
Applied on the next mutation cycle.
</div>
</div>
)}
</div>
</Modal>
);
};

View File

@@ -1,21 +1,6 @@
export interface DeckyFixture {
name: string;
ip: string;
services: string[];
distro: string;
hostname: string;
archetype: string | null;
service_config: Record<string, Record<string, unknown>>;
mutate_interval: number | null;
last_mutated: number;
swarm?: {
host_uuid: string;
host_name: string;
state: string;
last_error: string | null;
last_seen: string | null;
};
}
import type { Decky } from '../../components/DeckyFleet/types';
export type DeckyFixture = Decky;
export const makeDecky = (overrides: Partial<DeckyFixture> = {}): DeckyFixture => ({
name: 'decoy-01',