From 33b5200e42d0f7279544018a20e36aa6b5980e17 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Mon, 21 Jul 2025 21:03:59 +0200 Subject: [PATCH] Add Header, InstanceList, and InstanceCard components for instance management UI --- ui/src/App.tsx | 21 +++------ ui/src/components/Header.tsx | 26 +++++++++++ ui/src/components/InstanceCard.tsx | 74 ++++++++++++++++++++++++++++++ ui/src/components/InstanceList.tsx | 56 ++++++++++++++++++++++ ui/src/components/ui/badge.tsx | 46 +++++++++++++++++++ 5 files changed, 209 insertions(+), 14 deletions(-) create mode 100644 ui/src/components/Header.tsx create mode 100644 ui/src/components/InstanceCard.tsx create mode 100644 ui/src/components/InstanceList.tsx create mode 100644 ui/src/components/ui/badge.tsx diff --git a/ui/src/App.tsx b/ui/src/App.tsx index ae21055..e6ef2fc 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,20 +1,13 @@ -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import Header from '@/components/Header' +import InstanceList from '@/components/InstanceList' function App() { return ( -
-

Llamactl Dashboard

- - - - Sample Instance - - -

Status: Running

- -
-
+
+
+
+ +
) } diff --git a/ui/src/components/Header.tsx b/ui/src/components/Header.tsx new file mode 100644 index 0000000..f2ff9a1 --- /dev/null +++ b/ui/src/components/Header.tsx @@ -0,0 +1,26 @@ +import { Button } from '@/components/ui/button' + +function Header() { + const handleCreateInstance = () => { + // TODO: Open create instance dialog + console.log('Create instance clicked') + } + + return ( +
+
+
+

+ LlamaCtl Dashboard +

+ + +
+
+
+ ) +} + +export default Header \ No newline at end of file diff --git a/ui/src/components/InstanceCard.tsx b/ui/src/components/InstanceCard.tsx new file mode 100644 index 0000000..081a951 --- /dev/null +++ b/ui/src/components/InstanceCard.tsx @@ -0,0 +1,74 @@ +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Instance } from '@/types/instance' +import { useInstances } from '@/hooks/useInstances' + +interface InstanceCardProps { + instance: Instance +} + +function InstanceCard({ instance }: InstanceCardProps) { + const { startInstance, stopInstance, deleteInstance } = useInstances() + + const handleStart = () => { + startInstance(instance.name) + } + + const handleStop = () => { + stopInstance(instance.name) + } + + const handleDelete = () => { + if (confirm(`Are you sure you want to delete instance "${instance.name}"?`)) { + deleteInstance(instance.name) + } + } + + return ( + + +
+ {instance.name} + + {instance.running ? "Running" : "Stopped"} + +
+
+ + +
+ {!instance.running ? ( + + ) : ( + + )} + + +
+
+
+ ) +} + +export default InstanceCard \ No newline at end of file diff --git a/ui/src/components/InstanceList.tsx b/ui/src/components/InstanceList.tsx new file mode 100644 index 0000000..1fb0398 --- /dev/null +++ b/ui/src/components/InstanceList.tsx @@ -0,0 +1,56 @@ +import { useInstances } from '@/hooks/useInstances' +import InstanceCard from '@/components/InstanceCard' + +function InstanceList() { + const { instances, loading, error } = useInstances() + + if (loading) { + return ( +
+
+
+

Loading instances...

+
+
+ ) + } + + if (error) { + return ( +
+
+

Error loading instances

+

{error}

+
+
+ ) + } + + if (instances.length === 0) { + return ( +
+

No instances found

+

Create your first instance to get started

+
+ ) + } + + return ( +
+

+ Instances ({instances.length}) +

+ +
+ {instances.map((instance) => ( + + ))} +
+
+ ) +} + +export default InstanceList \ No newline at end of file diff --git a/ui/src/components/ui/badge.tsx b/ui/src/components/ui/badge.tsx new file mode 100644 index 0000000..0205413 --- /dev/null +++ b/ui/src/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants }