1import type { ReactNode } from "react";
2import type { DeviceToolVersions as ToolVersions } from "@t3tools/contracts";
3import { InlineButton } from "~/components/ui/button";
4import { Popover, PopoverPopup, PopoverTitle, PopoverTrigger } from "~/components/ui/popover";
5
6export function DeviceToolVersions({
7 tools,
8 action,
9 kind,
10 owner,
11 error,
12}: {
13 tools: ToolVersions | undefined;
14 action?: ReactNode;
15 kind?: keyof ToolVersions;
16 owner?: string | undefined;
17 error?: string | undefined;
18}) {
19 const selected = kind ? tools?.[kind] : undefined;
20 const version =
21 selected?.runningVersion ??
22 (selected?.installedVersions.includes(selected.requiredVersion)
23 ? selected.requiredVersion
24 : selected?.installedVersions
25 .toSorted((a, b) => a.localeCompare(b, undefined, { numeric: true }))
26 .at(-1));
27 const label = kind === "hub" ? "Device hub" : "Agent device";
28 return (
29 <Popover>
30 <PopoverTrigger
31 aria-label={
32 kind
33 ? `${label}: ${version ? `version ${version}` : selected ? "not installed" : "version unknown"}. Show details`
34 : undefined
35 }
36 render={<InlineButton tone="muted" />}
37 >
38 {kind
39 ? version
40 ? `v${version}`
41 : selected
42 ? "Not installed"
43 : "Version unknown"
44 : error
45 ? "Versions unavailable"
46 : "Versions"}
47 </PopoverTrigger>
48 <PopoverPopup align="end" width="md">
49 <PopoverTitle>{kind ? label : "Device tools"}</PopoverTitle>
50 {tools ? (
51 <div className="mt-4 divide-y divide-border/50">
52 {(
53 [
54 ["Device hub", tools.hub],
55 ["Agent device", tools.agent],
56 ] as const
57 )
58 .filter(([name]) => !kind || name === label)
59 .map(([name, tool]) => (
60 <div key={name} className="space-y-2 py-3 first:pt-0 last:pb-0">
61 {!kind ? <p className="text-xs font-medium">{name}</p> : null}
62 <dl className="grid grid-cols-[auto_1fr] gap-x-6 gap-y-1 text-xs">
63 <dt className="text-muted-foreground">Running</dt>
64 <dd className="text-right font-mono">{tool.runningVersion ?? "Not running"}</dd>
65 <dt className="text-muted-foreground">Required</dt>
66 <dd className="text-right font-mono">{tool.requiredVersion}</dd>
67 <dt className="text-muted-foreground">Installed</dt>
68 <dd className="text-right font-mono break-words">
69 {tool.installedVersions.join(", ") || "None"}
70 </dd>
71 </dl>
72 </div>
73 ))}
74 </div>
75 ) : (
76 <p className="mt-3 text-xs text-muted-foreground">Versions have not been checked.</p>
77 )}
78 <p className="mt-4 border-t border-border/50 pt-3 text-xs text-muted-foreground">
79 {owner ? `Managed by ${owner}. ` : ""}Tools update automatically on this host when needed.
80 </p>
81 {error ? (
82 <p role="status" className="mt-2 text-xs text-destructive">
83 {error}
84 </p>
85 ) : null}
86 {action ? <div className="mt-3">{action}</div> : null}
87 </PopoverPopup>
88 </Popover>
89 );
90}