From b6ff288dcf15114cf63f2977c922ebb3e835c915 Mon Sep 17 00:00:00 2001 From: anti Date: Sat, 9 May 2026 04:55:30 -0400 Subject: [PATCH] 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). --- decnet_web/src/components/DeckyFleet.tsx | 62 +--------------- .../DeckyFleet/IntervalEditor.test.tsx | 62 ++++++++++++++++ .../components/DeckyFleet/IntervalEditor.tsx | 71 +++++++++++++++++++ decnet_web/src/test/fixtures/decky.ts | 21 +----- 4 files changed, 137 insertions(+), 79 deletions(-) create mode 100644 decnet_web/src/components/DeckyFleet/IntervalEditor.test.tsx create mode 100644 decnet_web/src/components/DeckyFleet/IntervalEditor.tsx diff --git a/decnet_web/src/components/DeckyFleet.tsx b/decnet_web/src/components/DeckyFleet.tsx index 74c6adf2..46265e07 100644 --- a/decnet_web/src/components/DeckyFleet.tsx +++ b/decnet_web/src/components/DeckyFleet.tsx @@ -1004,67 +1004,7 @@ const DeployWizard: React.FC = ({ ); }; -// ─── Interval editor modal ─────────────────────────────────────────────── - -interface IntervalEditorProps { - open: boolean; - deckyName: string; - current: number | null; - onClose: () => void; - onSave: (minutes: number | null) => void; -} - -const IntervalEditor: React.FC = ({ open, deckyName, current, onClose, onSave }) => { - const [enabled, setEnabled] = useState(current !== null); - const [minutes, setMinutes] = useState(current ?? 30); - - return ( - - - - - } - > -
-
- setEnabled(e.target.checked)} - style={{ accentColor: 'var(--matrix)' }} - /> - -
- {enabled && ( -
- - setMinutes(parseInt(e.target.value, 10))} - /> -
- Applied on the next mutation cycle. -
-
- )} -
-
- ); -}; +import { IntervalEditor } from './DeckyFleet/IntervalEditor'; // ─── Fleet page ────────────────────────────────────────────────────────── diff --git a/decnet_web/src/components/DeckyFleet/IntervalEditor.test.tsx b/decnet_web/src/components/DeckyFleet/IntervalEditor.test.tsx new file mode 100644 index 00000000..f3cd0326 --- /dev/null +++ b/decnet_web/src/components/DeckyFleet/IntervalEditor.test.tsx @@ -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( + {}} + 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( + {}} + 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( + , + ); + await user.click(screen.getByText('CANCEL')); + expect(onClose).toHaveBeenCalledTimes(1); + expect(onSave).not.toHaveBeenCalled(); + }); +}); diff --git a/decnet_web/src/components/DeckyFleet/IntervalEditor.tsx b/decnet_web/src/components/DeckyFleet/IntervalEditor.tsx new file mode 100644 index 00000000..674ecc54 --- /dev/null +++ b/decnet_web/src/components/DeckyFleet/IntervalEditor.tsx @@ -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 = ({ + open, + deckyName, + current, + onClose, + onSave, +}) => { + const [enabled, setEnabled] = useState(current !== null); + const [minutes, setMinutes] = useState(current ?? 30); + + return ( + + + + + } + > +
+
+ setEnabled(e.target.checked)} + style={{ accentColor: 'var(--matrix)' }} + /> + +
+ {enabled && ( +
+ + setMinutes(parseInt(e.target.value, 10))} + /> +
+ Applied on the next mutation cycle. +
+
+ )} +
+
+ ); +}; diff --git a/decnet_web/src/test/fixtures/decky.ts b/decnet_web/src/test/fixtures/decky.ts index 6fbbc1cf..aec43fbe 100644 --- a/decnet_web/src/test/fixtures/decky.ts +++ b/decnet_web/src/test/fixtures/decky.ts @@ -1,21 +1,6 @@ -export interface DeckyFixture { - name: string; - ip: string; - services: string[]; - distro: string; - hostname: string; - archetype: string | null; - service_config: Record>; - 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 => ({ name: 'decoy-01',