From b2e1acd2579a8affb3e8af5d2c1deb60fab594c7 Mon Sep 17 00:00:00 2001 From: Developer Date: Fri, 26 Jun 2026 19:27:16 +0000 Subject: [PATCH] Frontend: ssh_tasks Files + Actions tabs (Slice 6) Replace the FilesTab and ActionsTab stubs with real implementations on the ssh_tasks service page. FilesTab (pages/service-tabs/FilesTab.tsx): lifts the operational content from the top-level FileBrowser page into an instance-scoped tab. Directory listing, path bar, ffprobe preview, and job execution all use instance.id as the machine id (replacing the old machine-tab selector + machine_id search param). The initial path is read from ?path= search param so deep links work (resolves the MediaTab row-click navigation). ActionsTab (pages/service-tabs/ActionsTab.tsx): lifts the saved-tasks CRUD + run + history content from the top-level Actions page. instance.id is the fixed default run service -- the old service-selector dropdown is removed (the instance is implicit; switch instances via the service page switcher to run on a different one). MediaTab row-click cross-slice fix: navigates to /services/ssh_tasks/?path= when an enabled ssh_tasks instance exists, else falls back to /services/ssh_tasks (which shows the ServiceTypePage resolver/empty state). Resolves the 404 flag from slice 5. service-tabs/index.ts wires the new components; FilesTab/ActionsTab stubs removed. Note: this branch is based on main, not on mobile-responsive-parity, so the tabs lift main's DataTable + column-visibility pattern (no MobileCardRow -- reconciles when the branches merge). Tests: FilesTab (instance-scoped hooks + ?path= deep-link), ActionsTab (instance-scoped task list), MediaTab test mock updated. 94 tests pass (+4); lint/build green. Refs openspec/changes/services-as-hub-ia/ (spec R2.4, tasks slice 6). --- .../src/pages/service-tabs/ActionsTab.tsx | 456 ++++++++++ frontend/src/pages/service-tabs/FilesTab.tsx | 790 ++++++++++++++++++ frontend/src/pages/service-tabs/MediaTab.tsx | 11 +- .../__tests__/ActionsTab.test.tsx | 59 ++ .../service-tabs/__tests__/FilesTab.test.tsx | 63 ++ .../service-tabs/__tests__/MediaTab.test.tsx | 4 + frontend/src/pages/service-tabs/index.ts | 4 +- frontend/src/pages/service-tabs/stubs.tsx | 8 - 8 files changed, 1384 insertions(+), 11 deletions(-) create mode 100644 frontend/src/pages/service-tabs/ActionsTab.tsx create mode 100644 frontend/src/pages/service-tabs/FilesTab.tsx create mode 100644 frontend/src/pages/service-tabs/__tests__/ActionsTab.test.tsx create mode 100644 frontend/src/pages/service-tabs/__tests__/FilesTab.test.tsx diff --git a/frontend/src/pages/service-tabs/ActionsTab.tsx b/frontend/src/pages/service-tabs/ActionsTab.tsx new file mode 100644 index 0000000..a9e7cf4 --- /dev/null +++ b/frontend/src/pages/service-tabs/ActionsTab.tsx @@ -0,0 +1,456 @@ +/** + * ActionsTab — operational content for the ssh_tasks service page. + * + * Lifted from the old top-level `pages/Actions.tsx`. The `instance` prop + * provides the active ssh_tasks service id, which is used as the default run + * service. The page-level header is removed (the service page provides it). + * The task editor dialog, saved-task rail, and run history are preserved. + */ +import type { ReactNode } from "react"; +import { useMemo, useState } from "react"; +import type { SavedTask, SavedTaskInput, ServiceInstance } from "../../types"; +import { + useDeleteTask, + useRunTask, + useSaveTask, + useTaskRuns, + useTasks, +} 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"; + +type ActionTab = "new" | string; + +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, + name: "", + task_type: "shell", + content: "", + enabled: true, + default_service_id: "", + notes: "", + }; +} + +function sameTask(a: SavedTaskInput, b: SavedTaskInput) { + return ( + a.id === b.id && + a.name === b.name && + a.task_type === b.task_type && + a.content === b.content && + a.enabled === b.enabled && + a.default_service_id === b.default_service_id && + a.notes === b.notes + ); +} + +function initialFromTask(task: SavedTask): SavedTaskInput { + return { + id: task.id, + name: task.name, + task_type: task.task_type, + content: task.content, + enabled: task.enabled, + default_service_id: task.default_service_id, + notes: task.notes, + }; +} + +function TaskEditor({ + task, + onChange, +}: { + task: SavedTaskInput; + onChange: (task: SavedTaskInput) => void; +}) { + return ( +
+
+

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

+ {task.task_type} + {task.enabled ? "enabled" : "disabled"} +
+
+ + onChange({ ...task, name: e.target.value })} + /> + +
+
+ + + +
+
+ + onChange({ ...task, notes: e.target.value })} + /> + + +