diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 3a43772a02657..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 @@ -2392,9 +2397,13 @@ class ApiMethods { return res.data; }; - getProvisionerJobs = async (orgId: string) => { + getProvisionerJobs = async ( + orgId: string, + params: GetProvisionerJobsParams = {}, + ) => { const res = await this.axios.get( `/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 2dc0402d75484..b0aa698df7d3d 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -1,9 +1,10 @@ -import { API } from "api/api"; +import { API, type GetProvisionerJobsParams } from "api/api"; import type { CreateOrganizationRequest, GroupSyncSettings, PaginatedMembersRequest, PaginatedMembersResponse, + ProvisionerJobStatus, RoleSyncSettings, UpdateOrganizationRequest, } from "api/typesGenerated"; @@ -236,16 +237,18 @@ export const patchRoleSyncSettings = ( }; }; -export const provisionerJobQueryKey = (orgId: string) => [ - "organization", - orgId, - "provisionerjobs", -]; +export const provisionerJobsQueryKey = ( + orgId: string, + params: GetProvisionerJobsParams = {}, +) => ["organization", orgId, "provisionerjobs", params]; -export const provisionerJobs = (orgId: string) => { +export const provisionerJobs = ( + orgId: string, + params: GetProvisionerJobsParams = {}, +) => { return { - queryKey: provisionerJobQueryKey(orgId), - queryFn: () => API.getProvisionerJobs(orgId), + queryKey: provisionerJobsQueryKey(orgId, params), + queryFn: () => API.getProvisionerJobs(orgId, params), }; }; 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..8602fe0c23727 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx @@ -1,26 +1,39 @@ +import type { GetProvisionerJobsParams } from "api/api"; 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 [searchParams, setSearchParams] = useSearchParams(); + const filter = { + status: searchParams.get("status") || "", + }; + const queryParams = { + ...filter, + limit: 100, + } as GetProvisionerJobsParams; const { data: jobs, isLoadingError, refetch, } = useQuery({ - ...provisionerJobs(organization?.id || ""), + ...provisionerJobs(organization?.id || "", queryParams), 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..a5837cf527fc2 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"; @@ -20,6 +21,7 @@ const meta: Meta = { args: { organization: MockOrganization, jobs: MockProvisionerJobs, + filter: { status: "" }, onRetry: fn(), }, }; @@ -75,3 +77,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< +
+ +
+