refactor: move service administration into settings

This commit is contained in:
Developer
2026-07-14 15:47:54 +00:00
parent a9488af0b4
commit 70511d97f9
5 changed files with 73 additions and 21 deletions
+3 -5
View File
@@ -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() {
<Route path="/" element={<Dashboard />} />
<Route path="/d/:slug" element={<NamedDashboardPage />} />
<Route path="/settings" element={<Settings />} />
<Route path="/services" element={<ServicesPage />} />
<Route path="/services" element={<Navigate to="/settings?tab=services" replace />} />
<Route
path="/services/:serviceType"
element={<ServiceTypePage />}
@@ -497,7 +495,7 @@ function AppInner() {
<Route path="/" element={<Dashboard />} />
<Route path="/d/:slug" element={<NamedDashboardPage />} />
<Route path="/settings" element={<Settings />} />
<Route path="/services" element={<ServicesPage />} />
<Route path="/services" element={<Navigate to="/settings?tab=services" replace />} />
<Route
path="/services/:serviceType"
element={<ServiceTypePage />}
+6 -3
View File
@@ -183,7 +183,7 @@ function ServiceSecretFields({
);
}
function CreateServiceDialog({
export function CreateServiceDialog({
open,
onClose,
}: {
@@ -339,7 +339,10 @@ function CreateServiceDialog({
) : null}
{draft ? (
<DialogFooter
onCancel={reset}
onCancel={() => {
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();
+37 -10
View File
@@ -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 (
<p className="text-sm text-muted-foreground">
No service instances configured. Create one from the Services page.
</p>
);
}
return (
<div className="grid grid-cols-1 gap-4 md:grid-cols-[320px_minmax(0,1fr)]">
<>
<TabbedCard
value={serviceSubtab}
onChange={(value) => setServiceSubtab(value as "list" | "dashboards")}
tabs={[
<TabsTrigger key="list" value="list">List</TabsTrigger>,
<TabsTrigger key="dashboards" value="dashboards">Dashboards</TabsTrigger>,
]}
contentSx={{}}
>
{serviceSubtab === "list" && (
<div className="flex flex-col gap-4">
<div className="flex justify-end">
<Button variant="outline" onClick={() => setCreateOpen(true)}>
Add service
</Button>
</div>
{sortedServices.length === 0 ? (
<p className="text-sm text-muted-foreground">
No service instances configured yet.
</p>
) : (
<div className="grid grid-cols-1 gap-4 md:grid-cols-[320px_minmax(0,1fr)]">
<SelectionRailCard
title="Services"
description="Select a service to edit its configuration."
@@ -1441,7 +1458,17 @@ function ServicesAdminCard({
</p>
)}
</SectionCard>
</div>
</div>
)}
</div>
)}
{serviceSubtab === "dashboards" && <DashboardManagementCard />}
</TabbedCard>
<CreateServiceDialog
open={createOpen}
onClose={() => setCreateOpen(false)}
/>
</>
);
}
@@ -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(
<MemoryRouter initialEntries={["/settings?tab=services"]}>
<Settings />
</MemoryRouter>,
);
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();
});
});