mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: show summary if unable to edit org (#14214)
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.
This commit is contained in:
@@ -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<typeof OrganizationSettingsPage> = {
|
||||
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<typeof OrganizationSettingsPage>;
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -13,7 +13,7 @@ import OrganizationSettingsPage from "./OrganizationSettingsPage";
|
||||
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
const renderRootPage = async () => {
|
||||
const renderPage = async () => {
|
||||
renderWithManagementSettingsLayout(<OrganizationSettingsPage />, {
|
||||
route: "/organizations",
|
||||
path: "/organizations/:organization?",
|
||||
@@ -21,31 +21,7 @@ const renderRootPage = async () => {
|
||||
await waitForLoaderToBeRemoved();
|
||||
};
|
||||
|
||||
const renderPage = async (orgName: string) => {
|
||||
renderWithManagementSettingsLayout(<OrganizationSettingsPage />, {
|
||||
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,26 +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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 <EmptyState message="Organization not found" />;
|
||||
}
|
||||
|
||||
// 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 <OrganizationSummaryPageView organization={organization} />;
|
||||
}
|
||||
|
||||
const error =
|
||||
updateOrganizationMutation.error ?? deleteOrganizationMutation.error;
|
||||
|
||||
return (
|
||||
<OrganizationSettingsPageView
|
||||
canEdit={permissions[organization.id]?.editOrganization ?? false}
|
||||
organization={organization}
|
||||
error={error}
|
||||
onSubmit={async (values) => {
|
||||
|
||||
@@ -10,7 +10,6 @@ const meta: Meta<typeof OrganizationSettingsPageView> = {
|
||||
component: OrganizationSettingsPageView,
|
||||
args: {
|
||||
organization: MockOrganization,
|
||||
canEdit: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,9 +23,3 @@ export const DefaultOrg: Story = {
|
||||
organization: MockDefaultOrganization,
|
||||
},
|
||||
};
|
||||
|
||||
export const CannotEdit: Story = {
|
||||
args: {
|
||||
canEdit: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -44,12 +44,11 @@ interface OrganizationSettingsPageViewProps {
|
||||
error: unknown;
|
||||
onSubmit: (values: UpdateOrganizationRequest) => Promise<void>;
|
||||
onDeleteOrganization: () => void;
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
export const OrganizationSettingsPageView: FC<
|
||||
OrganizationSettingsPageViewProps
|
||||
> = ({ organization, error, onSubmit, onDeleteOrganization, canEdit }) => {
|
||||
> = ({ organization, error, onSubmit, onDeleteOrganization }) => {
|
||||
const form = useFormik<UpdateOrganizationRequest>({
|
||||
initialValues: {
|
||||
name: organization.name,
|
||||
@@ -85,7 +84,7 @@ export const OrganizationSettingsPageView: FC<
|
||||
description="The name and description of the organization."
|
||||
>
|
||||
<fieldset
|
||||
disabled={form.isSubmitting || !canEdit}
|
||||
disabled={form.isSubmitting}
|
||||
css={{ border: "unset", padding: 0, margin: 0, width: "100%" }}
|
||||
>
|
||||
<FormFields>
|
||||
@@ -117,10 +116,10 @@ export const OrganizationSettingsPageView: FC<
|
||||
</FormFields>
|
||||
</fieldset>
|
||||
</FormSection>
|
||||
{canEdit && <FormFooter isLoading={form.isSubmitting} />}
|
||||
<FormFooter isLoading={form.isSubmitting} />
|
||||
</HorizontalForm>
|
||||
|
||||
{canEdit && !organization.is_default && (
|
||||
{!organization.is_default && (
|
||||
<HorizontalContainer css={{ marginTop: 48 }}>
|
||||
<HorizontalSection
|
||||
title="Settings"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import {
|
||||
MockDefaultOrganization,
|
||||
MockOrganization,
|
||||
} from "testHelpers/entities";
|
||||
import { OrganizationSummaryPageView } from "./OrganizationSummaryPageView";
|
||||
|
||||
const meta: Meta<typeof OrganizationSummaryPageView> = {
|
||||
title: "pages/OrganizationSummaryPageView",
|
||||
component: OrganizationSummaryPageView,
|
||||
args: {
|
||||
organization: MockOrganization,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof OrganizationSummaryPageView>;
|
||||
|
||||
export const DefaultOrg: Story = {
|
||||
args: {
|
||||
organization: MockDefaultOrganization,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
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
|
||||
> = ({ organization }) => {
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
css={{
|
||||
// The deployment settings layout already has padding.
|
||||
paddingTop: 0,
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={3} alignItems="center">
|
||||
<UserAvatar
|
||||
key={organization.id}
|
||||
size="xl"
|
||||
username={organization.display_name || organization.name}
|
||||
avatarURL={organization.icon}
|
||||
/>
|
||||
<div>
|
||||
<PageHeaderTitle>
|
||||
{organization.display_name || organization.name}
|
||||
</PageHeaderTitle>
|
||||
{organization.description && (
|
||||
<PageHeaderSubtitle>
|
||||
{organization.description}
|
||||
</PageHeaderSubtitle>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
</PageHeader>
|
||||
You are a member of this organization.
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user