Thanks to visit codestin.com
Credit goes to github.com

Skip to content

refactor: rollback provisioners page to its previous version #16699

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { buildInfo } from "api/queries/buildInfo";
import { provisionerDaemonGroups } from "api/queries/organizations";
import { EmptyState } from "components/EmptyState/EmptyState";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
import { useDashboard } from "modules/dashboard/useDashboard";
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useQuery } from "react-query";
import { useParams } from "react-router-dom";
import { pageTitle } from "utils/page";
import { OrganizationProvisionersPageView } from "./OrganizationProvisionersPageView";

const OrganizationProvisionersPage: FC = () => {
const { organization: organizationName } = useParams() as {
organization: string;
};
const { organization } = useOrganizationSettings();
const { entitlements } = useDashboard();
const { metadata } = useEmbeddedMetadata();
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
const provisionersQuery = useQuery(provisionerDaemonGroups(organizationName));

if (!organization) {
return <EmptyState message="Organization not found" />;
}

return (
<>
<Helmet>
<title>
{pageTitle(
"Provisioners",
organization.display_name || organization.name,
)}
</title>
</Helmet>
<OrganizationProvisionersPageView
showPaywall={!entitlements.features.multiple_organizations.enabled}
error={provisionersQuery.error}
buildInfo={buildInfoQuery.data}
provisioners={provisionersQuery.data}
/>
</>
);
};

export default OrganizationProvisionersPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import type { Meta, StoryObj } from "@storybook/react";
import { screen, userEvent } from "@storybook/test";
import {
MockBuildInfo,
MockProvisioner,
MockProvisioner2,
MockProvisionerBuiltinKey,
MockProvisionerKey,
MockProvisionerPskKey,
MockProvisionerUserAuthKey,
MockProvisionerWithTags,
MockUserProvisioner,
mockApiError,
} from "testHelpers/entities";
import { OrganizationProvisionersPageView } from "./OrganizationProvisionersPageView";

const meta: Meta<typeof OrganizationProvisionersPageView> = {
title: "pages/OrganizationProvisionersPage",
component: OrganizationProvisionersPageView,
args: {
buildInfo: MockBuildInfo,
},
};

export default meta;
type Story = StoryObj<typeof OrganizationProvisionersPageView>;

export const Provisioners: Story = {
args: {
provisioners: [
{
key: MockProvisionerBuiltinKey,
daemons: [MockProvisioner, MockProvisioner2],
},
{
key: MockProvisionerPskKey,
daemons: [
MockProvisioner,
MockUserProvisioner,
MockProvisionerWithTags,
],
},
{
key: MockProvisionerPskKey,
daemons: [MockProvisioner, MockProvisioner2],
},
{
key: { ...MockProvisionerKey, id: "ジェイデン", name: "ジェイデン" },
daemons: [
MockProvisioner,
{ ...MockProvisioner2, tags: { scope: "organization", owner: "" } },
],
},
{
key: { ...MockProvisionerKey, id: "ベン", name: "ベン" },
daemons: [
MockProvisioner,
{
...MockProvisioner2,
version: "2.0.0",
api_version: "1.0",
},
],
},
{
key: {
...MockProvisionerKey,
id: "ケイラ",
name: "ケイラ",
tags: {
...MockProvisioner.tags,
都市: "ユタ",
きっぷ: "yes",
ちいさい: "no",
},
},
daemons: Array.from({ length: 117 }, (_, i) => ({
...MockProvisioner,
id: `ケイラ-${i}`,
name: `ケイラ-${i}`,
})),
},
{
key: MockProvisionerUserAuthKey,
daemons: [
MockUserProvisioner,
{
...MockUserProvisioner,
id: "mock-user-provisioner-2",
name: "Test User Provisioner 2",
},
],
},
],
},
play: async ({ step }) => {
await step("open all details", async () => {
const expandButtons = await screen.findAllByRole("button", {
name: "Show provisioner details",
});
for (const it of expandButtons) {
await userEvent.click(it);
}
});

await step("close uninteresting/large details", async () => {
const collapseButtons = await screen.findAllByRole("button", {
name: "Hide provisioner details",
});

await userEvent.click(collapseButtons[2]);
await userEvent.click(collapseButtons[3]);
await userEvent.click(collapseButtons[5]);
});

await step("show version popover", async () => {
const outOfDate = await screen.findByText("Out of date");
await userEvent.hover(outOfDate);
});
},
};

export const Empty: Story = {
args: {
provisioners: [],
},
};

export const WithError: Story = {
args: {
error: mockApiError({
message: "Fern is mad",
detail: "Frieren slept in and didn't get groceries",
}),
},
};

export const Paywall: Story = {
args: {
showPaywall: true,
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import Button from "@mui/material/Button";
import type {
BuildInfoResponse,
ProvisionerKey,
ProvisionerKeyDaemons,
} from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Loader } from "components/Loader/Loader";
import { Paywall } from "components/Paywall/Paywall";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import { ProvisionerGroup } from "modules/provisioners/ProvisionerGroup";
import type { FC } from "react";
import { docs } from "utils/docs";

interface OrganizationProvisionersPageViewProps {
/** Determines if the paywall will be shown or not */
showPaywall?: boolean;

/** An error to display instead of the page content */
error?: unknown;

/** Info about the version of coderd */
buildInfo?: BuildInfoResponse;

/** Groups of provisioners, along with their key information */
provisioners?: readonly ProvisionerKeyDaemons[];
}

export const OrganizationProvisionersPageView: FC<
OrganizationProvisionersPageViewProps
> = ({ showPaywall, error, buildInfo, provisioners }) => {
return (
<div>
<Stack
alignItems="baseline"
direction="row"
justifyContent="space-between"
>
<SettingsHeader title="Provisioners" />
{!showPaywall && (
<Button
endIcon={<OpenInNewIcon />}
target="_blank"
href={docs("/admin/provisioners")}
>
Create a provisioner
</Button>
)}
</Stack>
{showPaywall ? (
<Paywall
message="Provisioners"
description="Provisioners run your Terraform to create templates and workspaces. You need a Premium license to use this feature for multiple organizations."
documentationLink={docs("/")}
/>
) : error ? (
<ErrorAlert error={error} />
) : !buildInfo || !provisioners ? (
<Loader />
) : (
<ViewContent buildInfo={buildInfo} provisioners={provisioners} />
)}
</div>
);
};

type ViewContentProps = Required<
Pick<OrganizationProvisionersPageViewProps, "buildInfo" | "provisioners">
>;

const ViewContent: FC<ViewContentProps> = ({ buildInfo, provisioners }) => {
const isEmpty = provisioners.every((group) => group.daemons.length === 0);

const provisionerGroupsCount = provisioners.length;
const provisionersCount = provisioners.reduce(
(a, group) => a + group.daemons.length,
0,
);

return (
<>
{isEmpty ? (
<EmptyState
message="No provisioners"
description="A provisioner is required before you can create templates and workspaces. You can connect your first provisioner by following our documentation."
cta={
<Button
endIcon={<OpenInNewIcon />}
target="_blank"
href={docs("/admin/provisioners")}
>
Create a provisioner
</Button>
}
/>
) : (
<div
css={(theme) => ({
margin: 0,
fontSize: 12,
paddingBottom: 18,
color: theme.palette.text.secondary,
})}
>
Showing {provisionerGroupsCount} groups and {provisionersCount}{" "}
provisioners
</div>
)}
<Stack spacing={4.5}>
{provisioners.map((group) => (
<ProvisionerGroup
key={group.key.id}
buildInfo={buildInfo}
keyName={group.key.name}
keyTags={group.key.tags}
type={getGroupType(group.key)}
provisioners={group.daemons}
/>
))}
</Stack>
</>
);
};

// Ideally these would be generated and appear in typesGenerated.ts, but that is
// not currently the case. In the meantime, these are taken from verbatim from
// the corresponding codersdk declarations. The names remain unchanged to keep
// usage of these special values "grep-able".
// https://github.com/coder/coder/blob/7c77a3cc832fb35d9da4ca27df163c740f786137/codersdk/provisionerdaemons.go#L291-L295
const ProvisionerKeyIDBuiltIn = "00000000-0000-0000-0000-000000000001";
const ProvisionerKeyIDUserAuth = "00000000-0000-0000-0000-000000000002";
const ProvisionerKeyIDPSK = "00000000-0000-0000-0000-000000000003";

function getGroupType(key: ProvisionerKey) {
switch (key.id) {
case ProvisionerKeyIDBuiltIn:
return "builtin";
case ProvisionerKeyIDUserAuth:
return "userAuth";
case ProvisionerKeyIDPSK:
return "psk";
default:
return "key";
}
}
5 changes: 1 addition & 4 deletions site/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,7 @@ const CreateEditRolePage = lazy(
),
);
const ProvisionersPage = lazy(
() =>
import(
"./pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage"
),
() => import("./pages/OrganizationSettingsPage/OrganizationProvisionersPage"),
);
const TemplateEmbedPage = lazy(
() => import("./pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage"),
Expand Down
Loading