feat: add navigation for remote machines

This commit is contained in:
Developer
2026-07-14 21:06:25 +00:00
parent 37533dd219
commit 4562a9dfca
3 changed files with 68 additions and 8 deletions
+11 -5
View File
@@ -27,7 +27,10 @@ import { usePersistentState } from "./hooks/usePersistentState";
import { useIsMobile } from "./hooks/useIsMobile"; import { useIsMobile } from "./hooks/useIsMobile";
import { useServiceInstances } from "./hooks/useServices"; import { useServiceInstances } from "./hooks/useServices";
import { useDashboards } from "./hooks/useDashboards"; import { useDashboards } from "./hooks/useDashboards";
import { configuredNavEntries } from "./integrations/navEntries"; import {
configuredNavEntries,
remoteMachineNavEntries,
} from "./integrations/navEntries";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Tooltip, Tooltip,
@@ -97,10 +100,13 @@ function useNavItems() {
const configuredTypes = new Set( const configuredTypes = new Set(
services.filter((s) => s.enabled).map((s) => s.service_type), services.filter((s) => s.enabled).map((s) => s.service_type),
); );
const serviceEntries = configuredNavEntries(configuredTypes).map((e) => ({ const serviceEntries = [
path: e.path, ...configuredNavEntries(configuredTypes),
label: e.label, ...remoteMachineNavEntries(services),
icon: e.icon, ].map((entry) => ({
path: entry.path,
label: entry.label,
icon: entry.icon,
})); }));
const dashboardEntries = dashboards.map((d) => ({ const dashboardEntries = dashboards.map((d) => ({
path: `/d/${d.slug}`, path: `/d/${d.slug}`,
@@ -1,5 +1,9 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { configuredNavEntries, SERVICE_TYPE_NAV_ENTRIES } from "../navEntries"; import {
configuredNavEntries,
remoteMachineNavEntries,
SERVICE_TYPE_NAV_ENTRIES,
} from "../navEntries";
describe("navEntries", () => { describe("navEntries", () => {
it("returns no entries when no types are configured", () => { it("returns no entries when no types are configured", () => {
@@ -13,8 +17,33 @@ describe("navEntries", () => {
expect(entries[0].path).toBe("/services/jellyfin"); expect(entries[0].path).toBe("/services/jellyfin");
}); });
it("does not expose remote machines as a top-level entry", () => { it("creates one top-level entry per enabled remote machine", () => {
expect(configuredNavEntries(new Set(["remote_machine"]))).toEqual([]); const entries = remoteMachineNavEntries([
{
id: "storage",
name: "Storage",
service_type: "remote_machine",
enabled: true,
},
{
id: "worker",
name: "Worker",
service_type: "remote_machine",
enabled: true,
},
{
id: "disabled",
name: "Disabled",
service_type: "remote_machine",
enabled: false,
},
]);
expect(entries.map((entry) => entry.label)).toEqual(["Storage", "Worker"]);
expect(entries.map((entry) => entry.path)).toEqual([
"/services/remote_machine/storage",
"/services/remote_machine/worker",
]);
}); });
it("returns all observability entries", () => { it("returns all observability entries", () => {
+25
View File
@@ -12,6 +12,7 @@ import {
GanttChartSquare, GanttChartSquare,
Magnet, Magnet,
Monitor, Monitor,
Server,
Users, Users,
type LucideIcon, type LucideIcon,
} from "lucide-react"; } from "lucide-react";
@@ -24,6 +25,13 @@ export interface NavEntry {
path: string; path: string;
} }
export interface RemoteMachineNavSource {
id: string;
name: string;
service_type: string;
enabled: boolean;
}
/** /**
* Static mapping from service type to its conditional nav entry. * Static mapping from service type to its conditional nav entry.
* Uses the service type's display name. One entry per type. * Uses the service type's display name. One entry per type.
@@ -77,3 +85,20 @@ export function configuredNavEntries(configuredTypes: Set<string>): NavEntry[] {
configuredTypes.has(e.serviceType), configuredTypes.has(e.serviceType),
); );
} }
/** One direct sidebar entry for each enabled Remote Machine service. */
export function remoteMachineNavEntries(
services: RemoteMachineNavSource[],
): NavEntry[] {
return services
.filter(
(service) => service.enabled && service.service_type === "remote_machine",
)
.sort((left, right) => left.name.localeCompare(right.name))
.map((service) => ({
serviceType: service.service_type,
label: service.name,
icon: Server,
path: `/services/remote_machine/${encodeURIComponent(service.id)}`,
}));
}