mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add role selector in the create user form (#24711)
Adds a role selector to the create user form so admins can assign site-level roles at creation time rather than navigating to the user afterward. The `POST /api/v2/users` endpoint now accepts an optional `roles` field, wiring it through to the existing `RBACRoles` field on the internal `CreateUserRequest`. No database changes are needed since roles are already stored inline on the user row. On the frontend, a `RoleSelector` component renders the assignable roles as a scrollable multiselect checklist with the non-assignable Member role pinned as a non-interactive footer. The selector appears once a login type is chosen. Also adds a `condensed` size (690px) to `Margins` between the existing `small` (460px) and `medium` (1080px), and exposes a `size` prop on `FullPageForm`. The create user form uses `condensed` to give the role selector more breathing room. Also fixes `MockUserAdminRole` and `MockTemplateAdminRole` in test helpers to use hyphenated names (`user-admin`, `template-admin`) matching the canonical names in the Go RBAC layer. Fixes `sortRolesByAccessLevel` in `UserRoleCell` to sort unranked roles (e.g. `member`) after all known roles. Previously, `indexOf` returned -1 for unknown names, placing them first; now they receive `POSITIVE_INFINITY` as their rank. 🤖 Generated with [Claude Code](<https://claude.ai/claude-code>) --- https://github.com/user-attachments/assets/75e7c8c5-d0d2-481d-86e8-1fcfb574517c --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Generated
+7
@@ -15411,6 +15411,13 @@ const docTemplate = `{
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"roles": {
|
||||
"description": "Roles is an optional list of site-level roles to assign at creation.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"service_account": {
|
||||
"description": "Service accounts are admin-managed accounts that cannot login.",
|
||||
"type": "boolean"
|
||||
|
||||
Generated
+7
@@ -13890,6 +13890,13 @@
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"roles": {
|
||||
"description": "Roles is an optional list of site-level roles to assign at creation.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"service_account": {
|
||||
"description": "Service accounts are admin-managed accounts that cannot login.",
|
||||
"type": "boolean"
|
||||
|
||||
@@ -615,6 +615,7 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
|
||||
CreateUserRequestWithOrgs: req,
|
||||
LoginType: loginType,
|
||||
accountCreatorName: accountCreator.Name,
|
||||
RBACRoles: req.Roles,
|
||||
})
|
||||
|
||||
if dbauthz.IsNotAuthorizedError(err) {
|
||||
|
||||
@@ -189,6 +189,8 @@ type CreateUserRequestWithOrgs struct {
|
||||
OrganizationIDs []uuid.UUID `json:"organization_ids" validate:"" format:"uuid"`
|
||||
// Service accounts are admin-managed accounts that cannot login.
|
||||
ServiceAccount bool `json:"service_account,omitempty"`
|
||||
// Roles is an optional list of site-level roles to assign at creation.
|
||||
Roles []string `json:"roles,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the unmarshal for the legacy param "organization_id".
|
||||
|
||||
Generated
+4
@@ -2747,6 +2747,9 @@ This is required on creation to enable a user-flow of validating a template work
|
||||
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
|
||||
],
|
||||
"password": "string",
|
||||
"roles": [
|
||||
"string"
|
||||
],
|
||||
"service_account": true,
|
||||
"user_status": "active",
|
||||
"username": "string"
|
||||
@@ -2762,6 +2765,7 @@ This is required on creation to enable a user-flow of validating a template work
|
||||
| `name` | string | false | | |
|
||||
| `organization_ids` | array of string | false | | Organization ids is a list of organization IDs that the user should be a member of. |
|
||||
| `password` | string | false | | |
|
||||
| `roles` | array of string | false | | Roles is an optional list of site-level roles to assign at creation. |
|
||||
| `service_account` | boolean | false | | Service accounts are admin-managed accounts that cannot login. |
|
||||
| `user_status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | User status defaults to UserStatusDormant. |
|
||||
| `username` | string | true | | |
|
||||
|
||||
Generated
+3
@@ -92,6 +92,9 @@ curl -X POST http://coder-server:8080/api/v2/users \
|
||||
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
|
||||
],
|
||||
"password": "string",
|
||||
"roles": [
|
||||
"string"
|
||||
],
|
||||
"service_account": true,
|
||||
"user_status": "active",
|
||||
"username": "string"
|
||||
|
||||
Generated
+4
@@ -3108,6 +3108,10 @@ export interface CreateUserRequestWithOrgs {
|
||||
* Service accounts are admin-managed accounts that cannot login.
|
||||
*/
|
||||
readonly service_account?: boolean;
|
||||
/**
|
||||
* Roles is an optional list of site-level roles to assign at creation.
|
||||
*/
|
||||
readonly roles?: readonly string[];
|
||||
}
|
||||
|
||||
// From codersdk/usersecrets.go
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { Margins } from "#/components/Margins/Margins";
|
||||
import { Margins, type Size } from "#/components/Margins/Margins";
|
||||
import {
|
||||
PageHeader,
|
||||
PageHeaderSubtitle,
|
||||
@@ -9,15 +9,17 @@ export interface FullPageFormProps {
|
||||
title: string;
|
||||
detail?: ReactNode;
|
||||
children?: ReactNode;
|
||||
size?: Size;
|
||||
}
|
||||
|
||||
export const FullPageForm: FC<FullPageFormProps> = ({
|
||||
title,
|
||||
detail,
|
||||
children,
|
||||
size = "small",
|
||||
}) => {
|
||||
return (
|
||||
<Margins size="small">
|
||||
<Margins size={size}>
|
||||
<PageHeader className="pb-6">
|
||||
<PageHeaderTitle>{title}</PageHeaderTitle>
|
||||
{detail && <PageHeaderSubtitle>{detail}</PageHeaderSubtitle>}
|
||||
|
||||
@@ -6,11 +6,12 @@ import {
|
||||
} from "#/theme/constants";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
type Size = "regular" | "medium" | "small";
|
||||
export type Size = "regular" | "medium" | "condensed" | "small";
|
||||
|
||||
const widthBySize: Record<Size, number> = {
|
||||
regular: containerWidth,
|
||||
medium: containerWidthMedium,
|
||||
condensed: containerWidth / 2,
|
||||
small: containerWidth / 3,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,8 +2,14 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { action } from "storybook/actions";
|
||||
import { userEvent, within } from "storybook/test";
|
||||
import {
|
||||
assignableRole,
|
||||
MockAuditorRole,
|
||||
MockAuthMethodsPasswordOnly,
|
||||
MockOrganization,
|
||||
MockOrganization2,
|
||||
MockOwnerRole,
|
||||
MockTemplateAdminRole,
|
||||
MockUserAdminRole,
|
||||
mockApiError,
|
||||
} from "#/testHelpers/entities";
|
||||
import { CreateUserForm } from "./CreateUserForm";
|
||||
@@ -70,3 +76,45 @@ export const Loading: Story = {
|
||||
isLoading: true,
|
||||
},
|
||||
};
|
||||
|
||||
const mockAvailableRoles = [
|
||||
assignableRole(MockOwnerRole, true),
|
||||
assignableRole(MockUserAdminRole, true),
|
||||
assignableRole(MockTemplateAdminRole, true),
|
||||
assignableRole(MockAuditorRole, true),
|
||||
];
|
||||
|
||||
export const RolesLoading: Story = {
|
||||
args: {
|
||||
rolesLoading: true,
|
||||
authMethods: MockAuthMethodsPasswordOnly,
|
||||
},
|
||||
};
|
||||
|
||||
export const RolesError: Story = {
|
||||
args: {
|
||||
rolesError: mockApiError({
|
||||
message: "Failed to fetch assignable roles.",
|
||||
}),
|
||||
authMethods: MockAuthMethodsPasswordOnly,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithRoles: Story = {
|
||||
args: {
|
||||
availableRoles: mockAvailableRoles,
|
||||
authMethods: MockAuthMethodsPasswordOnly,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithRolesSelected: Story = {
|
||||
args: {
|
||||
availableRoles: mockAvailableRoles,
|
||||
authMethods: MockAuthMethodsPasswordOnly,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(canvas.getByRole("checkbox", { name: /owner/i }));
|
||||
await userEvent.click(canvas.getByRole("checkbox", { name: /auditor/i }));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
nameValidator,
|
||||
onChangeTrimmed,
|
||||
} from "#/utils/formUtils";
|
||||
import { RoleSelector } from "./RoleSelector";
|
||||
|
||||
const loginTypeOptions = {
|
||||
password: {
|
||||
@@ -80,6 +81,7 @@ type CreateUserFormData = {
|
||||
readonly login_type: TypesGen.LoginType;
|
||||
readonly password: string;
|
||||
readonly service_account: boolean;
|
||||
readonly roles: string[];
|
||||
};
|
||||
|
||||
interface CreateUserFormProps {
|
||||
@@ -90,6 +92,9 @@ interface CreateUserFormProps {
|
||||
authMethods?: TypesGen.AuthMethods;
|
||||
showOrganizations: boolean;
|
||||
serviceAccountsEnabled: boolean;
|
||||
availableRoles?: TypesGen.AssignableRoles[];
|
||||
rolesLoading?: boolean;
|
||||
rolesError?: unknown;
|
||||
}
|
||||
|
||||
// Stable reference for empty org options to avoid re-render loops
|
||||
@@ -104,6 +109,9 @@ export const CreateUserForm: FC<CreateUserFormProps> = ({
|
||||
showOrganizations,
|
||||
authMethods,
|
||||
serviceAccountsEnabled,
|
||||
availableRoles,
|
||||
rolesLoading,
|
||||
rolesError,
|
||||
}) => {
|
||||
const availableLoginTypes = [
|
||||
authMethods?.password.enabled && "password",
|
||||
@@ -125,6 +133,7 @@ export const CreateUserForm: FC<CreateUserFormProps> = ({
|
||||
: "00000000-0000-0000-0000-000000000000",
|
||||
login_type: defaultLoginType,
|
||||
service_account: defaultLoginType === "none",
|
||||
roles: [],
|
||||
},
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
@@ -172,44 +181,18 @@ export const CreateUserForm: FC<CreateUserFormProps> = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<FullPageForm title="Create user">
|
||||
<FullPageForm title="Create user" size="condensed">
|
||||
{isApiError(error) && !hasApiFieldErrors(error) && (
|
||||
<ErrorAlert error={error} className="mb-8" />
|
||||
)}
|
||||
<form onSubmit={form.handleSubmit} autoComplete="off">
|
||||
<form
|
||||
onSubmit={form.handleSubmit}
|
||||
autoComplete="off"
|
||||
className="border border-border border-solid rounded-md p-4"
|
||||
>
|
||||
<div className="flex flex-col gap-6">
|
||||
<FormField
|
||||
field={getFieldHelpers("username")}
|
||||
label="Username"
|
||||
id="username"
|
||||
name="username"
|
||||
value={form.values.username}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
onBlur={form.handleBlur}
|
||||
autoComplete="username"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<FormField
|
||||
field={getFieldHelpers("name")}
|
||||
label={
|
||||
<>
|
||||
Full name{" "}
|
||||
<span className="font-normal text-content-secondary">
|
||||
(optional)
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
id="name"
|
||||
name="name"
|
||||
value={form.values.name}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
autoComplete="name"
|
||||
/>
|
||||
|
||||
{showOrganizations && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-2 max-w-sm">
|
||||
<Label htmlFor="organization">Organization</Label>
|
||||
<OrganizationAutocomplete
|
||||
id="organization"
|
||||
@@ -225,7 +208,7 @@ export const CreateUserForm: FC<CreateUserFormProps> = ({
|
||||
)}
|
||||
|
||||
{/* Login type — "none" is presented as "Service account" */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-2 max-w-sm">
|
||||
<Label htmlFor="login_type">Login type</Label>
|
||||
<Select
|
||||
value={form.values.login_type}
|
||||
@@ -295,6 +278,37 @@ export const CreateUserForm: FC<CreateUserFormProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
field={getFieldHelpers("username")}
|
||||
label="Username"
|
||||
id="username"
|
||||
name="username"
|
||||
value={form.values.username}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
onBlur={form.handleBlur}
|
||||
autoComplete="username"
|
||||
autoFocus
|
||||
className="max-w-sm"
|
||||
/>
|
||||
|
||||
<FormField
|
||||
field={getFieldHelpers("name")}
|
||||
label={
|
||||
<>
|
||||
Full name{" "}
|
||||
<span className="font-normal text-content-secondary">
|
||||
(optional)
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
id="name"
|
||||
name="name"
|
||||
value={form.values.name}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
autoComplete="name"
|
||||
/>
|
||||
|
||||
{!isServiceAccount && (
|
||||
<FormField
|
||||
field={getFieldHelpers("email")}
|
||||
@@ -330,6 +344,31 @@ export const CreateUserForm: FC<CreateUserFormProps> = ({
|
||||
data-testid="password-input"
|
||||
/>
|
||||
)}
|
||||
|
||||
{rolesLoading ? (
|
||||
<RoleSelector
|
||||
loading
|
||||
roles={[]}
|
||||
selectedRoles={[]}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
) : rolesError ? (
|
||||
<RoleSelector
|
||||
error={rolesError}
|
||||
roles={[]}
|
||||
selectedRoles={[]}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
) : (
|
||||
availableRoles &&
|
||||
availableRoles.length > 0 && (
|
||||
<RoleSelector
|
||||
roles={availableRoles}
|
||||
selectedRoles={form.values.roles}
|
||||
onChange={(roles) => form.setFieldValue("roles", roles)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FormFooter className="mt-8">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { getErrorDetail, getErrorMessage } from "#/api/errors";
|
||||
import { roles } from "#/api/queries/roles";
|
||||
import { authMethods, createUser } from "#/api/queries/users";
|
||||
import { Margins } from "#/components/Margins/Margins";
|
||||
import { useDashboard } from "#/modules/dashboard/useDashboard";
|
||||
@@ -15,6 +16,7 @@ const CreateUserPage: FC = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const createUserMutation = useMutation(createUser(queryClient));
|
||||
const authMethodsQuery = useQuery(authMethods());
|
||||
const rolesQuery = useQuery(roles());
|
||||
const { showOrganizations } = useDashboard();
|
||||
const { service_accounts: serviceAccountsEnabled } = useFeatureVisibility();
|
||||
|
||||
@@ -36,6 +38,7 @@ const CreateUserPage: FC = () => {
|
||||
password: user.password,
|
||||
user_status: null,
|
||||
service_account: user.service_account,
|
||||
roles: user.roles,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
@@ -61,6 +64,9 @@ const CreateUserPage: FC = () => {
|
||||
authMethods={authMethodsQuery.data}
|
||||
showOrganizations={showOrganizations}
|
||||
serviceAccountsEnabled={serviceAccountsEnabled}
|
||||
availableRoles={rolesQuery.data}
|
||||
rolesLoading={rolesQuery.isLoading}
|
||||
rolesError={rolesQuery.error}
|
||||
/>
|
||||
</Margins>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { UserIcon } from "lucide-react";
|
||||
import { type FC, useId } from "react";
|
||||
import { getErrorMessage } from "#/api/errors";
|
||||
import type { AssignableRoles } from "#/api/typesGenerated";
|
||||
import { Alert, AlertTitle } from "#/components/Alert/Alert";
|
||||
import { Checkbox } from "#/components/Checkbox/Checkbox";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
|
||||
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.",
|
||||
"agents-access": "Grants access to Coder Agents chat.",
|
||||
member:
|
||||
"Everybody is a member. This is a shared and default role for all users.",
|
||||
};
|
||||
|
||||
interface RoleSelectorProps {
|
||||
roles: AssignableRoles[];
|
||||
selectedRoles: string[];
|
||||
onChange: (roles: string[]) => void;
|
||||
loading?: boolean;
|
||||
error?: unknown;
|
||||
}
|
||||
|
||||
export const RoleSelector: FC<RoleSelectorProps> = ({
|
||||
roles,
|
||||
selectedRoles,
|
||||
onChange,
|
||||
loading,
|
||||
error,
|
||||
}) => {
|
||||
const baseId = useId();
|
||||
const selectableRoles = roles.filter(
|
||||
(r) => r.assignable && r.name !== "member",
|
||||
);
|
||||
|
||||
const handleToggle = (roleName: string) => {
|
||||
if (selectedRoles.includes(roleName)) {
|
||||
onChange(selectedRoles.filter((r) => r !== roleName));
|
||||
} else {
|
||||
onChange([...selectedRoles, roleName]);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm font-medium">Roles</span>
|
||||
<div className="border border-border border-solid rounded-md">
|
||||
<div className="p-3 flex flex-col gap-2">
|
||||
{Array.from({ length: 4 }, (_, i) => (
|
||||
<div key={i} className="flex items-start gap-2">
|
||||
<Skeleton className="mt-1 shrink-0 size-4 rounded" />
|
||||
<div className="flex flex-col gap-1 flex-1">
|
||||
<Skeleton variant="text" className="w-24" />
|
||||
<Skeleton variant="text" className="w-48" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm font-medium">Roles</span>
|
||||
<Alert severity="error">
|
||||
<AlertTitle>
|
||||
{getErrorMessage(error, "Failed to load roles.")}
|
||||
</AlertTitle>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm font-medium">Roles</span>
|
||||
{selectableRoles.length > 0 && (
|
||||
<div className="border border-border border-solid rounded-md">
|
||||
<div className="overflow-y-auto max-h-72 p-3 flex flex-col gap-2">
|
||||
{selectableRoles.map((role) => {
|
||||
const checkboxId = `${baseId}-${role.name}`;
|
||||
return (
|
||||
<label
|
||||
key={role.name}
|
||||
htmlFor={checkboxId}
|
||||
className="flex items-start gap-2 cursor-pointer"
|
||||
>
|
||||
<Checkbox
|
||||
id={checkboxId}
|
||||
checked={selectedRoles.includes(role.name)}
|
||||
onCheckedChange={() => handleToggle(role.name)}
|
||||
className="mt-1 shrink-0"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">
|
||||
{role.display_name || role.name}
|
||||
</span>
|
||||
<span className="text-sm text-content-secondary">
|
||||
{roleDescriptions[role.name] ?? ""}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -180,6 +180,12 @@ const roleNamesByAccessLevel: readonly string[] = [
|
||||
"agents-access",
|
||||
];
|
||||
|
||||
// Roles not in the priority list should sort after all known roles.
|
||||
const roleSortComparator = (name: string) =>
|
||||
roleNamesByAccessLevel.includes(name)
|
||||
? roleNamesByAccessLevel.indexOf(name)
|
||||
: Number.POSITIVE_INFINITY;
|
||||
|
||||
function sortRolesByAccessLevel<T extends SlimRole>(
|
||||
roles: readonly T[],
|
||||
): readonly T[] {
|
||||
@@ -188,9 +194,7 @@ function sortRolesByAccessLevel<T extends SlimRole>(
|
||||
}
|
||||
|
||||
return [...roles].sort(
|
||||
(r1, r2) =>
|
||||
roleNamesByAccessLevel.indexOf(r1.name) -
|
||||
roleNamesByAccessLevel.indexOf(r2.name),
|
||||
(r1, r2) => roleSortComparator(r1.name) - roleSortComparator(r2.name),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -288,7 +288,7 @@ export const MockOwnerRole: TypesGen.Role = {
|
||||
};
|
||||
|
||||
export const MockUserAdminRole: TypesGen.Role = {
|
||||
name: "user_admin",
|
||||
name: "user-admin",
|
||||
display_name: "User Admin",
|
||||
site_permissions: [],
|
||||
user_permissions: [],
|
||||
@@ -298,7 +298,7 @@ export const MockUserAdminRole: TypesGen.Role = {
|
||||
};
|
||||
|
||||
export const MockTemplateAdminRole: TypesGen.Role = {
|
||||
name: "template_admin",
|
||||
name: "template-admin",
|
||||
display_name: "Template Admin",
|
||||
site_permissions: [],
|
||||
user_permissions: [],
|
||||
|
||||
Reference in New Issue
Block a user