Skip to content
121 linesCodeBlameRaw
1import { Laptop } from "lucide-react";
2import { Link } from "react-router";
3
4import type { DesktopBackend, DesktopDiagnosis, DesktopRunner } from "@g1t/contracts";
5
6import { BACKEND_LABEL, type DesktopRunnerAction, desktopRunner, useDesktopRunner } from "../lib/desktop-runner";
7import { EmptyState, TimeAgo } from "./ui";
8import { Badge } from "./ui/badge";
9import { Button } from "./ui/button";
10import { Card } from "./ui/card";
11
12/** A diagnosis's tone, as a dot. */
13const DOT: Record<DesktopDiagnosis["tone"], string> = {
14 ok: "bg-success",
15 working: "bg-info",
16 off: "bg-faint",
17 warning: "bg-warn",
18 problem: "bg-danger",
19};
20
21type Row = {
22 machine: string;
23 person: string | null;
24 backend: DesktopBackend;
25 version: string | null;
26 diagnosis: DesktopDiagnosis;
27 /** This computer's fixes, which only work here. */
28 actions: DesktopRunnerAction[];
29 here: boolean;
30 reportedAt: string | null;
31};
32
33/** The desktops to list: this computer, live, first; then the others, as their apps last said, or closed. */
34export function desktopRows(desktops: DesktopRunner[], here: ReturnType<typeof useDesktopRunner>, slug: string): Row[] {
35 const rows: Row[] = [];
36 if (here && here.workspace === slug) {
37 rows.push({
38 machine: here.machine,
39 person: null,
40 backend: here.backend,
41 version: here.version,
42 diagnosis: here.diagnosis,
43 actions: here.diagnosis.actions,
44 here: true,
45 reportedAt: null,
46 });
47 }
48 for (const desktop of desktops) {
49 if (rows.some((row) => row.here && row.machine.toLowerCase() === desktop.machine.toLowerCase())) continue;
50 rows.push({
51 machine: desktop.machine,
52 person: desktop.person,
53 backend: desktop.backend,
54 version: desktop.version,
55 diagnosis: desktop.open ? desktop.diagnosis : desktop.closed,
56 actions: [],
57 here: false,
58 reportedAt: desktop.reportedAt,
59 });
60 }
61 return rows;
62}
63
64/**
65 * The workspace's desktops: each person's computer with the g1t desktop
66 * app, whose runner runs while g1t is open there. What each says, why it
67 * isn't running and the fix, is the app's own words (lib/desktop-runner.ts).
68 */
69export function DesktopRunners({ desktops, slug }: { desktops: DesktopRunner[] | null; slug: string }) {
70 const here = useDesktopRunner();
71 if (!desktops) return <p className="text-sm text-muted">The desktops can&apos;t be read right now.</p>;
72 const rows = desktopRows(desktops, here, slug);
73 if (rows.length === 0) {
74 return (
75 <EmptyState title="No desktops yet">
76 The <Link to="/download" className="underline underline-offset-2">g1t desktop app</Link> runs this workspace&apos;s self-hosted jobs while it&apos;s open, sandboxed, with nothing to set up.
77 Open it signed in as an owner of {slug}.
78 </EmptyState>
79 );
80 }
81 return (
82 <Card asChild divided>
83 <ul>
84 {rows.map((row) => (
85 <li key={row.machine} className="flex items-start gap-3 px-4 py-3">
86 <span className={`mt-1.5 size-2 shrink-0 rounded-full ${DOT[row.diagnosis.tone]}`} aria-hidden />
87 <div className="min-w-0 grow">
88 <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
89 <Laptop size={13} className="text-muted" aria-hidden />
90 <span className="font-medium">{row.machine}</span>
91 {row.here && <Badge tone="accent">This computer</Badge>}
92 <Badge tone={row.backend === "sandboxed" ? "neutral" : "warn"}>{BACKEND_LABEL[row.backend]}</Badge>
93 {row.person && <span className="text-xs text-faint">@{row.person}</span>}
94 </div>
95 <p className="mt-1 text-sm text-fg">{row.diagnosis.title}</p>
96 {row.diagnosis.cause && <p className="mt-0.5 text-sm text-muted">{row.diagnosis.cause}</p>}
97 {row.diagnosis.fix && <p className="mt-0.5 text-sm text-fg-soft">{row.diagnosis.fix}</p>}
98 {row.actions.length > 0 && (
99 <div className="mt-2 flex flex-wrap gap-2">
100 {row.actions.map((action, at) => (
101 <Button key={action.id} size="xs" variant={at === 0 ? "secondary" : "outline"} onClick={() => void desktopRunner()?.act(action.id).catch(() => {})}>
102 {action.label}
103 </Button>
104 ))}
105 </div>
106 )}
107 </div>
108 <div className="shrink-0 text-right text-xs text-faint">
109 {row.version && <div>g1t-runner {row.version}</div>}
110 {row.reportedAt && (
111 <div className="mt-0.5">
112 Heard from <TimeAgo at={row.reportedAt} />
113 </div>
114 )}
115 </div>
116 </li>
117 ))}
118 </ul>
119 </Card>
120 );
121}