Bruno Quaresma
2025-04-04 10:02:36 -03:00
committed by GitHub
parent 43d584c4f3
commit 3bfafe3b43
7 changed files with 161 additions and 25 deletions
+10 -1
View File
@@ -400,6 +400,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
@@ -2395,9 +2400,13 @@ class ApiMethods {
return res.data;
};
getProvisionerJobs = async (orgId: string) => {
getProvisionerJobs = async (
orgId: string,
params: GetProvisionerJobsParams = {},
) => {
const res = await this.axios.get<TypesGen.ProvisionerJob[]>(
`/api/v2/organizations/${orgId}/provisionerjobs`,
{ params },
);
return res.data;
};
+12 -9
View File
@@ -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";
@@ -241,16 +242,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),
};
};
@@ -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),
@@ -52,16 +52,20 @@ export const JobRow: FC<JobRowProps> = ({ job }) => {
<Badge size="sm">{job.type}</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-1 whitespace-nowrap">
<Avatar
variant="icon"
src={metadata.template_icon}
fallback={
metadata.template_display_name || metadata.template_name
}
/>
{metadata.template_display_name || metadata.template_name}
</div>
{job.metadata.template_name !== "" ? (
<div className="flex items-center gap-1 whitespace-nowrap">
<Avatar
variant="icon"
src={metadata.template_icon}
fallback={
metadata.template_display_name || metadata.template_name
}
/>
{metadata.template_display_name || metadata.template_name}
</div>
) : (
<span>-</span>
)}
</TableCell>
<TableCell>
<TruncateTags tags={job.tags} />
@@ -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 (
<OrganizationProvisionerJobsPageView
jobs={jobs}
filter={filter}
organization={organization}
error={isLoadingError}
onRetry={refetch}
onFilterChange={setSearchParams}
/>
);
};
@@ -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<typeof OrganizationProvisionerJobsPageView> = {
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<ProvisionerJob[]>([]);
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 (
<OrganizationProvisionerJobsPageView
{...args}
filter={filter}
jobs={jobs}
onFilterChange={handleFilterChange}
/>
);
},
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);
},
};
@@ -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<
</div>
</header>
<div>
<Select
value={filter.status}
onValueChange={(status) => {
onFilterChange({ status: status as ProvisionerJobStatus });
}}
>
<SelectTrigger className="w-[180px]" data-testid="status-filter">
<SelectValue placeholder="All statuses" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{StatusFilters.map((status) => (
<SelectItem key={status} value={status}>
<StatusIndicator variant={variantByStatus[status]}>
<StatusIndicatorDot />
<span className="block first-letter:uppercase">
{status}
</span>
</StatusIndicator>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
<Table>
<TableHeader>
<TableRow>