From 38e2712a4eeaef6a144550e7afa1e6b6d60d2e50 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 25 Feb 2025 16:47:06 +0000 Subject: [PATCH] refactor: rollback provisioners page to its previous version --- .../OrganizationProvisionersPage.tsx | 48 ++++++ ...ganizationProvisionersPageView.stories.tsx | 142 +++++++++++++++++ .../OrganizationProvisionersPageView.tsx | 148 ++++++++++++++++++ site/src/router.tsx | 5 +- 4 files changed, 339 insertions(+), 4 deletions(-) create mode 100644 site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx create mode 100644 site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.stories.tsx create mode 100644 site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.tsx diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx new file mode 100644 index 0000000000000..5a4965c039e1f --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx @@ -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 ; + } + + return ( + <> + + + {pageTitle( + "Provisioners", + organization.display_name || organization.name, + )} + + + + + ); +}; + +export default OrganizationProvisionersPage; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.stories.tsx new file mode 100644 index 0000000000000..5bbf6cfe81731 --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.stories.tsx @@ -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 = { + title: "pages/OrganizationProvisionersPage", + component: OrganizationProvisionersPageView, + args: { + buildInfo: MockBuildInfo, + }, +}; + +export default meta; +type Story = StoryObj; + +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, + }, +}; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.tsx new file mode 100644 index 0000000000000..649a75836b603 --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.tsx @@ -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 ( +
+ + + {!showPaywall && ( + + )} + + {showPaywall ? ( + + ) : error ? ( + + ) : !buildInfo || !provisioners ? ( + + ) : ( + + )} +
+ ); +}; + +type ViewContentProps = Required< + Pick +>; + +const ViewContent: FC = ({ 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 ? ( + } + target="_blank" + href={docs("/admin/provisioners")} + > + Create a provisioner + + } + /> + ) : ( +
({ + margin: 0, + fontSize: 12, + paddingBottom: 18, + color: theme.palette.text.secondary, + })} + > + Showing {provisionerGroupsCount} groups and {provisionersCount}{" "} + provisioners +
+ )} + + {provisioners.map((group) => ( + + ))} + + + ); +}; + +// 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"; + } +} diff --git a/site/src/router.tsx b/site/src/router.tsx index 8490c966c8a54..66d37f92aeaf1 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -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"),