Skip to content

feat: display provisioner jobs and daemons for an organization #16532

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b6430bb
Set base structure to display the provisioner jobs
BrunoQuaresma Feb 5, 2025
5d7d58f
Merge branch 'main' of https://github.com/coder/coder into bq/refacto…
BrunoQuaresma Feb 7, 2025
643c362
[WIP]: Load data and display them in the table
BrunoQuaresma Feb 7, 2025
f9db209
Merge branch 'main' of https://github.com/coder/coder into bq/refacto…
BrunoQuaresma Feb 10, 2025
6e967f1
Update table to use API data
BrunoQuaresma Feb 10, 2025
943b7d7
Finish job structure
BrunoQuaresma Feb 10, 2025
2bc6ccf
Display tiny alert for error
BrunoQuaresma Feb 10, 2025
71f4fe5
Fix tags
BrunoQuaresma Feb 10, 2025
f027485
Add daemons page
BrunoQuaresma Feb 10, 2025
dcf8140
Merge branch 'main' of https://github.com/coder/coder into bq/refacto…
BrunoQuaresma Feb 10, 2025
994e186
Merge branch 'main' of https://github.com/coder/coder into bq/refacto…
BrunoQuaresma Feb 11, 2025
3083cef
Merge branch 'main' of https://github.com/coder/coder into bq/refacto…
BrunoQuaresma Feb 11, 2025
d66141e
Display all daemon data from server
BrunoQuaresma Feb 11, 2025
49a7ec7
Remove unused imports
BrunoQuaresma Feb 11, 2025
ffee2ed
Run fmt
BrunoQuaresma Feb 11, 2025
7802636
Add cancel provisioner job
BrunoQuaresma Feb 12, 2025
4f9030f
Run fmt
BrunoQuaresma Feb 12, 2025
22dc7be
Merge branch 'main' of https://github.com/coder/coder into bq/refacto…
BrunoQuaresma Feb 13, 2025
0ff39e5
Merge branch 'main' of https://github.com/coder/coder into bq/refacto…
BrunoQuaresma Feb 14, 2025
5953960
Apply PR reviews
BrunoQuaresma Feb 14, 2025
aabf8df
FMT
BrunoQuaresma Feb 14, 2025
ed61ce7
Reset devcontainer.json
BrunoQuaresma Feb 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Apply PR reviews
  • Loading branch information
BrunoQuaresma committed Feb 14, 2025
commit 59539607bc6cd52c9cd3747c8b4cda674e2aa157
4 changes: 2 additions & 2 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1238,7 +1238,7 @@ class ApiMethods {
};

cancelTemplateVersionBuild = async (
templateVersionId: TypesGen.TemplateVersion["id"],
templateVersionId: string,
): Promise<TypesGen.Response> => {
const response = await this.axios.patch(
`/api/v2/templateversions/${templateVersionId}/cancel`,
Expand All @@ -1248,7 +1248,7 @@ class ApiMethods {
};

cancelTemplateVersionDryRun = async (
templateVersionId: TypesGen.TemplateVersion["id"],
templateVersionId: string,
jobId: string,
): Promise<TypesGen.Response> => {
const response = await this.axios.patch(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { ProvisionerJob } from "api/typesGenerated";
import { Button } from "components/Button/Button";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import {
Tooltip,
TooltipContent,
Expand All @@ -9,22 +8,25 @@ import {
} from "components/Tooltip/Tooltip";
import { BanIcon } from "lucide-react";
import { type FC, useState } from "react";
import { CancelJobConfirmationDialog } from "./CancelJobConfirmationDialog";

const CANCELLABLE = ["pending", "running"];

type CancelJobButtonProps = {
job: ProvisionerJob;
};

export const CancelJobButton: FC<CancelJobButtonProps> = ({ job }) => {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const cancellable = ["pending", "running"].includes(job.status);
const isCancellable = CANCELLABLE.includes(job.status);

return (
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
disabled={!cancellable}
disabled={!isCancellable}
aria-label="Cancel job"
size="icon"
variant="outline"
Expand All @@ -39,16 +41,12 @@ export const CancelJobButton: FC<CancelJobButtonProps> = ({ job }) => {
</Tooltip>
</TooltipProvider>

<ConfirmDialog
type="delete"
onClose={(): void => {
<CancelJobConfirmationDialog
open={isDialogOpen}
job={job}
onClose={() => {
setIsDialogOpen(false);
}}
open={isDialogOpen}
title="Cancel provisioner job"
description={`Are you sure you want to cancel the provisioner job "${job.id}"? This operation will result in the associated workspaces not getting created.`}
confirmText="Confirm"
cancelText="Discard"
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,16 @@ import {
provisionerJobQueryKey,
} from "api/queries/organizations";
import type { ProvisionerJob } from "api/typesGenerated";
import {
ConfirmDialog,
type ConfirmDialogProps,
} from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import type { FC } from "react";
import { useMutation, useQueryClient } from "react-query";

type CancelJobConfirmationDialogProps = Omit<
ConfirmDialogProps,
| "type"
| "title"
| "description"
| "confirmText"
| "cancelText"
| "onConfirm"
| "confirmLoading"
> & {
type CancelJobConfirmationDialogProps = {
open: boolean;
onClose: () => void;
job: ProvisionerJob;
cancelProvisionerJob: typeof API.cancelProvisionerJob;
cancelProvisionerJob?: typeof API.cancelProvisionerJob;
};

export const CancelJobConfirmationDialog: FC<
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import type { FC, HTMLProps } from "react";
import { cn } from "utils/cn";

export const DataGrid: FC<HTMLProps<HTMLDivElement>> = ({
export const DataGrid: FC<HTMLProps<HTMLDListElement>> = ({
className,
...props
}) => {
return (
<div
<dl
{...props}
className={cn([
"grid grid-cols-[auto_1fr] gap-x-4 items-center",
"[&_span:nth-of-type(even)]:text-content-primary [&_span:nth-of-type(even)]:font-mono",
"[&_span:nth-of-type(even)]:leading-[22px]",
"m-0 grid grid-cols-[auto_1fr] gap-x-4 items-center",
"[&_dt]:text-content-primary [&_dt]:font-mono [&_dt]:leading-[22px]",
className,
])}
/>
Expand All @@ -22,7 +21,5 @@ export const DataGridSpace: FC<HTMLProps<HTMLDivElement>> = ({
className,
...props
}) => {
return (
<div aria-hidden {...props} className={cn(["h-6 col-span-2", className])} />
);
return <div {...props} className={cn(["h-6 col-span-2", className])} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,40 +11,50 @@ import {
import { TriangleAlertIcon } from "lucide-react";
import type { FC } from "react";

const variantByStatus: Record<
ProvisionerJobStatus,
StatusIndicatorProps["variant"]
> = {
succeeded: "success",
failed: "failed",
pending: "pending",
running: "pending",
canceling: "pending",
canceled: "inactive",
unknown: "inactive",
};

type JobStatusIndicatorProps = {
job: ProvisionerJob | ProvisionerDaemonJob;
job: ProvisionerJob;
};

export const JobStatusIndicator: FC<JobStatusIndicatorProps> = ({ job }) => {
const isProvisionerJob = "queue_position" in job;
return (
<StatusIndicator size="sm" variant={statusIndicatorVariant(job.status)}>
<StatusIndicator size="sm" variant={variantByStatus[job.status]}>
<StatusIndicatorDot />
<span className="[&:first-letter]:uppercase">{job.status}</span>
{job.status === "failed" && (
<TriangleAlertIcon className="size-icon-xs p-[1px]" />
)}
{job.status === "pending" &&
isProvisionerJob &&
`(${job.queue_position}/${job.queue_size})`}
{job.status === "pending" && `(${job.queue_position}/${job.queue_size})`}
</StatusIndicator>
);
};

function statusIndicatorVariant(
status: ProvisionerJobStatus,
): StatusIndicatorProps["variant"] {
switch (status) {
case "succeeded":
return "success";
case "failed":
return "failed";
case "pending":
case "running":
case "canceling":
return "pending";
case "canceled":
case "unknown":
return "inactive";
}
}
type DaemonJobStatusIndicatorProps = {
job: ProvisionerDaemonJob;
};

export const DaemonJobStatusIndicator: FC<DaemonJobStatusIndicatorProps> = ({
job,
}) => {
return (
<StatusIndicator size="sm" variant={variantByStatus[job.status]}>
<StatusIndicatorDot />
<span className="[&:first-letter]:uppercase">{job.status}</span>
{job.status === "failed" && (
<TriangleAlertIcon className="size-icon-xs p-[1px]" />
)}
</StatusIndicator>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,26 @@ import { cn } from "utils/cn";
import { docs } from "utils/docs";
import { relativeTime } from "utils/time";
import { DataGrid, DataGridSpace } from "./DataGrid";
import { JobStatusIndicator } from "./JobStatusIndicator";
import { ShrinkTags, Tag, Tags } from "./Tags";
import { DaemonJobStatusIndicator } from "./JobStatusIndicator";
import { TruncateTags, Tag, Tags } from "./Tags";
import { Button } from "components/Button/Button";

type ProvisionerDaemonsPageProps = {
org: Organization;
orgId: string;
};

export const ProvisionerDaemonsPage: FC<ProvisionerDaemonsPageProps> = ({
org,
orgId,
}) => {
const { data: daemons, isLoadingError } = useQuery({
...provisionerDaemons(org.id),
const {
data: daemons,
isLoadingError,
refetch,
} = useQuery({
...provisionerDaemons(orgId),
select: (data) =>
data.toSorted((a, b) => {
if (!a.last_seen_at && !b.last_seen_at) return 0;
if (!a.last_seen_at) return 1;
if (!b.last_seen_at) return -1;
return (
Expand All @@ -49,6 +55,7 @@ export const ProvisionerDaemonsPage: FC<ProvisionerDaemonsPageProps> = ({

return (
<section className="flex flex-col gap-8">
<h2 className="sr-only">Provisioner daemons</h2>
<p className="text-sm text-content-secondary m-0 mt-2">
Coder server runs provisioner daemons which execute terraform during
workspace and template builds.{" "}
Expand Down Expand Up @@ -85,7 +92,10 @@ export const ProvisionerDaemonsPage: FC<ProvisionerDaemonsPageProps> = ({
) : isLoadingError ? (
<TableRow>
<TableCell colSpan={999}>
<EmptyState message="Error loading the provisioner daemons" />
<EmptyState
message="Error loading the provisioner daemons"
cta={<Button onClick={() => refetch()}>Retry</Button>}
/>
</TableCell>
</TableRow>
) : (
Expand Down Expand Up @@ -128,6 +138,7 @@ const DaemonRow: FC<DaemonRowProps> = ({ daemon }) => {
) : (
<ChevronRightIcon className="size-icon-sm p-0.5" />
)}
<span className="sr-only">({isOpen ? "Hide" : "Show more"})</span>
<span className="[&:first-letter]:uppercase">
{relativeTime(
new Date(daemon.last_seen_at ?? new Date().toISOString()),
Expand Down Expand Up @@ -159,7 +170,7 @@ const DaemonRow: FC<DaemonRowProps> = ({ daemon }) => {
)}
</TableCell>
<TableCell>
<ShrinkTags tags={daemon.tags} />
<TruncateTags tags={daemon.tags} />
</TableCell>
<TableCell>
<StatusIndicator size="sm" variant={statusIndicatorVariant(daemon)}>
Expand All @@ -175,49 +186,49 @@ const DaemonRow: FC<DaemonRowProps> = ({ daemon }) => {
<TableRow>
<TableCell colSpan={999} className="p-4 border-t-0">
<DataGrid>
<span>Last seen:</span>
<span>{daemon.last_seen_at}</span>
<dt>Last seen:</dt>
<dd>{daemon.last_seen_at}</dd>

<span>Creation time:</span>
<span>{daemon.created_at}</span>
<dt>Creation time:</dt>
<dd>{daemon.created_at}</dd>

<span>Version:</span>
<span>{daemon.version}</span>
<dt>Version:</dt>
<dd>{daemon.version}</dd>

<span>Tags:</span>
<span>
<dt>Tags:</dt>
<dd>
<Tags>
{Object.entries(daemon.tags).map(([key, value]) => (
<Tag key={key} label={key} value={value} />
))}
</Tags>
</span>
</dd>

{daemon.current_job && (
<>
<DataGridSpace />

<span>Last job:</span>
<span>{daemon.current_job.id}</span>
<dt>Last job:</dt>
<dd>{daemon.current_job.id}</dd>

<span>Last job state:</span>
<span>
<JobStatusIndicator job={daemon.current_job} />
</span>
<dt>Last job state:</dt>
<dd>
<DaemonJobStatusIndicator job={daemon.current_job} />
</dd>
</>
)}

{daemon.previous_job && (
<>
<DataGridSpace />

<span>Previous job:</span>
<span>{daemon.previous_job.id}</span>
<dt>Previous job:</dt>
<dd>{daemon.previous_job.id}</dd>

<span>Previous job state:</span>
<span>
<JobStatusIndicator job={daemon.previous_job} />
</span>
<dt>Previous job state:</dt>
<dd>
<DaemonJobStatusIndicator job={daemon.previous_job} />
</dd>
</>
)}
</DataGrid>
Expand All @@ -240,8 +251,7 @@ function statusIndicatorVariant(
return "success";
case "busy":
return "pending";
case "offline":
case null:
default:
return "inactive";
}
}
Expand All @@ -258,7 +268,7 @@ function statusLabel(daemon: ProvisionerDaemon) {
return "Busy...";
case "offline":
return "Disconnected";
case null:
default:
return "Unknown";
}
}
Loading
Loading