Skip to content

Commit 86c86ad

Browse files
committed
the stuff
1 parent 23216d1 commit 86c86ad

File tree

12 files changed

+152
-79
lines changed

12 files changed

+152
-79
lines changed

site/src/api/api.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,27 @@ class ApiMethods {
549549
return response.data;
550550
};
551551

552+
getOrganizationRoles = async (organizationId: string) => {
553+
const response = await this.axios.get<TypesGen.AssignableRoles[]>(
554+
`/api/v2/organizations/${organizationId}/members/roles`,
555+
);
556+
557+
return response.data;
558+
};
559+
560+
updateOrganizationMemberRoles = async (
561+
organizationId: string,
562+
userId: string,
563+
roles: TypesGen.SlimRole["name"][],
564+
): Promise<TypesGen.User> => {
565+
const response = await this.axios.put<TypesGen.User>(
566+
`/api/v2/organizations/${organizationId}/members/${userId}/roles`,
567+
{ roles },
568+
);
569+
570+
return response.data;
571+
};
572+
552573
addOrganizationMember = async (organizationId: string, userId: string) => {
553574
const response = await this.axios.post<TypesGen.OrganizationMember>(
554575
`/api/v2/organizations/${organizationId}/members/${userId}`,

site/src/api/queries/organizations.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const deleteOrganization = (queryClient: QueryClient) => {
4949
export const organizationMembers = (id: string) => {
5050
return {
5151
queryFn: () => API.getOrganizationMembers(id),
52-
key: ["organization", id, "members"],
52+
queryKey: ["organization", id, "members"],
5353
};
5454
};
5555

@@ -80,6 +80,25 @@ export const removeOrganizationMember = (
8080
};
8181
};
8282

83+
export const updateOrganizationMemberRoles = (
84+
queryClient: QueryClient,
85+
organizationId: string,
86+
) => {
87+
return {
88+
mutationFn: ({ userId, roles }: { userId: string; roles: string[] }) => {
89+
return API.updateOrganizationMemberRoles(organizationId, userId, roles);
90+
},
91+
92+
onSuccess: async () => {
93+
await queryClient.invalidateQueries([
94+
"organization",
95+
organizationId,
96+
"members",
97+
]);
98+
},
99+
};
100+
};
101+
83102
export const organizationsKey = ["organizations"] as const;
84103

85104
export const organizations = () => {

site/src/api/queries/roles.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,10 @@ export const roles = () => {
66
queryFn: API.getRoles,
77
};
88
};
9+
10+
export const organizationRoles = (organizationId: string) => {
11+
return {
12+
queryKey: ["organizationRoles"],
13+
queryFn: () => API.getOrganizationRoles(organizationId),
14+
};
15+
};

site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx

Lines changed: 30 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ import {
1616
addOrganizationMember,
1717
organizationMembers,
1818
removeOrganizationMember,
19+
updateOrganizationMemberRoles,
1920
} from "api/queries/organizations";
20-
import type { OrganizationMemberWithUserData, User } from "api/typesGenerated";
21+
import type { User } from "api/typesGenerated";
2122
import { ErrorAlert } from "components/Alert/ErrorAlert";
2223
import { AvatarData } from "components/AvatarData/AvatarData";
23-
import { displayError } from "components/GlobalSnackbar/utils";
24+
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
2425
import {
2526
MoreMenu,
2627
MoreMenuTrigger,
@@ -29,27 +30,31 @@ import {
2930
ThreeDotsButton,
3031
} from "components/MoreMenu/MoreMenu";
3132
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
32-
import { Pill } from "components/Pill/Pill";
3333
import { Stack } from "components/Stack/Stack";
3434
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
3535
import { UserAvatar } from "components/UserAvatar/UserAvatar";
3636
import { useAuthenticated } from "contexts/auth/RequireAuth";
37-
import { TableColumnHelpTooltip } from "./TableColumnHelpTooltip";
38-
import { groupsByUserId } from "api/queries/groups";
37+
import { TableColumnHelpTooltip } from "./UserTable/TableColumnHelpTooltip";
38+
import { organizationRoles } from "api/queries/roles";
39+
import { UserRoleCell } from "./UserTable/UserRoleCell";
3940

4041
const OrganizationMembersPage: FC = () => {
4142
const queryClient = useQueryClient();
4243
const { organization } = useParams() as { organization: string };
4344
const { user: me } = useAuthenticated();
4445

4546
const membersQuery = useQuery(organizationMembers(organization));
46-
// const groupsByUserIdQuery = useQuery(groupsByUserId(organization));
47+
const organizationRolesQuery = useQuery(organizationRoles(organization));
48+
4749
const addMemberMutation = useMutation(
4850
addOrganizationMember(queryClient, organization),
4951
);
5052
const removeMemberMutation = useMutation(
5153
removeOrganizationMember(queryClient, organization),
5254
);
55+
const updateMemberRolesMutation = useMutation(
56+
updateOrganizationMemberRoles(queryClient, organization),
57+
);
5358

5459
const error =
5560
membersQuery.error ?? addMemberMutation.error ?? removeMemberMutation.error;
@@ -101,22 +106,25 @@ const OrganizationMembersPage: FC = () => {
101106
subtitle={member.email}
102107
/>
103108
</TableCell>
104-
<TableCell>
105-
{getMemberRoles(member).map((role) => (
106-
<Pill
107-
key={role.name}
108-
css={role.global ? styles.globalRole : styles.role}
109-
>
110-
{role.global ? (
111-
<Tooltip title="This user has this role for all organizations.">
112-
<span>{role.name}*</span>
113-
</Tooltip>
114-
) : (
115-
role.name
116-
)}
117-
</Pill>
118-
))}
119-
</TableCell>
109+
<UserRoleCell
110+
user={{
111+
id: member.user_id,
112+
login_type: "",
113+
}}
114+
inheritedRoles={member.global_roles}
115+
roles={member.roles}
116+
allAvailableRoles={organizationRolesQuery.data}
117+
oidcRoleSyncEnabled={false}
118+
isLoading={organizationRolesQuery.isLoading}
119+
canEditUsers
120+
onUserRolesUpdate={async (userId, newRoleNames) => {
121+
await updateMemberRolesMutation.mutateAsync({
122+
userId,
123+
roles: newRoleNames,
124+
});
125+
displaySuccess("Roles updated successfully.");
126+
}}
127+
/>
120128
<TableCell>
121129
{member.user_id !== me.id && (
122130
<MoreMenu>
@@ -149,25 +157,6 @@ const OrganizationMembersPage: FC = () => {
149157
);
150158
};
151159

152-
function getMemberRoles(member: OrganizationMemberWithUserData) {
153-
const roles = new Map<string, { name: string; global?: boolean }>();
154-
155-
for (const role of member.global_roles) {
156-
roles.set(role.name, {
157-
name: role.display_name || role.name,
158-
global: true,
159-
});
160-
}
161-
for (const role of member.roles) {
162-
if (roles.has(role.name)) {
163-
continue;
164-
}
165-
roles.set(role.name, { name: role.display_name || role.name });
166-
}
167-
168-
return [...roles.values()];
169-
}
170-
171160
export default OrganizationMembersPage;
172161

173162
interface AddOrganizationMemberProps {

site/src/pages/UsersPage/UsersTable/UserRoleCell.tsx renamed to site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
* went with a simpler design. If we decide we really do need to display the
1414
* users like that, though, know that it will be painful
1515
*/
16-
import { useTheme } from "@emotion/react";
16+
import { Interpolation, Theme, useTheme } from "@emotion/react";
1717
import Stack from "@mui/material/Stack";
1818
import TableCell from "@mui/material/TableCell";
19+
import Tooltip from "@mui/material/Tooltip";
1920
import type { FC } from "react";
2021
import type { SlimRole, User } from "api/typesGenerated";
2122
import { Pill } from "components/Pill/Pill";
@@ -27,26 +28,29 @@ import {
2728
import { EditRolesButton } from "./EditRolesButton";
2829

2930
type UserRoleCellProps = {
30-
canEditUsers: boolean;
31-
allAvailableRoles: SlimRole[] | undefined;
32-
user: User;
3331
isLoading: boolean;
32+
canEditUsers: boolean;
33+
allAvailableRoles: readonly SlimRole[] | undefined;
34+
user: Pick<User, "id" | "login_type">;
35+
inheritedRoles?: readonly SlimRole[];
36+
roles: readonly SlimRole[];
3437
oidcRoleSyncEnabled: boolean;
35-
onUserRolesUpdate: (user: User, newRoleNames: string[]) => void;
38+
onUserRolesUpdate: (userId: string, newRoleNames: string[]) => void;
3639
};
3740

3841
export const UserRoleCell: FC<UserRoleCellProps> = ({
42+
isLoading,
3943
canEditUsers,
4044
allAvailableRoles,
4145
user,
42-
isLoading,
46+
inheritedRoles,
47+
roles,
4348
oidcRoleSyncEnabled,
4449
onUserRolesUpdate,
4550
}) => {
46-
const theme = useTheme();
47-
51+
const theRolesForReal = getMergedRoles(inheritedRoles ?? [], roles);
4852
const [mainDisplayRole = fallbackRole, ...extraRoles] =
49-
sortRolesByAccessLevel(user.roles ?? []);
53+
sortRolesByAccessLevel(theRolesForReal ?? []);
5054
const hasOwnerRole = mainDisplayRole.name === "owner";
5155

5256
return (
@@ -55,7 +59,7 @@ export const UserRoleCell: FC<UserRoleCellProps> = ({
5559
{canEditUsers && (
5660
<EditRolesButton
5761
roles={sortRolesByAccessLevel(allAvailableRoles ?? [])}
58-
selectedRoleNames={getSelectedRoleNames(user.roles)}
62+
selectedRoleNames={getSelectedRoleNames(roles)}
5963
isLoading={isLoading}
6064
userLoginType={user.login_type}
6165
oidcRoleSync={oidcRoleSyncEnabled}
@@ -65,22 +69,19 @@ export const UserRoleCell: FC<UserRoleCellProps> = ({
6569
(role) => role !== fallbackRole.name,
6670
);
6771

68-
onUserRolesUpdate(user, rolesWithoutFallback);
72+
onUserRolesUpdate(user.id, rolesWithoutFallback);
6973
}}
7074
/>
7175
)}
7276

73-
<Pill
74-
css={{
75-
backgroundColor: hasOwnerRole
76-
? theme.roles.info.background
77-
: theme.experimental.l2.background,
78-
borderColor: hasOwnerRole
79-
? theme.roles.info.outline
80-
: theme.experimental.l2.outline,
81-
}}
82-
>
83-
{mainDisplayRole.display_name}
77+
<Pill css={hasOwnerRole ? styles.ownerRoleBadge : styles.roleBadge}>
78+
{mainDisplayRole.global ? (
79+
<Tooltip title="This user has this role for all organizations.">
80+
<span>{mainDisplayRole.display_name}*</span>
81+
</Tooltip>
82+
) : (
83+
mainDisplayRole.display_name
84+
)}
8485
</Pill>
8586

8687
{extraRoles.length > 0 && <OverflowRolePill roles={extraRoles} />}
@@ -105,7 +106,7 @@ const OverflowRolePill: FC<OverflowRolePillProps> = ({ roles }) => {
105106
borderColor: theme.palette.divider,
106107
}}
107108
>
108-
{`+${roles.length} more`}
109+
+{roles.length} more
109110
</Pill>
110111
</PopoverTrigger>
111112

@@ -148,9 +149,21 @@ const OverflowRolePill: FC<OverflowRolePillProps> = ({ roles }) => {
148149
);
149150
};
150151

151-
const fallbackRole: SlimRole = {
152+
const styles = {
153+
ownerRoleBadge: (theme) => ({
154+
backgroundColor: theme.roles.info.background,
155+
borderColor: theme.roles.info.outline,
156+
}),
157+
roleBadge: (theme) => ({
158+
backgroundColor: theme.experimental.l2.background,
159+
borderColor: theme.experimental.l2.outline,
160+
}),
161+
} satisfies Record<string, Interpolation<Theme>>;
162+
163+
const fallbackRole: MergedSlimRole = {
152164
name: "member",
153165
display_name: "Member",
166+
global: false,
154167
} as const;
155168

156169
const roleNamesByAccessLevel: readonly string[] = [
@@ -160,9 +173,9 @@ const roleNamesByAccessLevel: readonly string[] = [
160173
"auditor",
161174
];
162175

163-
function sortRolesByAccessLevel(
164-
roles: readonly SlimRole[],
165-
): readonly SlimRole[] {
176+
function sortRolesByAccessLevel<T extends SlimRole>(
177+
roles: readonly T[],
178+
): readonly T[] {
166179
if (roles.length === 0) {
167180
return roles;
168181
}
@@ -182,3 +195,29 @@ function getSelectedRoleNames(roles: readonly SlimRole[]) {
182195

183196
return roleNameSet;
184197
}
198+
199+
interface MergedSlimRole extends SlimRole {
200+
global?: boolean;
201+
}
202+
203+
function getMergedRoles(
204+
globalRoles: readonly SlimRole[],
205+
localRoles: readonly SlimRole[],
206+
) {
207+
const roles = new Map<string, MergedSlimRole>();
208+
209+
for (const role of globalRoles) {
210+
roles.set(role.name, {
211+
...role,
212+
global: true,
213+
});
214+
}
215+
for (const role of localRoles) {
216+
if (roles.has(role.name)) {
217+
continue;
218+
}
219+
roles.set(role.name, role);
220+
}
221+
222+
return [...roles.values()];
223+
}

site/src/pages/UsersPage/UsersPage.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,9 @@ export const UsersPage: FC = () => {
125125
newPassword: generateRandomString(12),
126126
});
127127
}}
128-
onUpdateUserRoles={async (user, roles) => {
128+
onUpdateUserRoles={async (userId, roles) => {
129129
try {
130-
await updateRolesMutation.mutateAsync({
131-
userId: user.id,
132-
roles,
133-
});
130+
await updateRolesMutation.mutateAsync({ userId, roles });
134131
displaySuccess("Successfully updated the user roles.");
135132
} catch (e) {
136133
displayError(

site/src/pages/UsersPage/UsersPageView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export interface UsersPageViewProps {
2424
onActivateUser: (user: TypesGen.User) => void;
2525
onResetUserPassword: (user: TypesGen.User) => void;
2626
onUpdateUserRoles: (
27-
user: TypesGen.User,
27+
userId: string,
2828
roles: TypesGen.SlimRole["name"][],
2929
) => void;
3030
filterProps: ComponentProps<typeof UsersFilter>;

0 commit comments

Comments
 (0)