Skip to content

Commit 64e62f7

Browse files
committed
feat: add link to provisioner jobs and daemons
1 parent cbc699b commit 64e62f7

File tree

11 files changed

+231
-68
lines changed

11 files changed

+231
-68
lines changed

site/src/api/api.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,16 @@ export class MissingBuildParameters extends Error {
398398
export type GetProvisionerJobsParams = {
399399
status?: TypesGen.ProvisionerJobStatus;
400400
limit?: number;
401+
// IDs separated by comma
402+
ids?: string;
403+
};
404+
405+
export type GetProvisionerDaemonsParams = {
406+
// IDs separated by comma
407+
ids?: string;
408+
// JSON Object
409+
tags?: string;
410+
limit?: number;
401411
};
402412

403413
/**
@@ -711,22 +721,13 @@ class ApiMethods {
711721
return response.data;
712722
};
713723

714-
/**
715-
* @param organization Can be the organization's ID or name
716-
* @param tags to filter provisioner daemons by.
717-
*/
718724
getProvisionerDaemonsByOrganization = async (
719725
organization: string,
720-
tags?: Record<string, string>,
726+
params?: GetProvisionerDaemonsParams,
721727
): Promise<TypesGen.ProvisionerDaemon[]> => {
722-
const params = new URLSearchParams();
723-
724-
if (tags) {
725-
params.append("tags", JSON.stringify(tags));
726-
}
727-
728728
const response = await this.axios.get<TypesGen.ProvisionerDaemon[]>(
729-
`/api/v2/organizations/${organization}/provisionerdaemons?${params}`,
729+
`/api/v2/organizations/${organization}/provisionerdaemons`,
730+
{ params },
730731
);
731732
return response.data;
732733
};

site/src/api/queries/organizations.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { API, type GetProvisionerJobsParams } from "api/api";
1+
import {
2+
API,
3+
type GetProvisionerDaemonsParams,
4+
type GetProvisionerJobsParams,
5+
} from "api/api";
26
import type {
37
CreateOrganizationRequest,
48
GroupSyncSettings,
@@ -164,16 +168,17 @@ export const organizations = () => {
164168

165169
export const getProvisionerDaemonsKey = (
166170
organization: string,
167-
tags?: Record<string, string>,
168-
) => ["organization", organization, tags, "provisionerDaemons"];
171+
params?: GetProvisionerDaemonsParams,
172+
) => ["organization", organization, "provisionerDaemons", params];
169173

170174
export const provisionerDaemons = (
171175
organization: string,
172-
tags?: Record<string, string>,
176+
params?: GetProvisionerDaemonsParams,
173177
) => {
174178
return {
175-
queryKey: getProvisionerDaemonsKey(organization, tags),
176-
queryFn: () => API.getProvisionerDaemonsByOrganization(organization, tags),
179+
queryKey: getProvisionerDaemonsKey(organization, params),
180+
queryFn: () =>
181+
API.getProvisionerDaemonsByOrganization(organization, params),
177182
};
178183
};
179184

site/src/components/Button/Button.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { forwardRef } from "react";
88
import { cn } from "utils/cn";
99

1010
export const buttonVariants = cva(
11-
`inline-flex items-center justify-center gap-1 whitespace-nowrap
11+
`inline-flex items-center justify-center gap-1 whitespace-nowrap font-sans
1212
border-solid rounded-md transition-colors
1313
text-sm font-semibold font-medium cursor-pointer no-underline
1414
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link
@@ -30,6 +30,7 @@ export const buttonVariants = cva(
3030
size: {
3131
lg: "min-w-20 h-10 px-3 py-2 [&_svg]:size-icon-lg",
3232
sm: "min-w-20 h-8 px-2 py-1.5 text-xs [&_svg]:size-icon-sm",
33+
xs: "min-w-8 py-1 px-2 text-2xs rounded-md",
3334
icon: "size-8 px-1.5 [&_svg]:size-icon-sm",
3435
"icon-lg": "size-10 px-2 [&_svg]:size-icon-lg",
3536
},

site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@ import { type FC, useState } from "react";
1818
import { cn } from "utils/cn";
1919
import { relativeTime } from "utils/time";
2020
import { CancelJobButton } from "./CancelJobButton";
21+
import { Link as RouterLink } from "react-router-dom";
2122

2223
type JobRowProps = {
2324
job: ProvisionerJob;
25+
defaultIsOpen: boolean;
2426
};
2527

26-
export const JobRow: FC<JobRowProps> = ({ job }) => {
28+
export const JobRow: FC<JobRowProps> = ({ job, defaultIsOpen = false }) => {
2729
const metadata = job.metadata;
28-
const [isOpen, setIsOpen] = useState(false);
30+
const [isOpen, setIsOpen] = useState(defaultIsOpen);
2931
const queue = {
3032
size: job.queue_size,
3133
position: job.queue_position,
@@ -114,19 +116,36 @@ export const JobRow: FC<JobRowProps> = ({ job }) => {
114116
: "[]"}
115117
</dd>
116118

117-
<dt>Completed by provisioner:</dt>
118-
<dd>{job.worker_id}</dd>
119+
{job.worker_id && (
120+
<>
121+
<dt>Completed by provisioner:</dt>
122+
<dd className="flex items-center gap-2">
123+
<span>{job.worker_id}</span>
124+
<Button size="xs" variant="outline" asChild>
125+
<RouterLink
126+
to={`../provisioners?${new URLSearchParams({ ids: job.worker_id })}`}
127+
>
128+
View provisioner
129+
</RouterLink>
130+
</Button>
131+
</dd>
132+
</>
133+
)}
119134

120135
<dt>Associated workspace:</dt>
121136
<dd>{job.metadata.workspace_name ?? "null"}</dd>
122137

123138
<dt>Creation time:</dt>
124139
<dd data-chromatic="ignore">{job.created_at}</dd>
125140

126-
<dt>Queue:</dt>
127-
<dd>
128-
{job.queue_position}/{job.queue_size}
129-
</dd>
141+
{job.queue_position > 0 && (
142+
<>
143+
<dt>Queue:</dt>
144+
<dd>
145+
{job.queue_position}/{job.queue_size}
146+
</dd>
147+
</>
148+
)}
130149

131150
<dt>Tags:</dt>
132151
<dd>

site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@ const OrganizationProvisionerJobsPage: FC = () => {
1212
const [searchParams, setSearchParams] = useSearchParams();
1313
const filter = {
1414
status: searchParams.get("status") || "",
15+
ids: searchParams.get("ids") || "",
1516
};
16-
const queryParams = {
17-
...filter,
18-
limit: 100,
19-
} as GetProvisionerJobsParams;
2017
const {
2118
data: jobs,
2219
isLoadingError,
2320
refetch,
2421
} = useQuery({
25-
...provisionerJobs(organization?.id || "", queryParams),
22+
...provisionerJobs(organization?.id || "", {
23+
...filter,
24+
limit: 100,
25+
} as GetProvisionerJobsParams),
2626
enabled: organization !== undefined,
2727
});
2828

site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const meta: Meta<typeof OrganizationProvisionerJobsPageView> = {
2121
args: {
2222
organization: MockOrganization,
2323
jobs: MockProvisionerJobs,
24-
filter: { status: "" },
24+
filter: { status: "", ids: "" },
2525
onRetry: fn(),
2626
},
2727
};
@@ -81,8 +81,8 @@ export const Empty: Story = {
8181
export const OnFilter: Story = {
8282
render: function FilterWithState({ ...args }) {
8383
const [jobs, setJobs] = useState<ProvisionerJob[]>([]);
84-
const [filter, setFilter] = useState({ status: "pending" });
85-
const handleFilterChange = (newFilter: { status: string }) => {
84+
const [filter, setFilter] = useState({ status: "pending", ids: "" });
85+
const handleFilterChange = (newFilter: { status: string; ids: string }) => {
8686
setFilter(newFilter);
8787
const filteredJobs = MockProvisionerJobs.filter((job) =>
8888
newFilter.status ? job.status === newFilter.status : true,
@@ -109,3 +109,13 @@ export const OnFilter: Story = {
109109
await userEvent.click(option);
110110
},
111111
};
112+
113+
export const FilterByID: Story = {
114+
args: {
115+
jobs: [MockProvisionerJob],
116+
filter: {
117+
ids: MockProvisionerJob.id,
118+
status: "",
119+
},
120+
},
121+
};

site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx

Lines changed: 72 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ import { Helmet } from "react-helmet-async";
3838
import { docs } from "utils/docs";
3939
import { pageTitle } from "utils/page";
4040
import { JobRow } from "./JobRow";
41+
import { Badge } from "components/Badge/Badge";
42+
import { XIcon } from "lucide-react";
43+
import {
44+
Tooltip,
45+
TooltipProvider,
46+
TooltipTrigger,
47+
TooltipContent,
48+
} from "components/Tooltip/Tooltip";
4149

4250
const variantByStatus: Record<
4351
ProvisionerJobStatus,
@@ -64,6 +72,7 @@ const StatusFilters: ProvisionerJobStatus[] = [
6472

6573
type JobProvisionersFilter = {
6674
status: string;
75+
ids: string;
6776
};
6877

6978
type OrganizationProvisionerJobsPageViewProps = {
@@ -110,30 +119,62 @@ const OrganizationProvisionerJobsPageView: FC<
110119
</SettingsHeaderDescription>
111120
</SettingsHeader>
112121

113-
<Select
114-
value={filter.status}
115-
onValueChange={(status) => {
116-
onFilterChange({ status: status as ProvisionerJobStatus });
117-
}}
118-
>
119-
<SelectTrigger className="w-[180px]" data-testid="status-filter">
120-
<SelectValue placeholder="All statuses" />
121-
</SelectTrigger>
122-
<SelectContent>
123-
<SelectGroup>
124-
{StatusFilters.map((status) => (
125-
<SelectItem key={status} value={status}>
126-
<StatusIndicator variant={variantByStatus[status]}>
127-
<StatusIndicatorDot />
128-
<span className="block first-letter:uppercase">
129-
{status}
130-
</span>
131-
</StatusIndicator>
132-
</SelectItem>
133-
))}
134-
</SelectGroup>
135-
</SelectContent>
136-
</Select>
122+
<div className="flex items-center gap-2">
123+
{filter.ids && (
124+
<div className="relative">
125+
<Badge className="h-10 text-sm pl-3 pr-10 font-mono">
126+
{filter.ids}
127+
</Badge>
128+
<div className="size-10 flex items-center justify-center absolute top-0 right-0">
129+
<TooltipProvider>
130+
<Tooltip>
131+
<TooltipTrigger asChild>
132+
<Button
133+
size="icon"
134+
variant="subtle"
135+
onClick={() => {
136+
onFilterChange({ ...filter, ids: "" });
137+
}}
138+
>
139+
<span className="sr-only">Clear ID</span>
140+
<XIcon />
141+
</Button>
142+
</TooltipTrigger>
143+
<TooltipContent>Clear ID</TooltipContent>
144+
</Tooltip>
145+
</TooltipProvider>
146+
</div>
147+
</div>
148+
)}
149+
150+
<Select
151+
value={filter.status}
152+
onValueChange={(status) => {
153+
onFilterChange({
154+
...filter,
155+
status: status as ProvisionerJobStatus,
156+
});
157+
}}
158+
>
159+
<SelectTrigger className="w-[180px]" data-testid="status-filter">
160+
<SelectValue placeholder="All statuses" />
161+
</SelectTrigger>
162+
<SelectContent>
163+
<SelectGroup>
164+
{StatusFilters.map((status) => (
165+
<SelectItem key={status} value={status}>
166+
<StatusIndicator variant={variantByStatus[status]}>
167+
<StatusIndicatorDot />
168+
<span className="block first-letter:uppercase">
169+
{status}
170+
</span>
171+
</StatusIndicator>
172+
</SelectItem>
173+
))}
174+
</SelectGroup>
175+
</SelectContent>
176+
</Select>
177+
</div>
137178

138179
<Table className="mt-6">
139180
<TableHeader>
@@ -149,7 +190,13 @@ const OrganizationProvisionerJobsPageView: FC<
149190
<TableBody>
150191
{jobs ? (
151192
jobs.length > 0 ? (
152-
jobs.map((j) => <JobRow key={j.id} job={j} />)
193+
jobs.map((j) => (
194+
<JobRow
195+
defaultIsOpen={filter.ids.includes(j.id)}
196+
key={j.id}
197+
job={j}
198+
/>
199+
))
153200
) : (
154201
<TableRow>
155202
<TableCell colSpan={999}>

site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPage.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,28 @@ import { RequirePermission } from "modules/permissions/RequirePermission";
88
import type { FC } from "react";
99
import { Helmet } from "react-helmet-async";
1010
import { useQuery } from "react-query";
11-
import { useParams } from "react-router-dom";
11+
import { useParams, useSearchParams } from "react-router-dom";
1212
import { pageTitle } from "utils/page";
1313
import { OrganizationProvisionersPageView } from "./OrganizationProvisionersPageView";
1414

1515
const OrganizationProvisionersPage: FC = () => {
1616
const { organization: organizationName } = useParams() as {
1717
organization: string;
1818
};
19+
const [searchParams, setSearchParams] = useSearchParams();
20+
const queryParams = {
21+
ids: searchParams.get("ids") || "",
22+
tags: searchParams.get("tags") || "",
23+
};
1924
const { organization, organizationPermissions } = useOrganizationSettings();
2025
const { entitlements } = useDashboard();
2126
const { metadata } = useEmbeddedMetadata();
2227
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
2328
const provisionersQuery = useQuery({
24-
...provisionerDaemons(organizationName),
25-
select: (provisioners) =>
26-
provisioners.filter((p) => p.status !== "offline"),
29+
...provisionerDaemons(organizationName, {
30+
...queryParams,
31+
limit: 100,
32+
}),
2733
});
2834

2935
if (!organization) {
@@ -59,6 +65,8 @@ const OrganizationProvisionersPage: FC = () => {
5965
provisioners={provisionersQuery.data}
6066
buildVersion={buildInfoQuery.data?.version}
6167
onRetry={provisionersQuery.refetch}
68+
filter={queryParams}
69+
onFilterChange={setSearchParams}
6270
/>
6371
</>
6472
);

site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.stories.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,12 @@ export const Paywall: Story = {
6060
showPaywall: true,
6161
},
6262
};
63+
64+
export const FilterByID: Story = {
65+
args: {
66+
provisioners: [MockProvisioner],
67+
filter: {
68+
ids: MockProvisioner.id,
69+
},
70+
},
71+
};

0 commit comments

Comments
 (0)