Skip to content

Commit

Permalink
feat: More UI responsiveness (#352)
Browse files Browse the repository at this point in the history
- Collapse Run list into drawer on narrow screens
- Move Runs, Machines and Services to horizontal tabs
- Standardise border radius'
  • Loading branch information
johnjcsmith authored Dec 22, 2024
1 parent 48b24c6 commit b36ca56
Show file tree
Hide file tree
Showing 10 changed files with 622 additions and 809 deletions.
60 changes: 51 additions & 9 deletions app/app/clusters/[clusterId]/runs/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { client } from "@/client/client";
import { ClusterDetails } from "@/components/cluster-details";
import { MachineList } from "@/components/MachineList";
import { ServiceList } from "@/components/ServiceList";
import { RunList } from "@/components/WorkflowList";
import { auth } from "@clerk/nextjs";
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { Blocks, Cpu } from "lucide-react";

export async function generateMetadata({
params: { clusterId },
Expand All @@ -23,7 +27,7 @@ export async function generateMetadata({
return { title: `${cluster.body?.name}` };
}

function Home({
async function Home({
params: { clusterId },
children,
}: {
Expand All @@ -32,15 +36,53 @@ function Home({
};
children: React.ReactNode;
}) {
const { getToken } = auth();
const token = await getToken();
const cluster = await client.getCluster({
headers: { authorization: `Bearer ${token}` },
params: { clusterId },
});

return (
<main className="flex-grow">
<div className="flex space-x-6 pt-6 pb-6 pl-6 pr-2">
<RunList clusterId={clusterId} />
<div className="w-7/12 flex flex-col space-y-2 overflow-auto">
{children}
<main className="flex flex-col h-[calc(100vh-8rem)]">
<div className="flex items-center justify-between px-6 py-2">
<div className="md:hidden">
<RunList clusterId={clusterId} />
</div>
<span className="text-sm text-muted-foreground font-medium">
{cluster.status === 200 ? cluster.body.name : clusterId}
</span>
<div className="flex items-center gap-2">
<Sheet>
<SheetTrigger asChild>
<Button variant="outline" size="sm" className="h-8">
<Cpu className="h-4 w-4 mr-1.5" />
Machines
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-[85%] sm:w-[600px] pt-10">
<MachineList clusterId={clusterId} />
</SheetContent>
</Sheet>
<Sheet>
<SheetTrigger asChild>
<Button variant="outline" size="sm" className="h-8">
<Blocks className="h-4 w-4 mr-1.5" />
Services
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-[85%] sm:w-[800px] pt-10">
<ServiceList clusterId={clusterId} />
</SheetContent>
</Sheet>
</div>
<div className="w-1/12 flex flex-col">
<ClusterDetails clusterId={clusterId} />
</div>
<div className="flex-1 flex min-h-0 pl-2">
<div className="hidden md:block">
<RunList clusterId={clusterId} />
</div>
<div className="flex-1 overflow-auto">
{children}
</div>
</div>
</main>
Expand Down
2 changes: 1 addition & 1 deletion app/app/clusters/[clusterId]/runs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default async function Page({

return (
<div className="flex flex-col overflow-auto px-2 space-y-4">
<Card className="w-full onboarding-prompt bg-white border border-gray-200 rounded-xl transition-all duration-200 hover:shadow-md mb-6">
<Card className="w-full onboarding-prompt bg-white border border-gray-200 rounded-xl transition-all duration-200 mb-6">
<CardContent className="pt-6">
<PromptTextarea clusterId={clusterId} />
</CardContent>
Expand Down
162 changes: 162 additions & 0 deletions app/components/MachineList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"use client";

import { client } from "@/client/client";
import { contract } from "@/client/contract";
import { useAuth } from "@clerk/nextjs";
import { ClientInferResponseBody } from "@ts-rest/core";
import { formatRelative } from "date-fns";
import { useCallback, useEffect, useState } from "react";
import { DeadGrayCircle, DeadRedCircle, LiveGreenCircle } from "./circles";
import ErrorDisplay from "./error-display";
import { EventsOverlayButton } from "./events-overlay";
import { cn } from "@/lib/utils";

function MachineCard({
machine,
clusterId,
}: {
machine: ClientInferResponseBody<typeof contract.listMachines, 200>[number];
clusterId: string;
}) {
const isLive =
Date.now() - new Date(machine.lastPingAt!).getTime() < 1000 * 60;

return (
<div
className={cn(
"rounded-xl p-5 shadow-sm border transition-all duration-200 hover:shadow-md",
isLive
? "bg-green-50/30 border-green-100"
: "bg-gray-50/30 border-gray-100"
)}
>
<div className="flex items-center justify-between mb-4 pb-3 border-b border-border/50">
<div className="flex items-center gap-3">
<div>{isLive ? <LiveGreenCircle /> : <DeadGrayCircle />}</div>
<div>
<div className="text-sm font-medium font-mono">{machine.id}</div>
<div className="text-xs text-muted-foreground">{machine.ip}</div>
</div>
</div>
<EventsOverlayButton
clusterId={clusterId}
query={{ machineId: machine.id }}
/>
</div>
<div className="flex items-center gap-2 text-xs">
<div
className={cn(
"px-2 py-1 rounded-full font-medium",
isLive ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-700"
)}
>
{isLive ? "Active" : "Inactive"}
</div>
<div className="text-muted-foreground">
Last heartbeat: {formatRelative(machine.lastPingAt!, new Date())}
</div>
</div>
</div>
);
}

export function MachineList({ clusterId }: { clusterId: string }) {
const [machines, setMachines] = useState<
ClientInferResponseBody<typeof contract.listMachines, 200>
>([]);
const [liveMachineCount, setLiveMachineCount] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const { getToken } = useAuth();
const [error, setError] = useState<any>(null);

const getClusterMachines = useCallback(async () => {
setIsLoading(true);
try {
const machinesResponse = await client.listMachines({
headers: {
authorization: `Bearer ${await getToken()}`,
},
params: {
clusterId,
},
});

if (machinesResponse.status === 200) {
setMachines(machinesResponse.body);
setLiveMachineCount(
machinesResponse.body.filter(
(m) => Date.now() - new Date(m.lastPingAt!).getTime() < 1000 * 60
).length
);
} else {
setError(machinesResponse);
}
} finally {
setIsLoading(false);
}
}, [clusterId, getToken]);

useEffect(() => {
getClusterMachines();
const interval = setInterval(getClusterMachines, 1000 * 10);
return () => clearInterval(interval);
}, [getClusterMachines]);

if (error) {
return <ErrorDisplay status={error.status} error={error} />;
}

if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}

return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold mb-2">Machines</h2>
<div className="flex items-center gap-4">
<p className="text-sm text-muted-foreground">
You have {liveMachineCount} machine
{liveMachineCount === 1 ? "" : "s"} connected
</p>
{liveMachineCount > 0 && (
<div className="px-2 py-1 rounded-full bg-green-100 text-green-700 text-xs font-medium">
{liveMachineCount} Active
</div>
)}
</div>
</div>

<div className="grid grid-cols-1 gap-4">
{machines && machines.length > 0 ? (
machines
.sort(
(a, b) =>
new Date(b.lastPingAt!).getTime() -
new Date(a.lastPingAt!).getTime()
)
.map((m) => (
<MachineCard key={m.id} machine={m} clusterId={clusterId} />
))
) : (
<div className="col-span-full flex items-center justify-center p-8 rounded-xl bg-gray-50 border border-gray-200">
<div className="flex flex-col items-center gap-3">
<DeadRedCircle />
<span className="text-sm text-gray-600">
Your machines are offline
</span>
<p className="text-xs text-muted-foreground max-w-[300px] text-center">
No active machines found in this cluster. Make sure your
machines are running and properly configured.
</p>
</div>
</div>
)}
</div>
</div>
);
}
Loading

0 comments on commit b36ca56

Please sign in to comment.