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"), pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy