Skip to content

Commit 3bfafe3

Browse files
1 parent 43d584c commit 3bfafe3

File tree

7 files changed

+161
-25
lines changed

7 files changed

+161
-25
lines changed

site/src/api/api.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,11 @@ export class MissingBuildParameters extends Error {
400400
}
401401
}
402402

403+
export type GetProvisionerJobsParams = {
404+
status?: TypesGen.ProvisionerJobStatus;
405+
limit?: number;
406+
};
407+
403408
/**
404409
* This is the container for all API methods. It's split off to make it more
405410
* clear where API methods should go, but it is eventually merged into the Api
@@ -2395,9 +2400,13 @@ class ApiMethods {
23952400
return res.data;
23962401
};
23972402

2398-
getProvisionerJobs = async (orgId: string) => {
2403+
getProvisionerJobs = async (
2404+
orgId: string,
2405+
params: GetProvisionerJobsParams = {},
2406+
) => {
23992407
const res = await this.axios.get<TypesGen.ProvisionerJob[]>(
24002408
`/api/v2/organizations/${orgId}/provisionerjobs`,
2409+
{ params },
24012410
);
24022411
return res.data;
24032412
};

site/src/api/queries/organizations.ts

+12-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { API } from "api/api";
1+
import { API, type GetProvisionerJobsParams } from "api/api";
22
import type {
33
CreateOrganizationRequest,
44
GroupSyncSettings,
55
PaginatedMembersRequest,
66
PaginatedMembersResponse,
7+
ProvisionerJobStatus,
78
RoleSyncSettings,
89
UpdateOrganizationRequest,
910
} from "api/typesGenerated";
@@ -241,16 +242,18 @@ export const patchRoleSyncSettings = (
241242
};
242243
};
243244

244-
export const provisionerJobQueryKey = (orgId: string) => [
245-
"organization",
246-
orgId,
247-
"provisionerjobs",
248-
];
245+
export const provisionerJobsQueryKey = (
246+
orgId: string,
247+
params: GetProvisionerJobsParams = {},
248+
) => ["organization", orgId, "provisionerjobs", params];
249249

250-
export const provisionerJobs = (orgId: string) => {
250+
export const provisionerJobs = (
251+
orgId: string,
252+
params: GetProvisionerJobsParams = {},
253+
) => {
251254
return {
252-
queryKey: provisionerJobQueryKey(orgId),
253-
queryFn: () => API.getProvisionerJobs(orgId),
255+
queryKey: provisionerJobsQueryKey(orgId, params),
256+
queryFn: () => API.getProvisionerJobs(orgId, params),
254257
};
255258
};
256259

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { API } from "api/api";
22
import {
33
getProvisionerDaemonsKey,
4-
provisionerJobQueryKey,
4+
provisionerJobsQueryKey,
55
} from "api/queries/organizations";
66
import type { ProvisionerJob } from "api/typesGenerated";
77
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
@@ -28,7 +28,7 @@ export const CancelJobConfirmationDialog: FC<
2828
mutationFn: cancelProvisionerJob,
2929
onSuccess: () => {
3030
queryClient.invalidateQueries(
31-
provisionerJobQueryKey(job.organization_id),
31+
provisionerJobsQueryKey(job.organization_id),
3232
);
3333
queryClient.invalidateQueries(
3434
getProvisionerDaemonsKey(job.organization_id, job.tags),

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

+14-10
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,20 @@ export const JobRow: FC<JobRowProps> = ({ job }) => {
5252
<Badge size="sm">{job.type}</Badge>
5353
</TableCell>
5454
<TableCell>
55-
<div className="flex items-center gap-1 whitespace-nowrap">
56-
<Avatar
57-
variant="icon"
58-
src={metadata.template_icon}
59-
fallback={
60-
metadata.template_display_name || metadata.template_name
61-
}
62-
/>
63-
{metadata.template_display_name || metadata.template_name}
64-
</div>
55+
{job.metadata.template_name !== "" ? (
56+
<div className="flex items-center gap-1 whitespace-nowrap">
57+
<Avatar
58+
variant="icon"
59+
src={metadata.template_icon}
60+
fallback={
61+
metadata.template_display_name || metadata.template_name
62+
}
63+
/>
64+
{metadata.template_display_name || metadata.template_name}
65+
</div>
66+
) : (
67+
<span>-</span>
68+
)}
6569
</TableCell>
6670
<TableCell>
6771
<TruncateTags tags={job.tags} />

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

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,39 @@
1+
import type { GetProvisionerJobsParams } from "api/api";
12
import { provisionerJobs } from "api/queries/organizations";
3+
import type { ProvisionerJobStatus } from "api/typesGenerated";
24
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
35
import type { FC } from "react";
46
import { useQuery } from "react-query";
7+
import { useSearchParams } from "react-router-dom";
58
import OrganizationProvisionerJobsPageView from "./OrganizationProvisionerJobsPageView";
69

710
const OrganizationProvisionerJobsPage: FC = () => {
811
const { organization } = useOrganizationSettings();
12+
const [searchParams, setSearchParams] = useSearchParams();
13+
const filter = {
14+
status: searchParams.get("status") || "",
15+
};
16+
const queryParams = {
17+
...filter,
18+
limit: 100,
19+
} as GetProvisionerJobsParams;
920
const {
1021
data: jobs,
1122
isLoadingError,
1223
refetch,
1324
} = useQuery({
14-
...provisionerJobs(organization?.id || ""),
25+
...provisionerJobs(organization?.id || "", queryParams),
1526
enabled: organization !== undefined,
1627
});
1728

1829
return (
1930
<OrganizationProvisionerJobsPageView
2031
jobs={jobs}
32+
filter={filter}
2133
organization={organization}
2234
error={isLoadingError}
2335
onRetry={refetch}
36+
onFilterChange={setSearchParams}
2437
/>
2538
);
2639
};

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

+34
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Meta, StoryObj } from "@storybook/react";
22
import { expect, fn, userEvent, waitFor, within } from "@storybook/test";
33
import type { ProvisionerJob } from "api/typesGenerated";
4+
import { useState } from "react";
45
import { MockOrganization, MockProvisionerJob } from "testHelpers/entities";
56
import { daysAgo } from "utils/time";
67
import OrganizationProvisionerJobsPageView from "./OrganizationProvisionerJobsPageView";
@@ -20,6 +21,7 @@ const meta: Meta<typeof OrganizationProvisionerJobsPageView> = {
2021
args: {
2122
organization: MockOrganization,
2223
jobs: MockProvisionerJobs,
24+
filter: { status: "" },
2325
onRetry: fn(),
2426
},
2527
};
@@ -75,3 +77,35 @@ export const Empty: Story = {
7577
jobs: [],
7678
},
7779
};
80+
81+
export const OnFilter: Story = {
82+
render: function FilterWithState({ ...args }) {
83+
const [jobs, setJobs] = useState<ProvisionerJob[]>([]);
84+
const [filter, setFilter] = useState({ status: "pending" });
85+
const handleFilterChange = (newFilter: { status: string }) => {
86+
setFilter(newFilter);
87+
const filteredJobs = MockProvisionerJobs.filter((job) =>
88+
newFilter.status ? job.status === newFilter.status : true,
89+
);
90+
setJobs(filteredJobs);
91+
};
92+
93+
return (
94+
<OrganizationProvisionerJobsPageView
95+
{...args}
96+
filter={filter}
97+
jobs={jobs}
98+
onFilterChange={handleFilterChange}
99+
/>
100+
);
101+
},
102+
play: async ({ canvasElement }) => {
103+
const canvas = within(canvasElement);
104+
const statusFilter = canvas.getByTestId("status-filter");
105+
await userEvent.click(statusFilter);
106+
107+
const body = within(canvasElement.ownerDocument.body);
108+
const option = await body.findByRole("option", { name: "succeeded" });
109+
await userEvent.click(option);
110+
},
111+
};

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

+75-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,25 @@
1-
import type { Organization, ProvisionerJob } from "api/typesGenerated";
1+
import type {
2+
Organization,
3+
ProvisionerJob,
4+
ProvisionerJobStatus,
5+
} from "api/typesGenerated";
26
import { Button } from "components/Button/Button";
37
import { EmptyState } from "components/EmptyState/EmptyState";
48
import { Link } from "components/Link/Link";
59
import { Loader } from "components/Loader/Loader";
10+
import {
11+
Select,
12+
SelectContent,
13+
SelectGroup,
14+
SelectItem,
15+
SelectTrigger,
16+
SelectValue,
17+
} from "components/Select/Select";
18+
import {
19+
StatusIndicator,
20+
StatusIndicatorDot,
21+
type StatusIndicatorProps,
22+
} from "components/StatusIndicator/StatusIndicator";
623
import {
724
Table,
825
TableBody,
@@ -17,16 +34,45 @@ import { docs } from "utils/docs";
1734
import { pageTitle } from "utils/page";
1835
import { JobRow } from "./JobRow";
1936

37+
const variantByStatus: Record<
38+
ProvisionerJobStatus,
39+
StatusIndicatorProps["variant"]
40+
> = {
41+
succeeded: "success",
42+
failed: "failed",
43+
pending: "pending",
44+
running: "pending",
45+
canceling: "pending",
46+
canceled: "inactive",
47+
unknown: "inactive",
48+
};
49+
50+
const StatusFilters: ProvisionerJobStatus[] = [
51+
"succeeded",
52+
"pending",
53+
"running",
54+
"canceling",
55+
"canceled",
56+
"failed",
57+
"unknown",
58+
];
59+
60+
type JobProvisionersFilter = {
61+
status: string;
62+
};
63+
2064
type OrganizationProvisionerJobsPageViewProps = {
2165
jobs: ProvisionerJob[] | undefined;
2266
organization: Organization | undefined;
2367
error: unknown;
68+
filter: JobProvisionersFilter;
2469
onRetry: () => void;
70+
onFilterChange: (filter: JobProvisionersFilter) => void;
2571
};
2672

2773
const OrganizationProvisionerJobsPageView: FC<
2874
OrganizationProvisionerJobsPageViewProps
29-
> = ({ jobs, organization, error, onRetry }) => {
75+
> = ({ jobs, organization, error, filter, onFilterChange, onRetry }) => {
3076
if (!organization) {
3177
return (
3278
<>
@@ -61,6 +107,33 @@ const OrganizationProvisionerJobsPageView: FC<
61107
</div>
62108
</header>
63109

110+
<div>
111+
<Select
112+
value={filter.status}
113+
onValueChange={(status) => {
114+
onFilterChange({ status: status as ProvisionerJobStatus });
115+
}}
116+
>
117+
<SelectTrigger className="w-[180px]" data-testid="status-filter">
118+
<SelectValue placeholder="All statuses" />
119+
</SelectTrigger>
120+
<SelectContent>
121+
<SelectGroup>
122+
{StatusFilters.map((status) => (
123+
<SelectItem key={status} value={status}>
124+
<StatusIndicator variant={variantByStatus[status]}>
125+
<StatusIndicatorDot />
126+
<span className="block first-letter:uppercase">
127+
{status}
128+
</span>
129+
</StatusIndicator>
130+
</SelectItem>
131+
))}
132+
</SelectGroup>
133+
</SelectContent>
134+
</Select>
135+
</div>
136+
64137
<Table>
65138
<TableHeader>
66139
<TableRow>

0 commit comments

Comments
 (0)