From b6da7df7f94a6fa9b4f9a63daeb16ef741a37ee2 Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 17 Jun 2026 13:47:57 +0000 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20slice=205=20=E2=80=94=20migra?= =?UTF-8?q?te=20Settings=20+=20Actions=20to=20shadcn/Tailwind?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Web UI rework. Form-heavy pair (controlled useState parity, no form lib): - pages/Settings.tsx off @mui: monitoring-machine CRUD, SSH-key mgmt, SSH test/validation feedback, danger-zone reset (ConfirmDialog), tabs - pages/Actions.tsx off @mui: saved-task editor, machine selection, run history, tabs - Both reuse migrated shared components (SectionCard/SelectionRailCard/ TabbedCard/HoverEditButton/ConfirmDialog/DialogFooter) as before - Behavioral tests added (mocked hooks; no live SSH) Gate: build + lint + test green (19 files / 39 tests). --- frontend/src/pages/Actions.tsx | 771 ++++---- frontend/src/pages/Settings.tsx | 1670 ++++++++--------- frontend/src/pages/__tests__/Actions.test.tsx | 118 ++ .../src/pages/__tests__/Settings.test.tsx | 122 ++ .../changes/web-ui-rework/apply-progress.md | 148 ++ openspec/changes/web-ui-rework/tasks.md | 10 +- 6 files changed, 1490 insertions(+), 1349 deletions(-) create mode 100644 frontend/src/pages/__tests__/Actions.test.tsx create mode 100644 frontend/src/pages/__tests__/Settings.test.tsx diff --git a/frontend/src/pages/Actions.tsx b/frontend/src/pages/Actions.tsx index 48025c3..b44a473 100644 --- a/frontend/src/pages/Actions.tsx +++ b/frontend/src/pages/Actions.tsx @@ -1,26 +1,6 @@ +import type { ReactNode } from "react"; import { useMemo, useState } from "react"; -import { - Alert, - Box, - Button, - Card, - CardContent, - Chip, - Dialog, - DialogContent, - DialogTitle, - Divider, - FormControl, - InputLabel, - MenuItem, - Select, - Stack, - Tab, - Tabs, - TextField, - Typography, -} from "@mui/material"; -import type { MonitoringMachine, SavedTaskInput } from "../types"; +import type { MonitoringMachine, SavedTask, SavedTaskInput } from "../types"; import { useDeleteTask, useMonitoringSettings, @@ -31,10 +11,63 @@ import { } from "../hooks/useSettings"; import { DialogFooter } from "../components/DialogFooter"; import { HoverEditButton } from "../components/HoverEditButton"; +import { SectionCard } from "../components/SectionCard"; import { SelectionRailCard } from "../components/SelectionRailCard"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Textarea } from "@/components/ui/textarea"; + +// Radix Select disallows empty-string item values; the "None" option maps to +// this sentinel and converts back to "" at the draft boundary. +const NONE = "__none__"; type ActionTab = "new" | string; +/** Small labeled-field wrapper replacing the MUI `` shell. */ +function FormField({ + label, + htmlFor, + helperText, + children, +}: { + label: string; + htmlFor?: string; + helperText?: string; + children: ReactNode; +}) { + return ( +
+ + {children} + {helperText ? ( +

{helperText}

+ ) : null} +
+ ); +} + function emptyTask(): SavedTaskInput { return { id: null, @@ -59,6 +92,18 @@ function sameTask(a: SavedTaskInput, b: SavedTaskInput) { ); } +function initialFromTask(task: SavedTask): SavedTaskInput { + return { + id: task.id, + name: task.name, + task_type: task.task_type, + content: task.content, + enabled: task.enabled, + default_machine_id: task.default_machine_id, + notes: task.notes, + }; +} + function TaskEditor({ task, machines, @@ -72,101 +117,98 @@ function TaskEditor({ (machine) => machine.id === task.default_machine_id, ); return ( - - - +
+
+

{task.id ? "Edit action" : "New action"} - - - +

+ {task.task_type} + {task.enabled ? "enabled" : "disabled"} {selectedMachine && ( - + {`default: ${selectedMachine.name}`} )} - +
- - onChange({ ...task, name: e.target.value })} - /> - - - Type - - - - Default machine - - - - onChange({ ...task, notes: e.target.value })} - /> - + + onChange({ ...task, name: e.target.value })} + /> + +
+
+ + + +
+
+ + + +
+
+ + onChange({ ...task, notes: e.target.value })} + /> + + onChange({ ...task, content: e.target.value })} helperText={ task.task_type === "python" ? "Python is run as `python3 -c`." : "Shell commands are run through `/bin/sh -c`." } - /> -
- + > +