mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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.
This commit is contained in:
@@ -89,3 +89,13 @@ export const OrganizationMemberRoles: Story = {
|
||||
availableRoles: orgMemberRoles,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAdditionalImpliedRoles: Story = {
|
||||
args: {
|
||||
availableRoles: orgMemberRoles,
|
||||
additionalImpliedRoles: [
|
||||
assignableRole(MockAgentsAccessRole, true),
|
||||
assignableRole(MockOrganizationAuditorRole, true),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ type RoleSelectorProps = {
|
||||
loading?: boolean;
|
||||
error?: unknown;
|
||||
availableRoles?: AssignableRoles[];
|
||||
additionalImpliedRoles?: AssignableRoles[];
|
||||
selectedRoles: Set<string>;
|
||||
onChange: (roles: Set<string>) => void;
|
||||
};
|
||||
@@ -25,6 +26,7 @@ export const RoleSelector: FC<RoleSelectorProps> = ({
|
||||
loading,
|
||||
error,
|
||||
availableRoles = [],
|
||||
additionalImpliedRoles = [],
|
||||
selectedRoles,
|
||||
onChange,
|
||||
}) => {
|
||||
@@ -32,7 +34,7 @@ export const RoleSelector: FC<RoleSelectorProps> = ({
|
||||
return (
|
||||
<RoleSelectorLayout>
|
||||
<RoleSelectorSkeleton />
|
||||
<MemberRole />
|
||||
<ImpliedRolesList additionalImpliedRoles={additionalImpliedRoles} />
|
||||
</RoleSelectorLayout>
|
||||
);
|
||||
}
|
||||
@@ -49,8 +51,11 @@ export const RoleSelector: FC<RoleSelectorProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
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<RoleSelectorProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
<MemberRole />
|
||||
<ImpliedRolesList additionalImpliedRoles={additionalImpliedRoles} />
|
||||
</RoleSelectorLayout>
|
||||
);
|
||||
};
|
||||
@@ -182,13 +187,46 @@ const RoleSelectorLayout: React.FC<RoleSelectorLayoutProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const MemberRole: React.FC = () => {
|
||||
type ImpliedRolesListProps = {
|
||||
additionalImpliedRoles: AssignableRoles[];
|
||||
};
|
||||
|
||||
const ImpliedRolesList: React.FC<ImpliedRolesListProps> = ({
|
||||
additionalImpliedRoles,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<ImpliedRoleRow title="Member" description={roleDescriptions.member} />
|
||||
{additionalImpliedRoles.map((role) => (
|
||||
<ImpliedRoleRow
|
||||
key={role.name}
|
||||
title={role.display_name || role.name}
|
||||
description={roleDescriptions[role.name] ?? ""}
|
||||
caption="Sourced from organization default roles"
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type ImpliedRoleRowProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
caption?: string;
|
||||
};
|
||||
|
||||
const ImpliedRoleRow: React.FC<ImpliedRoleRowProps> = ({
|
||||
title,
|
||||
description,
|
||||
caption,
|
||||
}) => {
|
||||
return (
|
||||
<div className="border-t border-border py-2 flex items-start gap-2 text-content-disabled">
|
||||
<UserIcon className="size-4 mt-1 shrink-0" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">Member</span>
|
||||
<span className="text-sm">{roleDescriptions.member}</span>
|
||||
<span className="text-sm font-medium">{title}</span>
|
||||
{description && <span className="text-sm">{description}</span>}
|
||||
{caption && <span className="text-xs italic">{caption}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<void>;
|
||||
@@ -36,6 +37,7 @@ type ThingWithRoles = {
|
||||
export const RoleSelectorDialog: React.FC<RoleSelectorDialogProps> = ({
|
||||
user,
|
||||
availableRoles = [],
|
||||
additionalImpliedRoles = [],
|
||||
onCancel,
|
||||
onUpdateRoles,
|
||||
isUpdatingRoles,
|
||||
@@ -48,6 +50,7 @@ export const RoleSelectorDialog: React.FC<RoleSelectorDialogProps> = ({
|
||||
<ActiveRoleSelectorDialog
|
||||
user={user}
|
||||
availableRoles={availableRoles}
|
||||
additionalImpliedRoles={additionalImpliedRoles}
|
||||
onCancel={onCancel}
|
||||
onUpdateRoles={onUpdateRoles}
|
||||
isUpdatingRoles={isUpdatingRoles}
|
||||
@@ -58,6 +61,7 @@ export const RoleSelectorDialog: React.FC<RoleSelectorDialogProps> = ({
|
||||
const ActiveRoleSelectorDialog: React.FC<Required<RoleSelectorDialogProps>> = ({
|
||||
user,
|
||||
availableRoles,
|
||||
additionalImpliedRoles,
|
||||
onCancel,
|
||||
onUpdateRoles,
|
||||
isUpdatingRoles,
|
||||
@@ -89,6 +93,7 @@ const ActiveRoleSelectorDialog: React.FC<Required<RoleSelectorDialogProps>> = ({
|
||||
<RoleSelector
|
||||
hideLabel
|
||||
availableRoles={availableRoles}
|
||||
additionalImpliedRoles={additionalImpliedRoles}
|
||||
selectedRoles={selectedRoles}
|
||||
onChange={setSelectedRoles}
|
||||
/>
|
||||
|
||||
@@ -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<AssignableRoles[]>(() => {
|
||||
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 <EmptyState message="Organization not found" />;
|
||||
}
|
||||
@@ -133,6 +154,7 @@ const OrganizationMembersPage: FC = () => {
|
||||
key={memberToEditRoles?.username}
|
||||
user={memberToEditRoles}
|
||||
availableRoles={organizationRolesQuery.data}
|
||||
additionalImpliedRoles={defaultMemberImpliedRoles}
|
||||
onCancel={() => setMemberToEditRoles(undefined)}
|
||||
onUpdateRoles={async (roles) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user