From 69f9b0e53584fd5a064de078f0dbe41d6cc468b2 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 5 Jun 2026 20:51:20 -0500 Subject: [PATCH] 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. --- .../modules/roles/RoleSelector.stories.tsx | 10 ++++ site/src/modules/roles/RoleSelector.tsx | 50 ++++++++++++++++--- site/src/modules/roles/RoleSelectorDialog.tsx | 5 ++ .../OrganizationMembersPage.tsx | 26 +++++++++- 4 files changed, 83 insertions(+), 8 deletions(-) diff --git a/site/src/modules/roles/RoleSelector.stories.tsx b/site/src/modules/roles/RoleSelector.stories.tsx index 4e3a26e2f9..c31f75444d 100644 --- a/site/src/modules/roles/RoleSelector.stories.tsx +++ b/site/src/modules/roles/RoleSelector.stories.tsx @@ -89,3 +89,13 @@ export const OrganizationMemberRoles: Story = { availableRoles: orgMemberRoles, }, }; + +export const WithAdditionalImpliedRoles: Story = { + args: { + availableRoles: orgMemberRoles, + additionalImpliedRoles: [ + assignableRole(MockAgentsAccessRole, true), + assignableRole(MockOrganizationAuditorRole, true), + ], + }, +}; diff --git a/site/src/modules/roles/RoleSelector.tsx b/site/src/modules/roles/RoleSelector.tsx index 93e1e2510b..82ecb7420d 100644 --- a/site/src/modules/roles/RoleSelector.tsx +++ b/site/src/modules/roles/RoleSelector.tsx @@ -16,6 +16,7 @@ type RoleSelectorProps = { loading?: boolean; error?: unknown; availableRoles?: AssignableRoles[]; + additionalImpliedRoles?: AssignableRoles[]; selectedRoles: Set; onChange: (roles: Set) => void; }; @@ -25,6 +26,7 @@ export const RoleSelector: FC = ({ loading, error, availableRoles = [], + additionalImpliedRoles = [], selectedRoles, onChange, }) => { @@ -32,7 +34,7 @@ export const RoleSelector: FC = ({ return ( - + ); } @@ -49,8 +51,11 @@ export const RoleSelector: FC = ({ ); } + const impliedRoleNames = new Set(additionalImpliedRoles.map((r) => r.name)); const { selectableRoles = [], advancedRoles = [] } = Object.groupBy( - availableRoles.filter((r) => r.name !== "member"), + availableRoles.filter( + (r) => r.name !== "member" && !impliedRoleNames.has(r.name), + ), (it) => advancedRoleNames.includes(it.name) ? "advancedRoles" : "selectableRoles", ); @@ -80,7 +85,7 @@ export const RoleSelector: FC = ({ /> )} - + ); }; @@ -182,13 +187,46 @@ const RoleSelectorLayout: React.FC = ({ ); }; -const MemberRole: React.FC = () => { +type ImpliedRolesListProps = { + additionalImpliedRoles: AssignableRoles[]; +}; + +const ImpliedRolesList: React.FC = ({ + additionalImpliedRoles, +}) => { + return ( + <> + + {additionalImpliedRoles.map((role) => ( + + ))} + + ); +}; + +type ImpliedRoleRowProps = { + title: string; + description: string; + caption?: string; +}; + +const ImpliedRoleRow: React.FC = ({ + title, + description, + caption, +}) => { return (
- Member - {roleDescriptions.member} + {title} + {description && {description}} + {caption && {caption}}
); diff --git a/site/src/modules/roles/RoleSelectorDialog.tsx b/site/src/modules/roles/RoleSelectorDialog.tsx index 4a306b018d..803f921671 100644 --- a/site/src/modules/roles/RoleSelectorDialog.tsx +++ b/site/src/modules/roles/RoleSelectorDialog.tsx @@ -20,6 +20,7 @@ type RoleSelectorDialogProps = { user?: ThingWithRoles; /** The roles available in this context that can be given or removed from the user */ availableRoles?: AssignableRoles[]; + additionalImpliedRoles?: AssignableRoles[]; onCancel: () => void; onUpdateRoles: (roles: string[]) => Promise; @@ -36,6 +37,7 @@ type ThingWithRoles = { export const RoleSelectorDialog: React.FC = ({ user, availableRoles = [], + additionalImpliedRoles = [], onCancel, onUpdateRoles, isUpdatingRoles, @@ -48,6 +50,7 @@ export const RoleSelectorDialog: React.FC = ({ = ({ const ActiveRoleSelectorDialog: React.FC> = ({ user, availableRoles, + additionalImpliedRoles, onCancel, onUpdateRoles, isUpdatingRoles, @@ -89,6 +93,7 @@ const ActiveRoleSelectorDialog: React.FC> = ({ diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx index be020df108..524e1d28f2 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx @@ -1,4 +1,4 @@ -import { type FC, useState } from "react"; +import { type FC, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { useParams, useSearchParams } from "react-router"; import { toast } from "sonner"; @@ -12,6 +12,7 @@ import { } from "#/api/queries/organizations"; import { organizationRoles } from "#/api/queries/roles"; import type { + AssignableRoles, OrganizationMemberWithUserData, User, } from "#/api/typesGenerated"; @@ -35,9 +36,10 @@ const OrganizationMembersPage: FC = () => { organization: string; }; const { organization, organizationPermissions } = useOrganizationSettings(); - const { entitlements } = useDashboard(); + const { entitlements, experiments } = useDashboard(); const searchParamsResult = useSearchParams(); const showAISeatColumn = shouldShowAISeatColumn(entitlements); + const defaultRolesEnabled = experiments.includes("minimum-implicit-member"); const organizationRolesQuery = useQuery(organizationRoles(organizationName)); const groupsByUserIdQuery = useQuery( @@ -76,6 +78,25 @@ const OrganizationMembersPage: FC = () => { removeOrganizationMember(queryClient, organizationName), ); + // Resolve the org's default member role names against the assignable + // roles list so the dialog can show full display names + descriptions. + const defaultMemberImpliedRoles = useMemo(() => { + if (!defaultRolesEnabled) { + return []; + } + const available = organizationRolesQuery.data; + if (!available) { + return []; + } + return (organization?.default_org_member_roles ?? []) + .map((name) => available.find((r) => r.name === name)) + .filter((r): r is AssignableRoles => r !== undefined); + }, [ + defaultRolesEnabled, + organization?.default_org_member_roles, + organizationRolesQuery.data, + ]); + if (!organization) { return ; } @@ -133,6 +154,7 @@ const OrganizationMembersPage: FC = () => { key={memberToEditRoles?.username} user={memberToEditRoles} availableRoles={organizationRolesQuery.data} + additionalImpliedRoles={defaultMemberImpliedRoles} onCancel={() => setMemberToEditRoles(undefined)} onUpdateRoles={async (roles) => { try {