From 70511d97f92c746d0eb99df2a3d7da1d2e71c3bf Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 14 Jul 2026 15:47:54 +0000 Subject: [PATCH] refactor: move service administration into settings --- docs/REQUIREMENTS.md | 8 ++-- frontend/src/App.tsx | 8 ++-- frontend/src/pages/ServicesPage.tsx | 9 ++-- frontend/src/pages/Settings.tsx | 47 +++++++++++++++---- .../__tests__/Settings.services.test.tsx | 22 +++++++++ 5 files changed, 73 insertions(+), 21 deletions(-) diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index a2c927b..e64df07 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -285,9 +285,11 @@ values missing an `http://` or `https://` schema with a clear validation error passphrase; provides a task-output widget. Tasks stay in the global saved-task registry; every run is recorded in `service_task_runs` as history. -Multiple instances per service type are supported. Services are managed from the -**Services** page (`/services`) and each instance has a detail page at -`/services/:serviceType/:serviceId`. +Multiple instances per service type are supported. Services are managed from +**Settings → Services**, which provides a **List** subtab for creating and editing +instances and a **Dashboards** subtab for named dashboard management. Each +instance retains its operational detail page at `/services/:serviceType/:serviceId`; +legacy `/services` navigation redirects to Settings. ### Built-in widgets diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b030a99..ab9d6d2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,6 +5,7 @@ import { NavLink, useLocation, Outlet, + Navigate, } from "react-router-dom"; import { QueryClient, @@ -19,7 +20,6 @@ import { NamedDashboardPage } from "./pages/NamedDashboardPage"; import { Settings } from "./pages/Settings"; import { ServicePage } from "./pages/ServicePage"; import { ServiceTypePage } from "./pages/ServiceTypePage"; -import { ServicesPage } from "./pages/ServicesPage"; import { getOidcConfig, isOidcConfigured, setAccessToken } from "./auth"; import { fetchAppVersion } from "./api/client"; import { FRONTEND_VERSION_LABEL } from "./version"; @@ -51,7 +51,6 @@ import { LogOut, ChevronLeft, ChevronRight, - Boxes, LayoutTemplate, } from "lucide-react"; @@ -112,7 +111,6 @@ function useNavItems() { { path: "/", label: "Dashboard", icon: LayoutDashboard }, ...dashboardEntries, ...serviceEntries, - { path: "/services", label: "Services", icon: Boxes }, { path: "/settings", label: "Settings", icon: SettingsIcon }, ]; }, [services, dashboards]); @@ -469,7 +467,7 @@ function AppInner() { } /> } /> } /> - } /> + } /> } @@ -497,7 +495,7 @@ function AppInner() { } /> } /> } /> - } /> + } /> } diff --git a/frontend/src/pages/ServicesPage.tsx b/frontend/src/pages/ServicesPage.tsx index c55d410..a5227d3 100644 --- a/frontend/src/pages/ServicesPage.tsx +++ b/frontend/src/pages/ServicesPage.tsx @@ -183,7 +183,7 @@ function ServiceSecretFields({ ); } -function CreateServiceDialog({ +export function CreateServiceDialog({ open, onClose, }: { @@ -339,7 +339,10 @@ function CreateServiceDialog({ ) : null} {draft ? ( { + reset(); + onClose(); + }} onConfirm={save} confirmLabel="Create service" confirmDisabled={ @@ -354,7 +357,7 @@ function CreateServiceDialog({ // --- Named dashboards management (Slice 10.3) --- -function DashboardManagementCard() { +export function DashboardManagementCard() { const { data: dashboards = [] } = useDashboards(); const saveDashboard = useSaveDashboard(); const deleteDashboard = useDeleteDashboard(); diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 5faedf1..ed37542 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -19,6 +19,7 @@ import { useTestMonitoringMachineSSH, } from "../hooks/useSettings"; import { useIsMobile } from "../hooks/useIsMobile"; +import { CreateServiceDialog, DashboardManagementCard } from "./ServicesPage"; import { SheetForm } from "@/components/ui/sheet-form"; import { DialogFooter } from "../components/DialogFooter"; import { HoverEditButton } from "../components/HoverEditButton"; @@ -1353,6 +1354,8 @@ function ServicesAdminCard({ const { data: services = [] } = useServiceInstances(); const { data: types = [] } = useServiceTypes(); const [selectedServiceId, setSelectedServiceId] = useState(initialServiceId); + const [serviceSubtab, setServiceSubtab] = useState<"list" | "dashboards">("list"); + const [createOpen, setCreateOpen] = useState(false); const sortedServices = useMemo( () => @@ -1376,16 +1379,30 @@ function ServicesAdminCard({ ? types.find((t) => t.service_type === selectedService.service_type) : undefined; - if (sortedServices.length === 0) { - return ( -

- No service instances configured. Create one from the Services page. -

- ); - } - return ( -
+ <> + setServiceSubtab(value as "list" | "dashboards")} + tabs={[ + List, + Dashboards, + ]} + contentSx={{}} + > + {serviceSubtab === "list" && ( +
+
+ +
+ {sortedServices.length === 0 ? ( +

+ No service instances configured yet. +

+ ) : ( +
)} -
+
+ )} +
+ )} + {serviceSubtab === "dashboards" && } + + setCreateOpen(false)} + /> + ); } diff --git a/frontend/src/pages/__tests__/Settings.services.test.tsx b/frontend/src/pages/__tests__/Settings.services.test.tsx index ffa3676..93e0664 100644 --- a/frontend/src/pages/__tests__/Settings.services.test.tsx +++ b/frontend/src/pages/__tests__/Settings.services.test.tsx @@ -35,6 +35,12 @@ vi.mock("../../hooks/useSettings", () => ({ useDeleteSSHKey: () => ({ mutate: vi.fn() }), })); +vi.mock("../../hooks/useDashboards", () => ({ + useDashboards: () => ({ data: [] }), + useSaveDashboard: () => ({ mutate: vi.fn(), isPending: false }), + useDeleteDashboard: () => ({ mutate: vi.fn(), isPending: false }), +})); + vi.mock("../../hooks/useServices", () => ({ useServiceTypes: () => ({ data: [ @@ -140,4 +146,20 @@ describe("Settings > Services editor", () => { timeout_seconds: 60, }); }); + + it("offers service creation and dashboard management as Services subtabs", async () => { + render( + + + , + ); + + await userEvent.click(screen.getByRole("button", { name: "Add service" })); + expect(screen.getByText("New service")).toBeInTheDocument(); + await userEvent.keyboard("{Escape}"); + + await userEvent.click(screen.getByRole("tab", { name: "Dashboards" })); + expect(screen.getByRole("heading", { name: "Dashboards" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "New dashboard" })).toBeInTheDocument(); + }); });