Skip to content

Commit 69f9b0e

Browse files
authored
feat(site): show org default roles in member role editor (#26107)
Surfaces the org's `default_org_member_roles` inside the org members role editor. These roles are implied, not physically assigned to any member. Just like the `member` role.
1 parent fa56224 commit 69f9b0e

4 files changed

Lines changed: 83 additions & 8 deletions

File tree

site/src/modules/roles/RoleSelector.stories.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,13 @@ export const OrganizationMemberRoles: Story = {
8989
availableRoles: orgMemberRoles,
9090
},
9191
};
92+
93+
export const WithAdditionalImpliedRoles: Story = {
94+
args: {
95+
availableRoles: orgMemberRoles,
96+
additionalImpliedRoles: [
97+
assignableRole(MockAgentsAccessRole, true),
98+
assignableRole(MockOrganizationAuditorRole, true),
99+
],
100+
},
101+
};

site/src/modules/roles/RoleSelector.tsx

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type RoleSelectorProps = {
1616
loading?: boolean;
1717
error?: unknown;
1818
availableRoles?: AssignableRoles[];
19+
additionalImpliedRoles?: AssignableRoles[];
1920
selectedRoles: Set<string>;
2021
onChange: (roles: Set<string>) => void;
2122
};
@@ -25,14 +26,15 @@ export const RoleSelector: FC<RoleSelectorProps> = ({
2526
loading,
2627
error,
2728
availableRoles = [],
29+
additionalImpliedRoles = [],
2830
selectedRoles,
2931
onChange,
3032
}) => {
3133
if (loading) {
3234
return (
3335
<RoleSelectorLayout>
3436
<RoleSelectorSkeleton />
35-
<MemberRole />
37+
<ImpliedRolesList additionalImpliedRoles={additionalImpliedRoles} />
3638
</RoleSelectorLayout>
3739
);
3840
}
@@ -49,8 +51,11 @@ export const RoleSelector: FC<RoleSelectorProps> = ({
4951
);
5052
}
5153

54+
const impliedRoleNames = new Set(additionalImpliedRoles.map((r) => r.name));
5255
const { selectableRoles = [], advancedRoles = [] } = Object.groupBy(
53-
availableRoles.filter((r) => r.name !== "member"),
56+
availableRoles.filter(
57+
(r) => r.name !== "member" && !impliedRoleNames.has(r.name),
58+
),
5459
(it) =>
5560
advancedRoleNames.includes(it.name) ? "advancedRoles" : "selectableRoles",
5661
);
@@ -80,7 +85,7 @@ export const RoleSelector: FC<RoleSelectorProps> = ({
8085
/>
8186
)}
8287

83-
<MemberRole />
88+
<ImpliedRolesList additionalImpliedRoles={additionalImpliedRoles} />
8489
</RoleSelectorLayout>
8590
);
8691
};
@@ -182,13 +187,46 @@ const RoleSelectorLayout: React.FC<RoleSelectorLayoutProps> = ({
182187
);
183188
};
184189

185-
const MemberRole: React.FC = () => {
190+
type ImpliedRolesListProps = {
191+
additionalImpliedRoles: AssignableRoles[];
192+
};
193+
194+
const ImpliedRolesList: React.FC<ImpliedRolesListProps> = ({
195+
additionalImpliedRoles,
196+
}) => {
197+
return (
198+
<>
199+
<ImpliedRoleRow title="Member" description={roleDescriptions.member} />
200+
{additionalImpliedRoles.map((role) => (
201+
<ImpliedRoleRow
202+
key={role.name}
203+
title={role.display_name || role.name}
204+
description={roleDescriptions[role.name] ?? ""}
205+
caption="Sourced from organization default roles"
206+
/>
207+
))}
208+
</>
209+
);
210+
};
211+
212+
type ImpliedRoleRowProps = {
213+
title: string;
214+
description: string;
215+
caption?: string;
216+
};
217+
218+
const ImpliedRoleRow: React.FC<ImpliedRoleRowProps> = ({
219+
title,
220+
description,
221+
caption,
222+
}) => {
186223
return (
187224
<div className="border-t border-border py-2 flex items-start gap-2 text-content-disabled">
188225
<UserIcon className="size-4 mt-1 shrink-0" />
189226
<div className="flex flex-col">
190-
<span className="text-sm font-medium">Member</span>
191-
<span className="text-sm">{roleDescriptions.member}</span>
227+
<span className="text-sm font-medium">{title}</span>
228+
{description && <span className="text-sm">{description}</span>}
229+
{caption && <span className="text-xs italic">{caption}</span>}
192230
</div>
193231
</div>
194232
);

site/src/modules/roles/RoleSelectorDialog.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type RoleSelectorDialogProps = {
2020
user?: ThingWithRoles;
2121
/** The roles available in this context that can be given or removed from the user */
2222
availableRoles?: AssignableRoles[];
23+
additionalImpliedRoles?: AssignableRoles[];
2324

2425
onCancel: () => void;
2526
onUpdateRoles: (roles: string[]) => Promise<void>;
@@ -36,6 +37,7 @@ type ThingWithRoles = {
3637
export const RoleSelectorDialog: React.FC<RoleSelectorDialogProps> = ({
3738
user,
3839
availableRoles = [],
40+
additionalImpliedRoles = [],
3941
onCancel,
4042
onUpdateRoles,
4143
isUpdatingRoles,
@@ -48,6 +50,7 @@ export const RoleSelectorDialog: React.FC<RoleSelectorDialogProps> = ({
4850
<ActiveRoleSelectorDialog
4951
user={user}
5052
availableRoles={availableRoles}
53+
additionalImpliedRoles={additionalImpliedRoles}
5154
onCancel={onCancel}
5255
onUpdateRoles={onUpdateRoles}
5356
isUpdatingRoles={isUpdatingRoles}
@@ -58,6 +61,7 @@ export const RoleSelectorDialog: React.FC<RoleSelectorDialogProps> = ({
5861
const ActiveRoleSelectorDialog: React.FC<Required<RoleSelectorDialogProps>> = ({
5962
user,
6063
availableRoles,
64+
additionalImpliedRoles,
6165
onCancel,
6266
onUpdateRoles,
6367
isUpdatingRoles,
@@ -89,6 +93,7 @@ const ActiveRoleSelectorDialog: React.FC<Required<RoleSelectorDialogProps>> = ({
8993
<RoleSelector
9094
hideLabel
9195
availableRoles={availableRoles}
96+
additionalImpliedRoles={additionalImpliedRoles}
9297
selectedRoles={selectedRoles}
9398
onChange={setSelectedRoles}
9499
/>

site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type FC, useState } from "react";
1+
import { type FC, useMemo, useState } from "react";
22
import { useMutation, useQuery, useQueryClient } from "react-query";
33
import { useParams, useSearchParams } from "react-router";
44
import { toast } from "sonner";
@@ -12,6 +12,7 @@ import {
1212
} from "#/api/queries/organizations";
1313
import { organizationRoles } from "#/api/queries/roles";
1414
import type {
15+
AssignableRoles,
1516
OrganizationMemberWithUserData,
1617
User,
1718
} from "#/api/typesGenerated";
@@ -35,9 +36,10 @@ const OrganizationMembersPage: FC = () => {
3536
organization: string;
3637
};
3738
const { organization, organizationPermissions } = useOrganizationSettings();
38-
const { entitlements } = useDashboard();
39+
const { entitlements, experiments } = useDashboard();
3940
const searchParamsResult = useSearchParams();
4041
const showAISeatColumn = shouldShowAISeatColumn(entitlements);
42+
const defaultRolesEnabled = experiments.includes("minimum-implicit-member");
4143

4244
const organizationRolesQuery = useQuery(organizationRoles(organizationName));
4345
const groupsByUserIdQuery = useQuery(
@@ -76,6 +78,25 @@ const OrganizationMembersPage: FC = () => {
7678
removeOrganizationMember(queryClient, organizationName),
7779
);
7880

81+
// Resolve the org's default member role names against the assignable
82+
// roles list so the dialog can show full display names + descriptions.
83+
const defaultMemberImpliedRoles = useMemo<AssignableRoles[]>(() => {
84+
if (!defaultRolesEnabled) {
85+
return [];
86+
}
87+
const available = organizationRolesQuery.data;
88+
if (!available) {
89+
return [];
90+
}
91+
return (organization?.default_org_member_roles ?? [])
92+
.map((name) => available.find((r) => r.name === name))
93+
.filter((r): r is AssignableRoles => r !== undefined);
94+
}, [
95+
defaultRolesEnabled,
96+
organization?.default_org_member_roles,
97+
organizationRolesQuery.data,
98+
]);
99+
79100
if (!organization) {
80101
return <EmptyState message="Organization not found" />;
81102
}
@@ -133,6 +154,7 @@ const OrganizationMembersPage: FC = () => {
133154
key={memberToEditRoles?.username}
134155
user={memberToEditRoles}
135156
availableRoles={organizationRolesQuery.data}
157+
additionalImpliedRoles={defaultMemberImpliedRoles}
136158
onCancel={() => setMemberToEditRoles(undefined)}
137159
onUpdateRoles={async (roles) => {
138160
try {

0 commit comments

Comments
 (0)