Skip to content

Commit f027485

Browse files
committed
Add daemons page
1 parent 71f4fe5 commit f027485

File tree

5 files changed

+331
-122
lines changed

5 files changed

+331
-122
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type {
2+
ProvisionerDaemonJob,
3+
ProvisionerJob,
4+
ProvisionerJobStatus,
5+
} from "api/typesGenerated";
6+
import {
7+
StatusIndicator,
8+
StatusIndicatorDot,
9+
type StatusIndicatorProps,
10+
} from "components/StatusIndicator/StatusIndicator";
11+
import { TriangleAlertIcon } from "lucide-react";
12+
import type { FC } from "react";
13+
14+
type JobStatusIndicatorProps = {
15+
job: ProvisionerJob | ProvisionerDaemonJob;
16+
};
17+
18+
export const JobStatusIndicator: FC<JobStatusIndicatorProps> = ({ job }) => {
19+
const isProvisionerJob = "queue_position" in job;
20+
return (
21+
<StatusIndicator size="sm" variant={statusIndicatorVariant(job.status)}>
22+
<StatusIndicatorDot />
23+
<span className="[&:first-letter]:uppercase">{job.status}</span>
24+
{job.status === "failed" && (
25+
<TriangleAlertIcon className="size-icon-xs p-[1px]" />
26+
)}
27+
{job.status === "pending" &&
28+
isProvisionerJob &&
29+
`(${job.queue_position}/${job.queue_size})`}
30+
</StatusIndicator>
31+
);
32+
};
33+
34+
function statusIndicatorVariant(
35+
status: ProvisionerJobStatus,
36+
): StatusIndicatorProps["variant"] {
37+
switch (status) {
38+
case "succeeded":
39+
return "success";
40+
case "failed":
41+
return "failed";
42+
case "pending":
43+
case "running":
44+
case "canceling":
45+
return "pending";
46+
case "canceled":
47+
case "unknown":
48+
return "inactive";
49+
}
50+
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { provisionerDaemons } from "api/queries/organizations";
2+
import type {
3+
Organization,
4+
ProvisionerDaemon,
5+
ProvisionerDaemonStatus,
6+
} from "api/typesGenerated";
7+
import { Badge } from "components/Badge/Badge";
8+
import { Link } from "components/Link/Link";
9+
import {
10+
StatusIndicator,
11+
StatusIndicatorDot,
12+
type StatusIndicatorProps,
13+
} from "components/StatusIndicator/StatusIndicator";
14+
import {
15+
Table,
16+
TableBody,
17+
TableCell,
18+
TableHead,
19+
TableHeader,
20+
TableRow,
21+
} from "components/Table/Table";
22+
import { TableEmpty } from "components/TableEmpty/TableEmpty";
23+
import { TableLoader } from "components/TableLoader/TableLoader";
24+
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
25+
import { useState, type FC } from "react";
26+
import { useQuery } from "react-query";
27+
import { cn } from "utils/cn";
28+
import { docs } from "utils/docs";
29+
import { relativeTime } from "utils/time";
30+
import { JobStatusIndicator } from "./JobStatusIndicator";
31+
32+
type ProvisionerDaemonsPageProps = {
33+
org: Organization;
34+
};
35+
36+
export const ProvisionerDaemonsPage: FC<ProvisionerDaemonsPageProps> = ({
37+
org,
38+
}) => {
39+
const { data: daemons, isLoadingError } = useQuery(
40+
provisionerDaemons(org.id),
41+
);
42+
43+
return (
44+
<section className="flex flex-col gap-8">
45+
<p className="text-sm text-content-secondary m-0 mt-2">
46+
Coder server runs provisioner daemons which execute terraform during
47+
workspace and template builds.{" "}
48+
<Link
49+
href={docs(
50+
"/tutorials/best-practices/security-best-practices#provisioner-daemons",
51+
)}
52+
>
53+
View docs
54+
</Link>
55+
</p>
56+
57+
<Table>
58+
<TableHeader>
59+
<TableRow>
60+
<TableHead>Last seen</TableHead>
61+
<TableHead>Name</TableHead>
62+
<TableHead>Template</TableHead>
63+
<TableHead>Tags</TableHead>
64+
<TableHead>Status</TableHead>
65+
</TableRow>
66+
</TableHeader>
67+
<TableBody>
68+
{daemons ? (
69+
daemons.length > 0 ? (
70+
daemons.map((d) => <DaemonRow key={d.id} daemon={d} />)
71+
) : (
72+
<TableEmpty message="No provisioner daemons found" />
73+
)
74+
) : isLoadingError ? (
75+
<TableEmpty message="Error loading the provisioner daemons" />
76+
) : (
77+
<TableLoader />
78+
)}
79+
</TableBody>
80+
</Table>
81+
</section>
82+
);
83+
};
84+
85+
type DaemonRowProps = {
86+
daemon: ProvisionerDaemon;
87+
};
88+
89+
const DaemonRow: FC<DaemonRowProps> = ({ daemon }) => {
90+
const [isOpen, setIsOpen] = useState(false);
91+
92+
return (
93+
<>
94+
<TableRow key={daemon.id}>
95+
<TableCell>
96+
<button
97+
className={cn([
98+
"flex items-center gap-1 p-0 bg-transparent border-0 text-inherit text-xs cursor-pointer",
99+
"transition-colors hover:text-content-primary font-medium whitespace-nowrap",
100+
isOpen && "text-content-primary",
101+
])}
102+
type="button"
103+
onClick={() => {
104+
setIsOpen((v) => !v);
105+
}}
106+
>
107+
{isOpen ? (
108+
<ChevronDownIcon className="size-icon-sm p-0.5" />
109+
) : (
110+
<ChevronRightIcon className="size-icon-sm p-0.5" />
111+
)}
112+
<span className="[&:first-letter]:uppercase">
113+
{relativeTime(
114+
new Date(daemon.last_seen_at ?? new Date().toISOString()),
115+
)}
116+
</span>
117+
</button>
118+
</TableCell>
119+
<TableCell>{daemon.name}</TableCell>
120+
<TableCell>Template</TableCell>
121+
<TableCell>
122+
<div className="flex items-center gap-1 flex-wrap">
123+
{Object.entries(daemon.tags).map(([k, v]) => (
124+
<Badge size="sm" key={k} className="whitespace-nowrap">
125+
[{k}
126+
{v && `=${v}`}]
127+
</Badge>
128+
))}
129+
</div>
130+
</TableCell>
131+
<TableCell>
132+
<StatusIndicator
133+
size="sm"
134+
variant={statusIndicatorVariant(daemon.status)}
135+
>
136+
<StatusIndicatorDot />
137+
<span className="[&:first-letter]:uppercase">{daemon.status}</span>
138+
</StatusIndicator>
139+
</TableCell>
140+
</TableRow>
141+
142+
{isOpen && (
143+
<TableRow>
144+
<TableCell colSpan={999} className="p-4 border-t-0">
145+
<div
146+
className={cn([
147+
"grid grid-cols-[auto_1fr] gap-x-4 items-center",
148+
"[&_span:nth-child(even)]:text-content-primary [&_span:nth-child(even)]:font-mono",
149+
"[&_span:nth-child(even)]:leading-[22px]",
150+
])}
151+
>
152+
<span>Last seen:</span>
153+
<span>{daemon.last_seen_at}</span>
154+
155+
<span>Creation time:</span>
156+
<span>{daemon.created_at}</span>
157+
158+
<span>Version:</span>
159+
<span>{daemon.version}</span>
160+
161+
{daemon.current_job && (
162+
<>
163+
<span>Last job:</span>
164+
<span>{daemon.current_job.id}</span>
165+
166+
<span>Last job state:</span>
167+
<span>
168+
<JobStatusIndicator job={daemon.current_job} />
169+
</span>
170+
</>
171+
)}
172+
173+
{daemon.previous_job && (
174+
<>
175+
<span>Previous job:</span>
176+
<span>{daemon.previous_job.id}</span>
177+
178+
<span>Previous job state:</span>
179+
<span>
180+
<JobStatusIndicator job={daemon.previous_job} />
181+
</span>
182+
</>
183+
)}
184+
</div>
185+
</TableCell>
186+
</TableRow>
187+
)}
188+
</>
189+
);
190+
};
191+
192+
function statusIndicatorVariant(
193+
status: ProvisionerDaemonStatus | null,
194+
): StatusIndicatorProps["variant"] {
195+
switch (status) {
196+
case "idle":
197+
return "success";
198+
case "busy":
199+
return "pending";
200+
case "offline":
201+
case null:
202+
return "inactive";
203+
}
204+
}

0 commit comments

Comments
 (0)