From 8da0a3a706a3fefdd1a9040d4858f58dcf3ffcb6 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 7 Aug 2024 13:47:32 -0800 Subject: [PATCH 1/3] feat: show summary if unable to edit org This can happen if you can edit the members, for example, but not the organization settings. In this case you will see a new summary page instead of the edit form. --- .../OrganizationSettingsPage.test.tsx | 20 ++++++++ .../OrganizationSettingsPage.tsx | 9 +++- .../OrganizationSettingsPageView.stories.tsx | 7 --- .../OrganizationSettingsPageView.tsx | 9 ++-- .../OrganizationSummaryPageView.stories.tsx | 23 +++++++++ .../OrganizationSummaryPageView.tsx | 50 +++++++++++++++++++ 6 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.stories.tsx create mode 100644 site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.test.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.test.tsx index e6107629920a4..f033e55188ceb 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.test.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.test.tsx @@ -116,4 +116,24 @@ describe("OrganizationSettingsPage", () => { await renderPage("the-endless-void"); await screen.findByText("Organization not found"); }); + + it("cannot edit organization", async () => { + server.use( + http.get("/api/v2/organizations", () => { + return HttpResponse.json([MockDefaultOrganization]); + }), + http.post("/api/v2/authcheck", async () => { + return HttpResponse.json({ + viewDeploymentValues: true, + }); + }), + ); + // No form since they cannot edit, instead sees the summary view. + await renderPage(MockDefaultOrganization.name); + expect(screen.queryByTestId("org-settings-form")).not.toBeInTheDocument(); + await screen.findByRole("heading", { + level: 1, + name: MockDefaultOrganization.display_name, + }); + }); }); diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx index 0b04b3848ed92..77246d2805295 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx @@ -15,6 +15,7 @@ import { useOrganizationSettings, } from "./ManagementSettingsLayout"; import { OrganizationSettingsPageView } from "./OrganizationSettingsPageView"; +import { OrganizationSummaryPageView } from "./OrganizationSummaryPageView"; const OrganizationSettingsPage: FC = () => { const { organization: organizationName } = useParams() as { @@ -65,12 +66,18 @@ const OrganizationSettingsPage: FC = () => { return ; } + // The user may not be able to edit this org but they can still see it because + // they can edit members, etc. In this case they will be shown a read-only + // summary page instead of the settings form. + if (!permissions[organization.id]?.editOrganization) { + return ; + } + const error = updateOrganizationMutation.error ?? deleteOrganizationMutation.error; return ( { diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.stories.tsx index 0b01e97b67a8e..37ce1185d7dba 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.stories.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.stories.tsx @@ -10,7 +10,6 @@ const meta: Meta = { component: OrganizationSettingsPageView, args: { organization: MockOrganization, - canEdit: true, }, }; @@ -24,9 +23,3 @@ export const DefaultOrg: Story = { organization: MockDefaultOrganization, }, }; - -export const CannotEdit: Story = { - args: { - canEdit: false, - }, -}; diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.tsx index 8be81243a396d..538387bcfef37 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.tsx @@ -44,12 +44,11 @@ interface OrganizationSettingsPageViewProps { error: unknown; onSubmit: (values: UpdateOrganizationRequest) => Promise; onDeleteOrganization: () => void; - canEdit: boolean; } export const OrganizationSettingsPageView: FC< OrganizationSettingsPageViewProps -> = ({ organization, error, onSubmit, onDeleteOrganization, canEdit }) => { +> = ({ organization, error, onSubmit, onDeleteOrganization }) => { const form = useFormik({ initialValues: { name: organization.name, @@ -85,7 +84,7 @@ export const OrganizationSettingsPageView: FC< description="The name and description of the organization." >
@@ -117,10 +116,10 @@ export const OrganizationSettingsPageView: FC<
- {canEdit && } + - {canEdit && !organization.is_default && ( + {!organization.is_default && ( = { + title: "pages/OrganizationSummaryPageView", + component: OrganizationSummaryPageView, + args: { + organization: MockOrganization, + }, +}; + +export default meta; +type Story = StoryObj; + +export const DefaultOrg: Story = { + args: { + organization: MockDefaultOrganization, + }, +}; diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.tsx new file mode 100644 index 0000000000000..124fc03b93655 --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.tsx @@ -0,0 +1,50 @@ +import type { FC } from "react"; +import type { Organization } from "api/typesGenerated"; +import { + PageHeader, + PageHeaderTitle, + PageHeaderSubtitle, +} from "components/PageHeader/PageHeader"; +import { Stack } from "components/Stack/Stack"; +import { UserAvatar } from "components/UserAvatar/UserAvatar"; + +interface OrganizationSummaryPageViewProps { + organization: Organization; +} + +export const OrganizationSummaryPageView: FC< + OrganizationSummaryPageViewProps +> = (props) => { + return ( +
+ + + +
+ + {props.organization.display_name || props.organization.name} + + {props.organization.description && ( + + {props.organization.description} + + )} +
+
+
+ You are a member of this organization. +
+ ); +}; From 7f520e7290b3f7c1469320f025a0a50b64f17407 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 7 Aug 2024 18:03:35 -0800 Subject: [PATCH 2/3] Destruct props --- .../OrganizationSummaryPageView.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.tsx index 124fc03b93655..2cb7ab60c090f 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.tsx @@ -14,7 +14,7 @@ interface OrganizationSummaryPageViewProps { export const OrganizationSummaryPageView: FC< OrganizationSummaryPageViewProps -> = (props) => { +> = ({ organization }) => { return (
- {props.organization.display_name || props.organization.name} + {organization.display_name || organization.name} - {props.organization.description && ( + {organization.description && ( - {props.organization.description} + {organization.description} )}
From 920b21060b3300a07b1694848bb6be54cc39b46a Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 9 Aug 2024 13:11:41 -0800 Subject: [PATCH 3/3] Use Storybook for non-redirect org settings tests --- .../OrganizationSettingsPage.stories.tsx | 63 +++++++++++++++++ .../OrganizationSettingsPage.test.tsx | 68 ++----------------- 2 files changed, 67 insertions(+), 64 deletions(-) create mode 100644 site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.stories.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.stories.tsx new file mode 100644 index 0000000000000..60932393a7260 --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.stories.tsx @@ -0,0 +1,63 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { reactRouterParameters } from "storybook-addon-remix-react-router"; +import { MockDefaultOrganization, MockUser } from "testHelpers/entities"; +import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook"; +import OrganizationSettingsPage from "./OrganizationSettingsPage"; + +const meta: Meta = { + title: "pages/OrganizationSettingsPage", + component: OrganizationSettingsPage, + decorators: [withAuthProvider, withDashboardProvider], + parameters: { + user: MockUser, + permissions: { viewDeploymentValues: true }, + queries: [ + { + key: ["organizations", [MockDefaultOrganization.id], "permissions"], + data: {}, + }, + ], + }, +}; + +export default meta; +type Story = StoryObj; + +export const NoRedirectableOrganizations: Story = {}; + +export const OrganizationDoesNotExist: Story = { + parameters: { + reactRouter: reactRouterParameters({ + location: { pathParams: { organization: "does-not-exist" } }, + routing: { path: "/organizations/:organization" }, + }), + }, +}; + +export const CannotEditOrganization: Story = { + parameters: { + reactRouter: reactRouterParameters({ + location: { pathParams: { organization: MockDefaultOrganization.name } }, + routing: { path: "/organizations/:organization" }, + }), + }, +}; + +export const CanEditOrganization: Story = { + parameters: { + reactRouter: reactRouterParameters({ + location: { pathParams: { organization: MockDefaultOrganization.name } }, + routing: { path: "/organizations/:organization" }, + }), + queries: [ + { + key: ["organizations", [MockDefaultOrganization.id], "permissions"], + data: { + [MockDefaultOrganization.id]: { + editOrganization: true, + }, + }, + }, + ], + }, +}; diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.test.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.test.tsx index f033e55188ceb..8bf86e8ee6387 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.test.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.test.tsx @@ -13,7 +13,7 @@ import OrganizationSettingsPage from "./OrganizationSettingsPage"; jest.spyOn(console, "error").mockImplementation(() => {}); -const renderRootPage = async () => { +const renderPage = async () => { renderWithManagementSettingsLayout(, { route: "/organizations", path: "/organizations/:organization?", @@ -21,31 +21,7 @@ const renderRootPage = async () => { await waitForLoaderToBeRemoved(); }; -const renderPage = async (orgName: string) => { - renderWithManagementSettingsLayout(, { - route: `/organizations/${orgName}`, - path: "/organizations/:organization", - }); - await waitForLoaderToBeRemoved(); -}; - describe("OrganizationSettingsPage", () => { - it("has no organizations", async () => { - server.use( - http.get("/api/v2/organizations", () => { - return HttpResponse.json([]); - }), - http.post("/api/v2/authcheck", async () => { - return HttpResponse.json({ - [`${MockDefaultOrganization.id}.editOrganization`]: true, - viewDeploymentValues: true, - }); - }), - ); - await renderRootPage(); - await screen.findByText("No organizations found"); - }); - it("has no editable organizations", async () => { server.use( http.get("/api/v2/organizations", () => { @@ -57,7 +33,7 @@ describe("OrganizationSettingsPage", () => { }); }), ); - await renderRootPage(); + await renderPage(); await screen.findByText("No organizations found"); }); @@ -75,7 +51,7 @@ describe("OrganizationSettingsPage", () => { }); }), ); - await renderRootPage(); + await renderPage(); const form = screen.getByTestId("org-settings-form"); expect(within(form).getByRole("textbox", { name: "Name" })).toHaveValue( MockDefaultOrganization.name, @@ -94,46 +70,10 @@ describe("OrganizationSettingsPage", () => { }); }), ); - await renderRootPage(); + await renderPage(); const form = screen.getByTestId("org-settings-form"); expect(within(form).getByRole("textbox", { name: "Name" })).toHaveValue( MockOrganization2.name, ); }); - - it("cannot find organization", async () => { - server.use( - http.get("/api/v2/organizations", () => { - return HttpResponse.json([MockDefaultOrganization, MockOrganization2]); - }), - http.post("/api/v2/authcheck", async () => { - return HttpResponse.json({ - [`${MockOrganization2.id}.editOrganization`]: true, - viewDeploymentValues: true, - }); - }), - ); - await renderPage("the-endless-void"); - await screen.findByText("Organization not found"); - }); - - it("cannot edit organization", async () => { - server.use( - http.get("/api/v2/organizations", () => { - return HttpResponse.json([MockDefaultOrganization]); - }), - http.post("/api/v2/authcheck", async () => { - return HttpResponse.json({ - viewDeploymentValues: true, - }); - }), - ); - // No form since they cannot edit, instead sees the summary view. - await renderPage(MockDefaultOrganization.name); - expect(screen.queryByTestId("org-settings-form")).not.toBeInTheDocument(); - await screen.findByRole("heading", { - level: 1, - name: MockDefaultOrganization.display_name, - }); - }); });