diff --git a/web/app/workflows/page.tsx b/web/app/workflows/page.tsx new file mode 100644 index 0000000..d93d55a --- /dev/null +++ b/web/app/workflows/page.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { api, Workflow } from "@/lib/api"; +import { Button, Card } from "@/components/ui"; +import { Table, Thead, Tbody, Tr, Th, Td } from "@/components/ui"; + +export default function WorkflowsPage() { + const qc = useQueryClient(); + const router = useRouter(); + const [error, setError] = useState(null); + + const { data: workflows, isLoading, error: loadError } = useQuery({ + queryKey: ["workflows"], + queryFn: api.listWorkflows, + }); + + const { mutate: create, isPending } = useMutation({ + mutationFn: () => api.createWorkflow({ name: "Untitled workflow", target_server_ids: [], steps: [] }), + onSuccess: (workflow) => { + qc.invalidateQueries({ queryKey: ["workflows"] }); + router.push(`/workflows/${workflow.workflow_id}`); + }, + onError: (err) => setError((err as Error).message), + }); + + return ( +
+
+
+

Workflows

+

+ {workflows?.length ?? 0} workflow{workflows?.length !== 1 ? "s" : ""} · run reusable steps across servers +

+
+ +
+ + {error && ( +
+ {error} +
+ )} + + + {isLoading ? ( +
+
+
+ ) : loadError ? ( +
Failed to load workflows. Is the backend running?
+ ) : workflows && workflows.length > 0 ? ( + + + + + + + + + + {workflows.map((w: Workflow) => ( + + + + + + + ))} + +
NameTargetsSteps +
+ {w.name} + + + {w.target_server_ids.length} server{w.target_server_ids.length !== 1 ? "s" : ""} + + + {w.steps.length} + + + + +
+ ) : ( +
+
+ + + +
+

No workflows yet.

+ +
+ )} + +
+ ); +} diff --git a/web/components/Sidebar.tsx b/web/components/Sidebar.tsx index ed9ade6..387bf2d 100644 --- a/web/components/Sidebar.tsx +++ b/web/components/Sidebar.tsx @@ -35,6 +35,14 @@ function SecretIcon() { ); } +function WorkflowIcon() { + return ( + + + + ); +} + function AuditIcon() { return ( @@ -56,6 +64,7 @@ const navItems: NavItem[] = [ { href: "/servers", label: "Servers", icon: }, { href: "/keys", label: "SSH Keys", icon: }, { href: "/secrets", label: "Secrets", icon: }, + { href: "/workflows", label: "Workflows", icon: }, { href: "/audit", label: "Audit Log", icon: }, { href: "/settings", label: "Settings", icon: }, ];