mirror of https://github.com/coder/coder.git
232 lines
5.7 KiB
TypeScript
232 lines
5.7 KiB
TypeScript
import type { Interpolation, Theme } from "@emotion/react";
|
|
import UserIcon from "@mui/icons-material/PersonOutline";
|
|
import Checkbox from "@mui/material/Checkbox";
|
|
import IconButton from "@mui/material/IconButton";
|
|
import type { FC } from "react";
|
|
import type { Role } from "api/typesGenerated";
|
|
import {
|
|
HelpTooltip,
|
|
HelpTooltipContent,
|
|
HelpTooltipText,
|
|
HelpTooltipTitle,
|
|
HelpTooltipTrigger,
|
|
} from "components/HelpTooltip/HelpTooltip";
|
|
import { EditSquare } from "components/Icons/EditSquare";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "components/Popover/Popover";
|
|
import { Stack } from "components/Stack/Stack";
|
|
import { type ClassName, useClassName } from "hooks/useClassName";
|
|
|
|
const roleDescriptions: Record<string, string> = {
|
|
owner:
|
|
"Owner can manage all resources, including users, groups, templates, and workspaces.",
|
|
"user-admin": "User admin can manage all users and groups.",
|
|
"template-admin": "Template admin can manage all templates and workspaces.",
|
|
auditor: "Auditor can access the audit logs.",
|
|
member:
|
|
"Everybody is a member. This is a shared and default role for all users.",
|
|
};
|
|
|
|
interface OptionProps {
|
|
value: string;
|
|
name: string;
|
|
description: string;
|
|
isChecked: boolean;
|
|
onChange: (roleName: string) => void;
|
|
}
|
|
|
|
const Option: FC<OptionProps> = ({
|
|
value,
|
|
name,
|
|
description,
|
|
isChecked,
|
|
onChange,
|
|
}) => {
|
|
return (
|
|
<label htmlFor={name} css={styles.option}>
|
|
<Stack direction="row" alignItems="flex-start">
|
|
<Checkbox
|
|
id={name}
|
|
size="small"
|
|
css={styles.checkbox}
|
|
value={value}
|
|
checked={isChecked}
|
|
onChange={(e) => {
|
|
onChange(e.currentTarget.value);
|
|
}}
|
|
/>
|
|
<Stack spacing={0}>
|
|
<strong>{name}</strong>
|
|
<span css={styles.optionDescription}>{description}</span>
|
|
</Stack>
|
|
</Stack>
|
|
</label>
|
|
);
|
|
};
|
|
|
|
export interface EditRolesButtonProps {
|
|
isLoading: boolean;
|
|
roles: readonly Role[];
|
|
selectedRoleNames: Set<string>;
|
|
onChange: (roles: Role["name"][]) => void;
|
|
isDefaultOpen?: boolean;
|
|
oidcRoleSync: boolean;
|
|
userLoginType: string;
|
|
}
|
|
|
|
export const EditRolesButton: FC<EditRolesButtonProps> = ({
|
|
roles,
|
|
selectedRoleNames,
|
|
onChange,
|
|
isLoading,
|
|
isDefaultOpen = false,
|
|
userLoginType,
|
|
oidcRoleSync,
|
|
}) => {
|
|
const paper = useClassName(classNames.paper, []);
|
|
|
|
const handleChange = (roleName: string) => {
|
|
if (selectedRoleNames.has(roleName)) {
|
|
const serialized = [...selectedRoleNames];
|
|
onChange(serialized.filter((role) => role !== roleName));
|
|
return;
|
|
}
|
|
|
|
onChange([...selectedRoleNames, roleName]);
|
|
};
|
|
|
|
const canSetRoles =
|
|
userLoginType !== "oidc" || (userLoginType === "oidc" && !oidcRoleSync);
|
|
|
|
if (!canSetRoles) {
|
|
return (
|
|
<HelpTooltip>
|
|
<HelpTooltipTrigger size="small" />
|
|
<HelpTooltipContent>
|
|
<HelpTooltipTitle>Externally controlled</HelpTooltipTitle>
|
|
<HelpTooltipText>
|
|
Roles for this user are controlled by the OIDC identity provider.
|
|
</HelpTooltipText>
|
|
</HelpTooltipContent>
|
|
</HelpTooltip>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Popover isDefaultOpen={isDefaultOpen}>
|
|
<PopoverTrigger>
|
|
<IconButton
|
|
size="small"
|
|
css={styles.editButton}
|
|
title="Edit user roles"
|
|
>
|
|
<EditSquare />
|
|
</IconButton>
|
|
</PopoverTrigger>
|
|
|
|
<PopoverContent classes={{ paper }}>
|
|
<fieldset
|
|
css={styles.fieldset}
|
|
disabled={isLoading}
|
|
title="Available roles"
|
|
>
|
|
<Stack css={styles.options} spacing={3}>
|
|
{roles.map((role) => (
|
|
<Option
|
|
key={role.name}
|
|
onChange={handleChange}
|
|
isChecked={selectedRoleNames.has(role.name)}
|
|
value={role.name}
|
|
name={role.display_name}
|
|
description={roleDescriptions[role.name] ?? ""}
|
|
/>
|
|
))}
|
|
</Stack>
|
|
</fieldset>
|
|
<div css={styles.footer}>
|
|
<Stack direction="row" alignItems="flex-start">
|
|
<UserIcon css={styles.userIcon} />
|
|
<Stack spacing={0}>
|
|
<strong>Member</strong>
|
|
<span css={styles.optionDescription}>
|
|
{roleDescriptions.member}
|
|
</span>
|
|
</Stack>
|
|
</Stack>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
};
|
|
|
|
const classNames = {
|
|
paper: (css, theme) => css`
|
|
width: 360px;
|
|
margin-top: 8px;
|
|
background: ${theme.palette.background.paper};
|
|
`,
|
|
} satisfies Record<string, ClassName>;
|
|
|
|
const styles = {
|
|
editButton: (theme) => ({
|
|
color: theme.palette.text.secondary,
|
|
|
|
"& .MuiSvgIcon-root": {
|
|
width: 16,
|
|
height: 16,
|
|
position: "relative",
|
|
top: -2, // Align the pencil square
|
|
},
|
|
|
|
"&:hover": {
|
|
color: theme.palette.text.primary,
|
|
backgroundColor: "transparent",
|
|
},
|
|
}),
|
|
fieldset: {
|
|
border: 0,
|
|
margin: 0,
|
|
padding: 0,
|
|
|
|
"&:disabled": {
|
|
opacity: 0.5,
|
|
},
|
|
},
|
|
options: {
|
|
padding: 24,
|
|
},
|
|
option: {
|
|
cursor: "pointer",
|
|
fontSize: 14,
|
|
},
|
|
checkbox: {
|
|
padding: 0,
|
|
position: "relative",
|
|
top: 1, // Alignment
|
|
|
|
"& svg": {
|
|
width: 20,
|
|
height: 20,
|
|
},
|
|
},
|
|
optionDescription: (theme) => ({
|
|
fontSize: 13,
|
|
color: theme.palette.text.secondary,
|
|
lineHeight: "160%",
|
|
}),
|
|
footer: (theme) => ({
|
|
padding: 24,
|
|
backgroundColor: theme.palette.background.paper,
|
|
borderTop: `1px solid ${theme.palette.divider}`,
|
|
fontSize: 14,
|
|
}),
|
|
userIcon: (theme) => ({
|
|
width: 20, // Same as the checkbox
|
|
height: 20,
|
|
color: theme.palette.primary.main,
|
|
}),
|
|
} satisfies Record<string, Interpolation<Theme>>;
|