Skip to content

Commit 943b7d7

Browse files
committed
Finish job structure
1 parent 6e967f1 commit 943b7d7

File tree

1 file changed

+152
-76
lines changed

1 file changed

+152
-76
lines changed

site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx

Lines changed: 152 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import {
33
provisionerDaemonGroups,
44
provisionerJobs,
55
} from "api/queries/organizations";
6-
import type { Organization, ProvisionerJobStatus } from "api/typesGenerated";
6+
import type {
7+
Organization,
8+
ProvisionerJob,
9+
ProvisionerJobStatus,
10+
} from "api/typesGenerated";
711
import { Avatar } from "components/Avatar/Avatar";
812
import { Badge } from "components/Badge/Badge";
913
import { Button } from "components/Button/Button";
@@ -32,13 +36,21 @@ import {
3236
TooltipTrigger,
3337
} from "components/Tooltip/Tooltip";
3438
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
35-
import { BanIcon, TriangleAlertIcon } from "lucide-react";
39+
import { useSearchParamsKey } from "hooks/useSearchParamsKey";
40+
import {
41+
BanIcon,
42+
ChevronDownIcon,
43+
ChevronRightIcon,
44+
Tangent,
45+
TriangleAlertIcon,
46+
} from "lucide-react";
3647
import { useDashboard } from "modules/dashboard/useDashboard";
3748
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
38-
import type { FC } from "react";
49+
import { useState, type FC } from "react";
3950
import { Helmet } from "react-helmet-async";
4051
import { useQuery } from "react-query";
4152
import { useParams } from "react-router-dom";
53+
import { cn } from "utils/cn";
4254
import { docs } from "utils/docs";
4355
import { pageTitle } from "utils/page";
4456
import { relativeTime } from "utils/time";
@@ -48,6 +60,10 @@ const OrganizationProvisionersPage: FC = () => {
4860
// organization: string;
4961
// };
5062
const { organization } = useOrganizationSettings();
63+
const tab = useSearchParamsKey({
64+
key: "tab",
65+
defaultValue: "jobs",
66+
});
5167
// const { entitlements } = useDashboard();
5268
// const { metadata } = useEmbeddedMetadata();
5369
// const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
@@ -76,7 +92,7 @@ const OrganizationProvisionersPage: FC = () => {
7692
</header>
7793

7894
<main>
79-
<Tabs active="jobs">
95+
<Tabs active={tab.value}>
8096
<TabsList>
8197
<TabLink value="jobs" to="?tab=jobs">
8298
Jobs
@@ -88,7 +104,7 @@ const OrganizationProvisionersPage: FC = () => {
88104
</Tabs>
89105

90106
<div className="mt-6">
91-
<JobsTabContent org={organization} />
107+
{tab.value === "jobs" && <JobsTabContent org={organization} />}
92108
</div>
93109
</main>
94110
</div>
@@ -101,7 +117,6 @@ type JobsTabContentProps = {
101117
};
102118

103119
const JobsTabContent: FC<JobsTabContentProps> = ({ org }) => {
104-
const { organization } = useOrganizationSettings();
105120
const { data: jobs, isLoadingError } = useQuery(provisionerJobs(org.id));
106121

107122
return (
@@ -125,76 +140,7 @@ const JobsTabContent: FC<JobsTabContentProps> = ({ org }) => {
125140
<TableBody>
126141
{jobs ? (
127142
jobs.length > 0 ? (
128-
jobs.map(({ metadata, ...job }) => {
129-
if (!metadata) {
130-
throw new Error(
131-
`Metadata is required but it is missing in the job ${job.id}`,
132-
);
133-
}
134-
135-
const canCancel = ["pending", "running"].includes(job.status);
136-
137-
return (
138-
<TableRow key={job.id}>
139-
<TableCell className="[&:first-letter]:uppercase">
140-
{relativeTime(new Date(job.created_at))}
141-
</TableCell>
142-
<TableCell>
143-
<Badge size="sm">{job.type}</Badge>
144-
</TableCell>
145-
<TableCell>
146-
<div className="flex items-center gap-1">
147-
<Avatar
148-
variant="icon"
149-
src={metadata.template_icon}
150-
fallback={
151-
metadata.template_display_name ||
152-
metadata.template_name
153-
}
154-
/>
155-
{metadata.template_display_name ??
156-
metadata.template_name}
157-
</div>
158-
</TableCell>
159-
<TableCell>
160-
<Badge size="sm">[foo=bar]</Badge>
161-
</TableCell>
162-
<TableCell>
163-
<StatusIndicator
164-
size="sm"
165-
variant={statusIndicatorVariant(job.status)}
166-
>
167-
<StatusIndicatorDot />
168-
<span className="[&:first-letter]:uppercase">
169-
{job.status}
170-
</span>
171-
{job.status === "failed" && (
172-
<TriangleAlertIcon className="size-icon-xs p-[1px]" />
173-
)}
174-
{job.status === "pending" &&
175-
`(${job.queue_position}/${job.queue_size})`}
176-
</StatusIndicator>
177-
</TableCell>
178-
<TableCell className="text-right">
179-
<TooltipProvider>
180-
<Tooltip>
181-
<TooltipTrigger asChild>
182-
<Button
183-
disabled={!canCancel}
184-
aria-label="Cancel job"
185-
size="icon"
186-
variant="outline"
187-
>
188-
<BanIcon />
189-
</Button>
190-
</TooltipTrigger>
191-
<TooltipContent>Cancel job</TooltipContent>
192-
</Tooltip>
193-
</TooltipProvider>
194-
</TableCell>
195-
</TableRow>
196-
);
197-
})
143+
jobs.map((j) => <JobRow key={j.id} job={j} />)
198144
) : (
199145
<TableEmpty message="No provisioner jobs found" />
200146
)
@@ -209,6 +155,136 @@ const JobsTabContent: FC<JobsTabContentProps> = ({ org }) => {
209155
);
210156
};
211157

158+
type JobRowProps = {
159+
job: ProvisionerJob;
160+
};
161+
162+
const JobRow: FC<JobRowProps> = ({ job }) => {
163+
const metadata = job.metadata;
164+
const canCancel = ["pending", "running"].includes(job.status);
165+
const [isOpen, setIsOpen] = useState(false);
166+
167+
return (
168+
<>
169+
<TableRow key={job.id}>
170+
<TableCell>
171+
<button
172+
className={cn([
173+
"flex items-center gap-1 p-0 bg-transparent border-0 text-inherit text-xs cursor-pointer",
174+
"transition-colors hover:text-content-primary font-medium",
175+
isOpen && "text-content-primary",
176+
])}
177+
type="button"
178+
onClick={() => {
179+
setIsOpen((v) => !v);
180+
}}
181+
>
182+
{isOpen ? (
183+
<ChevronDownIcon className="size-icon-sm p-0.5" />
184+
) : (
185+
<ChevronRightIcon className="size-icon-sm p-0.5" />
186+
)}
187+
<span className="[&:first-letter]:uppercase">
188+
{relativeTime(new Date(job.created_at))}
189+
</span>
190+
</button>
191+
</TableCell>
192+
<TableCell>
193+
<Badge size="sm">{job.type}</Badge>
194+
</TableCell>
195+
<TableCell>
196+
{job.metadata.template_name ? (
197+
<div className="flex items-center gap-1">
198+
<Avatar
199+
variant="icon"
200+
src={metadata.template_icon}
201+
fallback={
202+
metadata.template_display_name || metadata.template_name
203+
}
204+
/>
205+
{metadata.template_display_name ?? metadata.template_name}
206+
</div>
207+
) : (
208+
"Not linked to any template"
209+
)}
210+
</TableCell>
211+
<TableCell>
212+
<Badge size="sm">[foo=bar]</Badge>
213+
</TableCell>
214+
<TableCell>
215+
<StatusIndicator
216+
size="sm"
217+
variant={statusIndicatorVariant(job.status)}
218+
>
219+
<StatusIndicatorDot />
220+
<span className="[&:first-letter]:uppercase">{job.status}</span>
221+
{job.status === "failed" && (
222+
<TriangleAlertIcon className="size-icon-xs p-[1px]" />
223+
)}
224+
{job.status === "pending" &&
225+
`(${job.queue_position}/${job.queue_size})`}
226+
</StatusIndicator>
227+
</TableCell>
228+
<TableCell className="text-right">
229+
<TooltipProvider>
230+
<Tooltip>
231+
<TooltipTrigger asChild>
232+
<Button
233+
disabled={!canCancel}
234+
aria-label="Cancel job"
235+
size="icon"
236+
variant="outline"
237+
>
238+
<BanIcon />
239+
</Button>
240+
</TooltipTrigger>
241+
<TooltipContent>Cancel job</TooltipContent>
242+
</Tooltip>
243+
</TooltipProvider>
244+
</TableCell>
245+
</TableRow>
246+
247+
{isOpen && (
248+
<TableRow>
249+
<TableCell colSpan={999} className="p-4 border-t-0">
250+
<div
251+
className={cn([
252+
"grid grid-cols-[auto_1fr] gap-x-4 items-center",
253+
"[&_span:nth-child(even)]:text-content-primary [&_span:nth-child(even)]:font-mono",
254+
"[&_span:nth-child(even)]:leading-[22px]",
255+
])}
256+
>
257+
<span>Job ID:</span>
258+
<span>{job.id}</span>
259+
260+
<span>Available provisioners:</span>
261+
<span>
262+
{job.available_workers
263+
? JSON.stringify(job.available_workers)
264+
: "[]"}
265+
</span>
266+
267+
<span>Completed by provisioner:</span>
268+
<span>{job.worker_id}</span>
269+
270+
<span>Associated workspace:</span>
271+
<span>{job.metadata.workspace_name ?? "null"}</span>
272+
273+
<span>Creation time:</span>
274+
<span>{job.created_at}</span>
275+
276+
<span>Queue:</span>
277+
<span>
278+
{job.queue_position}/{job.queue_size}
279+
</span>
280+
</div>
281+
</TableCell>
282+
</TableRow>
283+
)}
284+
</>
285+
);
286+
};
287+
212288
function statusIndicatorVariant(
213289
status: ProvisionerJobStatus,
214290
): StatusIndicatorProps["variant"] {

0 commit comments

Comments
 (0)