Apply Kaylas suggestion

This commit is contained in:
BrunoQuaresma
2025-03-13 13:25:44 +00:00
parent a56246f027
commit 8a62eccdef
6 changed files with 209 additions and 238 deletions

View File

@ -30,12 +30,6 @@ type Story = StoryObj<typeof JobRow>;
export const Close: Story = {};
export const Open: Story = {
args: {
defaultOpen: true,
},
};
export const OpenOnClick: Story = {
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
@ -46,29 +40,19 @@ export const OpenOnClick: Story = {
const jobId = canvas.getByText(args.job.id);
expect(jobId).toBeInTheDocument();
},
parameters: {
chromatic: {
disableSnapshot: true,
},
},
};
export const HideOnClick: Story = {
args: {
defaultOpen: true,
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const showMoreButton = canvas.getByRole("button", { name: /hide/i });
const showMoreButton = canvas.getByRole("button", { name: /show more/i });
await userEvent.click(showMoreButton);
const hideButton = canvas.getByRole("button", { name: /hide/i });
await userEvent.click(hideButton);
const jobId = canvas.queryByText(args.job.id);
expect(jobId).not.toBeInTheDocument();
},
parameters: {
chromatic: {
disableSnapshot: true,
},
},
};

View File

@ -16,12 +16,11 @@ import { Tag, Tags, TruncateTags } from "./Tags";
type JobRowProps = {
job: ProvisionerJob;
defaultOpen?: boolean;
};
export const JobRow: FC<JobRowProps> = ({ job, defaultOpen }) => {
export const JobRow: FC<JobRowProps> = ({ job }) => {
const metadata = job.metadata;
const [isOpen, setIsOpen] = useState(defaultOpen);
const [isOpen, setIsOpen] = useState(false);
return (
<>

View File

@ -1,113 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import { expect, userEvent, waitFor, within } from "@storybook/test";
import type { ProvisionerJob } from "api/typesGenerated";
import { OrganizationSettingsContext } from "modules/management/OrganizationSettingsLayout";
import {
MockOrganization,
MockOrganizationPermissions,
MockProvisionerJob,
} from "testHelpers/entities";
import { daysAgo } from "utils/time";
import OrganizationProvisionerJobsPage from "./OrganizationProvisionerJobsPage";
const defaultOrganizationSettingsValue = {
organization: MockOrganization,
organizationPermissionsByOrganizationId: {},
organizations: [MockOrganization],
organizationPermissions: MockOrganizationPermissions,
};
const meta: Meta<typeof OrganizationProvisionerJobsPage> = {
title: "pages/OrganizationProvisionerJobsPage",
component: OrganizationProvisionerJobsPage,
decorators: [
(Story, { parameters }) => (
<OrganizationSettingsContext.Provider
value={parameters.organizationSettingsValue}
>
<Story />
</OrganizationSettingsContext.Provider>
),
],
args: {
getProvisionerJobs: async () => MockProvisionerJobs,
},
parameters: {
organizationSettingsValue: defaultOrganizationSettingsValue,
},
};
export default meta;
type Story = StoryObj<typeof OrganizationProvisionerJobsPage>;
export const Default: Story = {};
export const OrganizationNotFound: Story = {
parameters: {
organizationSettingsValue: {
...defaultOrganizationSettingsValue,
organization: null,
},
},
};
export const Loading: Story = {
args: {
getProvisionerJobs: () =>
new Promise((res) => {
setTimeout(res, 100_000);
}),
},
};
export const LoadingError: Story = {
args: {
getProvisionerJobs: async () => {
throw new Error("Failed to load jobs");
},
},
};
export const RetryAfterError: Story = {
args: {
getProvisionerJobs: (() => {
let count = 0;
return async () => {
count++;
if (count === 1) {
throw new Error("Failed to load jobs");
}
return MockProvisionerJobs;
};
})(),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const retryButton = await canvas.findByRole("button", { name: "Retry" });
userEvent.click(retryButton);
await waitFor(() => {
const rows = canvasElement.querySelectorAll("tbody > tr");
expect(rows).toHaveLength(MockProvisionerJobs.length);
});
},
};
export const Empty: Story = {
args: {
getProvisionerJobs: async () => [],
},
};
const MockProvisionerJobs: ProvisionerJob[] = Array.from(
{ length: 50 },
(_, i) => ({
...MockProvisionerJob,
id: i.toString(),
created_at: daysAgo(2),
}),
);

View File

@ -1,116 +1,27 @@
import type { API } from "api/api";
import { provisionerJobs } from "api/queries/organizations";
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 {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "components/Table/Table";
import {
type OrganizationSettingsValue,
useOrganizationSettings,
} from "modules/management/OrganizationSettingsLayout";
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useQuery } from "react-query";
import { docs } from "utils/docs";
import { pageTitle } from "utils/page";
import { JobRow } from "./JobRow";
import OrganizationProvisionerJobsPageView from "./OrganizationProvisionerJobsPageView";
type OrganizationProvisionerJobsPageProps = {
getProvisionerJobs?: typeof API.getProvisionerJobs;
};
const OrganizationProvisionerJobsPage: FC<
OrganizationProvisionerJobsPageProps
> = ({ getProvisionerJobs }) => {
const OrganizationProvisionerJobsPage: FC = () => {
const { organization } = useOrganizationSettings();
if (!organization) {
return <EmptyState message="Organization not found" />;
}
const {
data: jobs,
isLoadingError,
refetch,
} = useQuery(provisionerJobs(organization.id, getProvisionerJobs));
} = useQuery({
...provisionerJobs(organization?.id || ""),
enabled: organization !== undefined,
});
return (
<>
<Helmet>
<title>
{pageTitle(
"Provisioner Jobs",
organization.display_name || organization.name,
)}
</title>
</Helmet>
<section className="flex flex-col gap-8">
<header className="flex flex-row items-baseline justify-between">
<div className="flex flex-col gap-2">
<h1 className="text-3xl m-0">Provisioner Jobs</h1>
<p className="text-sm text-content-secondary m-0">
Provisioner Jobs are the individual tasks assigned to Provisioners
when the workspaces are being built.{" "}
<Link href={docs("/admin/provisioners")}>View docs</Link>
</p>
</div>
</header>
<Table>
<TableHeader>
<TableRow>
<TableHead>Created</TableHead>
<TableHead>Type</TableHead>
<TableHead>Template</TableHead>
<TableHead>Tags</TableHead>
<TableHead>Status</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
{jobs ? (
jobs.length > 0 ? (
jobs.map((j) => <JobRow key={j.id} job={j} />)
) : (
<TableRow>
<TableCell colSpan={999}>
<EmptyState message="No provisioner jobs found" />
</TableCell>
</TableRow>
)
) : isLoadingError ? (
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message="Error loading the provisioner jobs"
cta={
<Button size="sm" onClick={() => refetch()}>
Retry
</Button>
}
/>
</TableCell>
</TableRow>
) : (
<TableRow>
<TableCell colSpan={999}>
<Loader />
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</section>
</>
<OrganizationProvisionerJobsPageView
jobs={jobs}
organization={organization}
error={isLoadingError}
onRetry={refetch}
/>
);
};

View File

@ -0,0 +1,77 @@
import type { Meta, StoryObj } from "@storybook/react";
import { expect, fn, userEvent, waitFor, within } from "@storybook/test";
import type { ProvisionerJob } from "api/typesGenerated";
import { MockOrganization, MockProvisionerJob } from "testHelpers/entities";
import { daysAgo } from "utils/time";
import OrganizationProvisionerJobsPageView from "./OrganizationProvisionerJobsPageView";
const MockProvisionerJobs: ProvisionerJob[] = Array.from(
{ length: 50 },
(_, i) => ({
...MockProvisionerJob,
id: i.toString(),
created_at: daysAgo(2),
}),
);
const meta: Meta<typeof OrganizationProvisionerJobsPageView> = {
title: "pages/OrganizationProvisionerJobsPage",
component: OrganizationProvisionerJobsPageView,
args: {
organization: MockOrganization,
jobs: MockProvisionerJobs,
onRetry: fn(),
},
};
export default meta;
type Story = StoryObj<typeof OrganizationProvisionerJobsPageView>;
export const Default: Story = {};
export const OrganizationNotFound: Story = {
args: {
organization: undefined,
},
};
export const Loading: Story = {
args: {
jobs: undefined,
},
};
export const LoadingError: Story = {
args: {
jobs: undefined,
error: new Error("Failed to load jobs"),
},
};
export const RetryAfterError: Story = {
args: {
jobs: undefined,
error: new Error("Failed to load jobs"),
onRetry: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const retryButton = await canvas.findByRole("button", { name: "Retry" });
userEvent.click(retryButton);
await waitFor(() => {
expect(args.onRetry).toHaveBeenCalled();
});
},
parameters: {
chromatic: {
disableSnapshot: true,
},
},
};
export const Empty: Story = {
args: {
jobs: [],
},
};

View File

@ -0,0 +1,113 @@
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 {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "components/Table/Table";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { docs } from "utils/docs";
import { pageTitle } from "utils/page";
import { JobRow } from "./JobRow";
import type { ProvisionerJob, Organization } from "api/typesGenerated";
type OrganizationProvisionerJobsPageViewProps = {
jobs: ProvisionerJob[] | undefined;
organization: Organization | undefined;
error: unknown;
onRetry: () => void;
};
const OrganizationProvisionerJobsPageView: FC<
OrganizationProvisionerJobsPageViewProps
> = ({ jobs, organization, error, onRetry }) => {
if (!organization) {
return (
<>
<Helmet>
<title>{pageTitle("Provisioner Jobs")}</title>
</Helmet>
<EmptyState message="Organization not found" />
</>
);
}
return (
<>
<Helmet>
<title>
{pageTitle(
"Provisioner Jobs",
organization.display_name || organization.name,
)}
</title>
</Helmet>
<section className="flex flex-col gap-8">
<header className="flex flex-row items-baseline justify-between">
<div className="flex flex-col gap-2">
<h1 className="text-3xl m-0">Provisioner Jobs</h1>
<p className="text-sm text-content-secondary m-0">
Provisioner Jobs are the individual tasks assigned to Provisioners
when the workspaces are being built.{" "}
<Link href={docs("/admin/provisioners")}>View docs</Link>
</p>
</div>
</header>
<Table>
<TableHeader>
<TableRow>
<TableHead>Created</TableHead>
<TableHead>Type</TableHead>
<TableHead>Template</TableHead>
<TableHead>Tags</TableHead>
<TableHead>Status</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
{jobs ? (
jobs.length > 0 ? (
jobs.map((j) => <JobRow key={j.id} job={j} />)
) : (
<TableRow>
<TableCell colSpan={999}>
<EmptyState message="No provisioner jobs found" />
</TableCell>
</TableRow>
)
) : error ? (
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message="Error loading the provisioner jobs"
cta={
<Button size="sm" onClick={onRetry}>
Retry
</Button>
}
/>
</TableCell>
</TableRow>
) : (
<TableRow>
<TableCell colSpan={999}>
<Loader />
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</section>
</>
);
};
export default OrganizationProvisionerJobsPageView;