Skip to content

Commit c0ddabe

Browse files
committed
feat(site): move Default Roles UI from Settings to Roles page
Relocate the Default Roles section, its dialog, and the roles query to the organization Roles page. The selector sits above Custom Roles, shares the page-level roles query, and stays gated on the experiment plus the editSettings permission.
1 parent 48e79c2 commit c0ddabe

7 files changed

Lines changed: 300 additions & 253 deletions

File tree

site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
33
import { useParams } from "react-router";
44
import { toast } from "sonner";
55
import { getErrorDetail, getErrorMessage } from "#/api/errors";
6+
import { updateOrganization } from "#/api/queries/organizations";
67
import { deleteOrganizationRole, organizationRoles } from "#/api/queries/roles";
78
import type { Role } from "#/api/typesGenerated";
89
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
@@ -12,6 +13,7 @@ import {
1213
SettingsHeaderDescription,
1314
SettingsHeaderTitle,
1415
} from "#/components/SettingsHeader/SettingsHeader";
16+
import { useDashboard } from "#/modules/dashboard/useDashboard";
1517
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
1618
import { useOrganizationSettings } from "#/modules/management/OrganizationSettingsLayout";
1719
import { RequirePermission } from "#/modules/permissions/RequirePermission";
@@ -25,6 +27,10 @@ const CustomRolesPage: FC = () => {
2527
organization: string;
2628
};
2729
const { organization, organizationPermissions } = useOrganizationSettings();
30+
const { experiments, entitlements } = useDashboard();
31+
const defaultRolesEnabled = experiments.includes("minimum-implicit-member");
32+
const defaultRolesEntitled =
33+
entitlements.features.multiple_organizations.enabled;
2834

2935
const [roleToDelete, setRoleToDelete] = useState<Role>();
3036

@@ -39,6 +45,9 @@ const CustomRolesPage: FC = () => {
3945
const deleteRoleMutation = useMutation(
4046
deleteOrganizationRole(queryClient, organizationName),
4147
);
48+
const updateOrganizationMutation = useMutation(
49+
updateOrganization(queryClient),
50+
);
4251

4352
useEffect(() => {
4453
if (organizationRolesQuery.error) {
@@ -80,13 +89,33 @@ const CustomRolesPage: FC = () => {
8089
</div>
8190

8291
<CustomRolesPageView
92+
organization={organization}
8393
builtInRoles={builtInRoles}
8494
customRoles={customRoles}
8595
onDeleteRole={setRoleToDelete}
8696
canCreateOrgRole={organizationPermissions?.createOrgRoles ?? false}
8797
canUpdateOrgRole={organizationPermissions?.updateOrgRoles ?? false}
8898
canDeleteOrgRole={organizationPermissions?.deleteOrgRoles ?? false}
99+
canEditDefaultRoles={organizationPermissions?.editSettings ?? false}
89100
isCustomRolesEnabled={isCustomRolesEnabled}
101+
defaultRolesEnabled={defaultRolesEnabled}
102+
defaultRolesEntitled={defaultRolesEntitled}
103+
availableOrgRoles={organizationRolesQuery.data}
104+
isUpdatingDefaultRoles={updateOrganizationMutation.isPending}
105+
onUpdateDefaultRoles={async (roles) => {
106+
try {
107+
await updateOrganizationMutation.mutateAsync({
108+
organizationId: organization.id,
109+
req: { default_org_member_roles: roles },
110+
});
111+
toast.success("Default roles updated.");
112+
} catch (error) {
113+
toast.error(
114+
getErrorMessage(error, "Failed to update default roles."),
115+
{ description: getErrorDetail(error) },
116+
);
117+
}
118+
}}
90119
/>
91120

92121
<DeleteDialog

site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,59 @@
11
import type { Meta, StoryObj } from "@storybook/react-vite";
2+
import { action } from "storybook/actions";
3+
import { expect, userEvent, within } from "storybook/test";
4+
import type { AssignableRoles } from "#/api/typesGenerated";
25
import {
6+
MockOrganization,
37
MockOrganizationAuditorRole,
48
MockRoleWithOrgPermissions,
59
} from "#/testHelpers/entities";
610
import { CustomRolesPageView } from "./CustomRolesPageView";
711

12+
const mockOrgRoles: AssignableRoles[] = [
13+
{
14+
name: "organization-workspace-access",
15+
display_name: "Organization Workspace Access",
16+
organization_id: MockOrganization.id,
17+
site_permissions: [],
18+
organization_permissions: [],
19+
organization_member_permissions: [],
20+
user_permissions: [],
21+
assignable: true,
22+
built_in: true,
23+
},
24+
{
25+
name: "organization-admin",
26+
display_name: "Organization Admin",
27+
organization_id: MockOrganization.id,
28+
site_permissions: [],
29+
organization_permissions: [],
30+
organization_member_permissions: [],
31+
user_permissions: [],
32+
assignable: true,
33+
built_in: true,
34+
},
35+
{
36+
name: "agents-access",
37+
display_name: "Agents Access",
38+
organization_id: MockOrganization.id,
39+
site_permissions: [],
40+
organization_permissions: [],
41+
organization_member_permissions: [],
42+
user_permissions: [],
43+
assignable: true,
44+
built_in: true,
45+
},
46+
];
47+
848
const meta: Meta<typeof CustomRolesPageView> = {
949
title: "pages/OrganizationCustomRolesPage",
1050
component: CustomRolesPageView,
1151
args: {
52+
organization: MockOrganization,
1253
builtInRoles: [MockRoleWithOrgPermissions],
1354
customRoles: [MockRoleWithOrgPermissions],
1455
canCreateOrgRole: true,
56+
canEditDefaultRoles: true,
1557
isCustomRolesEnabled: true,
1658
},
1759
};
@@ -66,3 +108,98 @@ export const EmptyTableUserWithPermission: Story = {
66108
customRoles: [],
67109
},
68110
};
111+
112+
export const DefaultRolesHidden: Story = {
113+
args: {
114+
defaultRolesEnabled: false,
115+
availableOrgRoles: mockOrgRoles,
116+
onUpdateDefaultRoles: async () => {
117+
action("onUpdateDefaultRoles")();
118+
},
119+
},
120+
play: async ({ canvasElement }) => {
121+
const body = within(canvasElement.ownerDocument.body);
122+
expect(body.queryByText("Default Roles")).toBeNull();
123+
},
124+
};
125+
126+
export const DefaultRolesEnabled: Story = {
127+
args: {
128+
defaultRolesEnabled: true,
129+
defaultRolesEntitled: true,
130+
availableOrgRoles: mockOrgRoles,
131+
onUpdateDefaultRoles: async () => {
132+
action("onUpdateDefaultRoles")();
133+
},
134+
},
135+
};
136+
137+
export const DefaultRolesNotEntitled: Story = {
138+
args: {
139+
defaultRolesEnabled: true,
140+
defaultRolesEntitled: false,
141+
availableOrgRoles: mockOrgRoles,
142+
onUpdateDefaultRoles: async () => {
143+
action("onUpdateDefaultRoles")();
144+
},
145+
},
146+
play: async ({ canvasElement }) => {
147+
const body = within(canvasElement.ownerDocument.body);
148+
const editButton = await body.findByRole("button", {
149+
name: /edit default roles/i,
150+
});
151+
expect(editButton).toBeDisabled();
152+
await body.findByText(/requires a Premium license/i);
153+
},
154+
};
155+
156+
export const DefaultRolesEmpty: Story = {
157+
args: {
158+
organization: {
159+
...MockOrganization,
160+
default_org_member_roles: [],
161+
},
162+
defaultRolesEnabled: true,
163+
defaultRolesEntitled: true,
164+
availableOrgRoles: mockOrgRoles,
165+
onUpdateDefaultRoles: async () => {
166+
action("onUpdateDefaultRoles")();
167+
},
168+
},
169+
};
170+
171+
export const DefaultRolesHiddenWithoutEditPermission: Story = {
172+
args: {
173+
defaultRolesEnabled: true,
174+
defaultRolesEntitled: true,
175+
canEditDefaultRoles: false,
176+
availableOrgRoles: mockOrgRoles,
177+
onUpdateDefaultRoles: async () => {
178+
action("onUpdateDefaultRoles")();
179+
},
180+
},
181+
play: async ({ canvasElement }) => {
182+
const body = within(canvasElement.ownerDocument.body);
183+
expect(body.queryByText("Default Roles")).toBeNull();
184+
},
185+
};
186+
187+
export const DefaultRolesEditDialog: Story = {
188+
args: {
189+
defaultRolesEnabled: true,
190+
defaultRolesEntitled: true,
191+
availableOrgRoles: mockOrgRoles,
192+
onUpdateDefaultRoles: async () => {
193+
action("onUpdateDefaultRoles")();
194+
},
195+
},
196+
play: async ({ canvasElement }) => {
197+
const user = userEvent.setup();
198+
const body = within(canvasElement.ownerDocument.body);
199+
const editButton = await body.findByRole("button", {
200+
name: /edit default roles/i,
201+
});
202+
await user.click(editButton);
203+
await body.findByRole("heading", { name: /edit default roles/i });
204+
},
205+
};

0 commit comments

Comments
 (0)