mirror of
https://github.com/coder/coder.git
synced 2025-03-14 10:09:57 +00:00
Apply Kaylas suggestion
This commit is contained in:
@ -30,12 +30,6 @@ type Story = StoryObj<typeof JobRow>;
|
|||||||
|
|
||||||
export const Close: Story = {};
|
export const Close: Story = {};
|
||||||
|
|
||||||
export const Open: Story = {
|
|
||||||
args: {
|
|
||||||
defaultOpen: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const OpenOnClick: Story = {
|
export const OpenOnClick: Story = {
|
||||||
play: async ({ canvasElement, args }) => {
|
play: async ({ canvasElement, args }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
@ -46,29 +40,19 @@ export const OpenOnClick: Story = {
|
|||||||
const jobId = canvas.getByText(args.job.id);
|
const jobId = canvas.getByText(args.job.id);
|
||||||
expect(jobId).toBeInTheDocument();
|
expect(jobId).toBeInTheDocument();
|
||||||
},
|
},
|
||||||
parameters: {
|
|
||||||
chromatic: {
|
|
||||||
disableSnapshot: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HideOnClick: Story = {
|
export const HideOnClick: Story = {
|
||||||
args: {
|
|
||||||
defaultOpen: true,
|
|
||||||
},
|
|
||||||
play: async ({ canvasElement, args }) => {
|
play: async ({ canvasElement, args }) => {
|
||||||
const canvas = within(canvasElement);
|
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);
|
await userEvent.click(showMoreButton);
|
||||||
|
|
||||||
|
const hideButton = canvas.getByRole("button", { name: /hide/i });
|
||||||
|
await userEvent.click(hideButton);
|
||||||
|
|
||||||
const jobId = canvas.queryByText(args.job.id);
|
const jobId = canvas.queryByText(args.job.id);
|
||||||
expect(jobId).not.toBeInTheDocument();
|
expect(jobId).not.toBeInTheDocument();
|
||||||
},
|
},
|
||||||
parameters: {
|
|
||||||
chromatic: {
|
|
||||||
disableSnapshot: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
@ -16,12 +16,11 @@ import { Tag, Tags, TruncateTags } from "./Tags";
|
|||||||
|
|
||||||
type JobRowProps = {
|
type JobRowProps = {
|
||||||
job: ProvisionerJob;
|
job: ProvisionerJob;
|
||||||
defaultOpen?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const JobRow: FC<JobRowProps> = ({ job, defaultOpen }) => {
|
export const JobRow: FC<JobRowProps> = ({ job }) => {
|
||||||
const metadata = job.metadata;
|
const metadata = job.metadata;
|
||||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -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),
|
|
||||||
}),
|
|
||||||
);
|
|
@ -1,116 +1,27 @@
|
|||||||
import type { API } from "api/api";
|
|
||||||
import { provisionerJobs } from "api/queries/organizations";
|
import { provisionerJobs } from "api/queries/organizations";
|
||||||
import { Button } from "components/Button/Button";
|
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
|
||||||
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 type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { Helmet } from "react-helmet-async";
|
|
||||||
import { useQuery } from "react-query";
|
import { useQuery } from "react-query";
|
||||||
import { docs } from "utils/docs";
|
import OrganizationProvisionerJobsPageView from "./OrganizationProvisionerJobsPageView";
|
||||||
import { pageTitle } from "utils/page";
|
|
||||||
import { JobRow } from "./JobRow";
|
|
||||||
|
|
||||||
type OrganizationProvisionerJobsPageProps = {
|
const OrganizationProvisionerJobsPage: FC = () => {
|
||||||
getProvisionerJobs?: typeof API.getProvisionerJobs;
|
|
||||||
};
|
|
||||||
|
|
||||||
const OrganizationProvisionerJobsPage: FC<
|
|
||||||
OrganizationProvisionerJobsPageProps
|
|
||||||
> = ({ getProvisionerJobs }) => {
|
|
||||||
const { organization } = useOrganizationSettings();
|
const { organization } = useOrganizationSettings();
|
||||||
|
|
||||||
if (!organization) {
|
|
||||||
return <EmptyState message="Organization not found" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: jobs,
|
data: jobs,
|
||||||
isLoadingError,
|
isLoadingError,
|
||||||
refetch,
|
refetch,
|
||||||
} = useQuery(provisionerJobs(organization.id, getProvisionerJobs));
|
} = useQuery({
|
||||||
|
...provisionerJobs(organization?.id || ""),
|
||||||
|
enabled: organization !== undefined,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<OrganizationProvisionerJobsPageView
|
||||||
<Helmet>
|
jobs={jobs}
|
||||||
<title>
|
organization={organization}
|
||||||
{pageTitle(
|
error={isLoadingError}
|
||||||
"Provisioner Jobs",
|
onRetry={refetch}
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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: [],
|
||||||
|
},
|
||||||
|
};
|
@ -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;
|
Reference in New Issue
Block a user