From ec99e7966b81832ad362d78a1aae9a27ec8874a7 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 1 Apr 2025 19:06:35 +0000 Subject: [PATCH 1/4] feat: add job status filter --- site/src/api/api.ts | 11 ++- site/src/api/queries/organizations.ts | 13 +++- .../CancelJobConfirmationDialog.tsx | 4 +- .../JobRow.tsx | 24 +++--- .../OrganizationProvisionerJobsPage.tsx | 13 +++- ...izationProvisionerJobsPageView.stories.tsx | 33 ++++++++ .../OrganizationProvisionerJobsPageView.tsx | 77 ++++++++++++++++++- 7 files changed, 154 insertions(+), 21 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 3a43772a02657..c3b202660ad9b 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -2392,9 +2392,16 @@ class ApiMethods { return res.data; }; - getProvisionerJobs = async (orgId: string) => { + getProvisionerJobs = async ( + orgId: string, + status?: TypesGen.ProvisionerJobStatus, + ) => { + const params = new URLSearchParams(); + if (status) { + params.append("status", status); + } const res = await this.axios.get( - `/api/v2/organizations/${orgId}/provisionerjobs`, + `/api/v2/organizations/${orgId}/provisionerjobs?${params.toString()}`, ); return res.data; }; diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 2dc0402d75484..a69312c87f974 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -4,6 +4,7 @@ import type { GroupSyncSettings, PaginatedMembersRequest, PaginatedMembersResponse, + ProvisionerJobStatus, RoleSyncSettings, UpdateOrganizationRequest, } from "api/typesGenerated"; @@ -236,16 +237,20 @@ export const patchRoleSyncSettings = ( }; }; -export const provisionerJobQueryKey = (orgId: string) => [ +export const provisionerJobsQueryKey = (orgId: string, status?: string) => [ "organization", orgId, "provisionerjobs", + status, ]; -export const provisionerJobs = (orgId: string) => { +export const provisionerJobs = ( + orgId: string, + status?: ProvisionerJobStatus, +) => { return { - queryKey: provisionerJobQueryKey(orgId), - queryFn: () => API.getProvisionerJobs(orgId), + queryKey: provisionerJobsQueryKey(orgId, status), + queryFn: () => API.getProvisionerJobs(orgId, status), }; }; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/CancelJobConfirmationDialog.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/CancelJobConfirmationDialog.tsx index 573f7090a1ebb..a8ee325e391e5 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/CancelJobConfirmationDialog.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/CancelJobConfirmationDialog.tsx @@ -1,7 +1,7 @@ import { API } from "api/api"; import { getProvisionerDaemonsKey, - provisionerJobQueryKey, + provisionerJobsQueryKey, } from "api/queries/organizations"; import type { ProvisionerJob } from "api/typesGenerated"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; @@ -28,7 +28,7 @@ export const CancelJobConfirmationDialog: FC< mutationFn: cancelProvisionerJob, onSuccess: () => { queryClient.invalidateQueries( - provisionerJobQueryKey(job.organization_id), + provisionerJobsQueryKey(job.organization_id), ); queryClient.invalidateQueries( getProvisionerDaemonsKey(job.organization_id, job.tags), diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx index 9c7aecbba5c14..bda73cb2e3688 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx @@ -52,16 +52,20 @@ export const JobRow: FC = ({ job }) => { {job.type} -
- - {metadata.template_display_name || metadata.template_name} -
+ {job.metadata.template_name !== "" ? ( +
+ + {metadata.template_display_name || metadata.template_name} +
+ ) : ( + - + )}
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx index bae561c4a9ee3..f843a2b8e417f 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx @@ -1,26 +1,37 @@ import { provisionerJobs } from "api/queries/organizations"; +import type { ProvisionerJobStatus } from "api/typesGenerated"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; import type { FC } from "react"; import { useQuery } from "react-query"; +import { useSearchParams } from "react-router-dom"; import OrganizationProvisionerJobsPageView from "./OrganizationProvisionerJobsPageView"; const OrganizationProvisionerJobsPage: FC = () => { const { organization } = useOrganizationSettings(); + const [params, setParams] = useSearchParams(); + const filter = { + status: params.get("status") || "", + }; const { data: jobs, isLoadingError, refetch, } = useQuery({ - ...provisionerJobs(organization?.id || ""), + ...provisionerJobs( + organization?.id || "", + filter.status as ProvisionerJobStatus, + ), enabled: organization !== undefined, }); return ( ); }; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx index 9b6a25a3521ef..6fd7425a7dd22 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { expect, fn, userEvent, waitFor, within } from "@storybook/test"; import type { ProvisionerJob } from "api/typesGenerated"; +import { useState } from "react"; import { MockOrganization, MockProvisionerJob } from "testHelpers/entities"; import { daysAgo } from "utils/time"; import OrganizationProvisionerJobsPageView from "./OrganizationProvisionerJobsPageView"; @@ -75,3 +76,35 @@ export const Empty: Story = { jobs: [], }, }; + +export const OnFilter: Story = { + render: function FilterWithState({ ...args }) { + const [jobs, setJobs] = useState([]); + const [filter, setFilter] = useState({ status: "pending" }); + const handleFilterChange = (newFilter: { status: string }) => { + setFilter(newFilter); + const filteredJobs = MockProvisionerJobs.filter((job) => + newFilter.status ? job.status === newFilter.status : true, + ); + setJobs(filteredJobs); + }; + + return ( + + ); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const statusFilter = canvas.getByTestId("status-filter"); + await userEvent.click(statusFilter); + + const body = within(canvasElement.ownerDocument.body); + const option = await body.findByRole("option", { name: "succeeded" }); + await userEvent.click(option); + }, +}; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx index 98168ef39adb8..e77b3933e73c8 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx @@ -1,8 +1,25 @@ -import type { Organization, ProvisionerJob } from "api/typesGenerated"; +import type { + Organization, + ProvisionerJob, + ProvisionerJobStatus, +} from "api/typesGenerated"; import { Button } from "components/Button/Button"; import { EmptyState } from "components/EmptyState/EmptyState"; import { Link } from "components/Link/Link"; import { Loader } from "components/Loader/Loader"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "components/Select/Select"; +import { + StatusIndicator, + StatusIndicatorDot, + type StatusIndicatorProps, +} from "components/StatusIndicator/StatusIndicator"; import { Table, TableBody, @@ -17,16 +34,45 @@ import { docs } from "utils/docs"; import { pageTitle } from "utils/page"; import { JobRow } from "./JobRow"; +const variantByStatus: Record< + ProvisionerJobStatus, + StatusIndicatorProps["variant"] +> = { + succeeded: "success", + failed: "failed", + pending: "pending", + running: "pending", + canceling: "pending", + canceled: "inactive", + unknown: "inactive", +}; + +const StatusFilters: ProvisionerJobStatus[] = [ + "succeeded", + "pending", + "running", + "canceling", + "canceled", + "failed", + "unknown", +]; + +type JobProvisionersFilter = { + status: string; +}; + type OrganizationProvisionerJobsPageViewProps = { jobs: ProvisionerJob[] | undefined; organization: Organization | undefined; error: unknown; + filter: JobProvisionersFilter; onRetry: () => void; + onFilterChange: (filter: JobProvisionersFilter) => void; }; const OrganizationProvisionerJobsPageView: FC< OrganizationProvisionerJobsPageViewProps -> = ({ jobs, organization, error, onRetry }) => { +> = ({ jobs, organization, error, filter, onFilterChange, onRetry }) => { if (!organization) { return ( <> @@ -61,6 +107,33 @@ const OrganizationProvisionerJobsPageView: FC< +
+ +
+ From 131983ee704dfc759778205c4dd744a6f67edad9 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 2 Apr 2025 12:32:59 +0000 Subject: [PATCH 2/4] Fix storybook --- .../OrganizationProvisionerJobsPageView.stories.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx index 6fd7425a7dd22..a5837cf527fc2 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx @@ -21,6 +21,7 @@ const meta: Meta = { args: { organization: MockOrganization, jobs: MockProvisionerJobs, + filter: { status: "" }, onRetry: fn(), }, }; From 6f426b3b98ea7bc69eb8c54e5f1da59fee36761a Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 2 Apr 2025 13:02:18 +0000 Subject: [PATCH 3/4] Load 100 jobs --- site/src/api/api.ts | 14 ++++++++------ site/src/api/queries/organizations.ts | 18 ++++++++---------- .../OrganizationProvisionerJobsPage.tsx | 16 +++++++++------- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index c3b202660ad9b..7fd54e5b6a1ef 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -397,6 +397,11 @@ export class MissingBuildParameters extends Error { } } +export type GetProvisionerJobsParams = { + status?: TypesGen.ProvisionerJobStatus; + limit?: number; +}; + /** * This is the container for all API methods. It's split off to make it more * clear where API methods should go, but it is eventually merged into the Api @@ -2394,14 +2399,11 @@ class ApiMethods { getProvisionerJobs = async ( orgId: string, - status?: TypesGen.ProvisionerJobStatus, + params: GetProvisionerJobsParams = {}, ) => { - const params = new URLSearchParams(); - if (status) { - params.append("status", status); - } const res = await this.axios.get( - `/api/v2/organizations/${orgId}/provisionerjobs?${params.toString()}`, + `/api/v2/organizations/${orgId}/provisionerjobs`, + { params }, ); return res.data; }; diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index a69312c87f974..b0aa698df7d3d 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -1,4 +1,4 @@ -import { API } from "api/api"; +import { API, type GetProvisionerJobsParams } from "api/api"; import type { CreateOrganizationRequest, GroupSyncSettings, @@ -237,20 +237,18 @@ export const patchRoleSyncSettings = ( }; }; -export const provisionerJobsQueryKey = (orgId: string, status?: string) => [ - "organization", - orgId, - "provisionerjobs", - status, -]; +export const provisionerJobsQueryKey = ( + orgId: string, + params: GetProvisionerJobsParams = {}, +) => ["organization", orgId, "provisionerjobs", params]; export const provisionerJobs = ( orgId: string, - status?: ProvisionerJobStatus, + params: GetProvisionerJobsParams = {}, ) => { return { - queryKey: provisionerJobsQueryKey(orgId, status), - queryFn: () => API.getProvisionerJobs(orgId, status), + queryKey: provisionerJobsQueryKey(orgId, params), + queryFn: () => API.getProvisionerJobs(orgId, params), }; }; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx index f843a2b8e417f..1032cd9f8d5c7 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx @@ -5,22 +5,24 @@ import type { FC } from "react"; import { useQuery } from "react-query"; import { useSearchParams } from "react-router-dom"; import OrganizationProvisionerJobsPageView from "./OrganizationProvisionerJobsPageView"; +import type { GetProvisionerJobsParams } from "api/api"; const OrganizationProvisionerJobsPage: FC = () => { const { organization } = useOrganizationSettings(); - const [params, setParams] = useSearchParams(); + const [searchParams, setSearchParams] = useSearchParams(); const filter = { - status: params.get("status") || "", + status: searchParams.get("status") || "", }; + const queryParams = { + ...filter, + limit: 100, + } as GetProvisionerJobsParams; const { data: jobs, isLoadingError, refetch, } = useQuery({ - ...provisionerJobs( - organization?.id || "", - filter.status as ProvisionerJobStatus, - ), + ...provisionerJobs(organization?.id || "", queryParams), enabled: organization !== undefined, }); @@ -31,7 +33,7 @@ const OrganizationProvisionerJobsPage: FC = () => { organization={organization} error={isLoadingError} onRetry={refetch} - onFilterChange={setParams} + onFilterChange={setSearchParams} /> ); }; From 079d266ce4b1b89916675b866194b0d502a62ddb Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 4 Apr 2025 12:55:09 +0000 Subject: [PATCH 4/4] FMT --- .../OrganizationProvisionerJobsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx index 1032cd9f8d5c7..8602fe0c23727 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx @@ -1,3 +1,4 @@ +import type { GetProvisionerJobsParams } from "api/api"; import { provisionerJobs } from "api/queries/organizations"; import type { ProvisionerJobStatus } from "api/typesGenerated"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; @@ -5,7 +6,6 @@ import type { FC } from "react"; import { useQuery } from "react-query"; import { useSearchParams } from "react-router-dom"; import OrganizationProvisionerJobsPageView from "./OrganizationProvisionerJobsPageView"; -import type { GetProvisionerJobsParams } from "api/api"; const OrganizationProvisionerJobsPage: FC = () => { const { organization } = useOrganizationSettings();