0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(console,phrases): add type selection for role creation modal (#4482)

* feat(console,phrases): add type selection for role creation modal

* chore(phrases): update i18n
This commit is contained in:
Darcy Ye 2023-09-14 10:52:20 +08:00 committed by GitHub
parent e8b0b1d020
commit 1863c2f817
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 125 additions and 26 deletions

View file

@ -1,5 +1,5 @@
import type { ResourceResponse, Scope, ScopeResponse } from '@logto/schemas';
import { isManagementApi, PredefinedScope } from '@logto/schemas';
import { isManagementApi, PredefinedScope, RoleType } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import classNames from 'classnames';
import type { ChangeEvent } from 'react';
@ -10,6 +10,7 @@ import useSWR from 'swr';
import Search from '@/assets/icons/search.svg';
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
import type { DetailedResourceResponse } from '@/components/RoleScopesTransfer/types';
import { isProduction } from '@/consts/env';
import TextInput from '@/ds-components/TextInput';
import type { RequestError } from '@/hooks/use-api';
import * as transferLayout from '@/scss/transfer.module.scss';
@ -20,11 +21,12 @@ import * as styles from './index.module.scss';
type Props = {
roleId?: string;
roleType: RoleType;
selectedScopes: ScopeResponse[];
onChange: (value: ScopeResponse[]) => void;
};
function SourceScopesBox({ roleId, selectedScopes, onChange }: Props) {
function SourceScopesBox({ roleId, roleType, selectedScopes, onChange }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { data: allResources, error: fetchAllResourcesError } = useSWR<
@ -85,7 +87,10 @@ function SourceScopesBox({ roleId, selectedScopes, onChange }: Props) {
return allResources
.filter(
({ indicator, scopes }) =>
!isManagementApi(indicator) && scopes.some(({ id }) => !excludeScopeIds.has(id))
/** Should show management API scopes for machine-to-machine roles */
((!isProduction && roleType === RoleType.MachineToMachine) ||
!isManagementApi(indicator)) &&
scopes.some(({ id }) => !excludeScopeIds.has(id))
)
.map(({ scopes, ...resource }) => ({
...resource,
@ -96,7 +101,7 @@ function SourceScopesBox({ roleId, selectedScopes, onChange }: Props) {
resource,
})),
}));
}, [allResources, roleId, roleScopes]);
}, [allResources, roleType, roleId, roleScopes]);
const dataSource = useMemo(() => {
const lowerCasedKeyword = keyword.toLowerCase();

View file

@ -1,4 +1,4 @@
import type { ScopeResponse } from '@logto/schemas';
import type { ScopeResponse, RoleType } from '@logto/schemas';
import classNames from 'classnames';
import * as transferLayout from '@/scss/transfer.module.scss';
@ -9,14 +9,20 @@ import * as styles from './index.module.scss';
type Props = {
roleId?: string;
roleType: RoleType;
value: ScopeResponse[];
onChange: (value: ScopeResponse[]) => void;
};
function RoleScopesTransfer({ roleId, value, onChange }: Props) {
function RoleScopesTransfer({ roleId, roleType, value, onChange }: Props) {
return (
<div className={classNames(transferLayout.container, styles.roleScopesTransfer)}>
<SourceScopesBox roleId={roleId} selectedScopes={value} onChange={onChange} />
<SourceScopesBox
roleId={roleId}
roleType={roleType}
selectedScopes={value}
onChange={onChange}
/>
<div className={transferLayout.verticalBar} />
<TargetScopesBox selectedScopes={value} onChange={onChange} />
</div>

View file

@ -1,4 +1,4 @@
import type { ScopeResponse } from '@logto/schemas';
import type { ScopeResponse, RoleType } from '@logto/schemas';
import { useContext, useState } from 'react';
import { toast } from 'react-hot-toast';
import { Trans, useTranslation } from 'react-i18next';
@ -19,11 +19,12 @@ import { hasReachedQuotaLimit } from '@/utils/quota';
type Props = {
roleId: string;
roleType: RoleType;
totalRoleScopeCount: number;
onClose: (success?: boolean) => void;
};
function AssignPermissionsModal({ roleId, totalRoleScopeCount, onClose }: Props) {
function AssignPermissionsModal({ roleId, roleType, totalRoleScopeCount, onClose }: Props) {
const { currentTenantId } = useContext(TenantsContext);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { data: currentPlan } = useSubscriptionPlan(currentTenantId);
@ -108,6 +109,7 @@ function AssignPermissionsModal({ roleId, totalRoleScopeCount, onClose }: Props)
<FormField title="role_details.permission.assign_form_field">
<RoleScopesTransfer
roleId={roleId}
roleType={roleType}
value={scopes}
onChange={(scopes) => {
setScopes(scopes);

View file

@ -22,7 +22,7 @@ const pageSize = defaultPageSize;
function RolePermissions() {
const {
role: { id: roleId },
role: { id: roleId, type: roleType },
} = useOutletContext<RoleDetailsOutletContext>();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
@ -117,6 +117,7 @@ function RolePermissions() {
{isAssignPermissionsModalOpen && totalCount !== undefined && (
<AssignPermissionsModal
roleId={roleId}
roleType={roleType}
totalRoleScopeCount={totalCount}
onClose={(success) => {
if (success) {

View file

@ -0,0 +1,6 @@
@use '@/scss/underscore' as _;
.roleTypes {
display: flex;
gap: _.unit(6);
}

View file

@ -1,5 +1,6 @@
import { type AdminConsoleKey } from '@logto/phrases';
import type { Role, ScopeResponse } from '@logto/schemas';
import { internalRolePrefix } from '@logto/schemas';
import { RoleType, internalRolePrefix } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useContext } from 'react';
import { Controller, useForm } from 'react-hook-form';
@ -9,26 +10,36 @@ import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
import PlanName from '@/components/PlanName';
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
import RoleScopesTransfer from '@/components/RoleScopesTransfer';
import { isProduction } from '@/consts/env';
import { TenantsContext } from '@/contexts/TenantsProvider';
import Button from '@/ds-components/Button';
import DynamicT from '@/ds-components/DynamicT';
import FormField from '@/ds-components/FormField';
import ModalLayout from '@/ds-components/ModalLayout';
import RadioGroup, { Radio } from '@/ds-components/RadioGroup';
import TextInput from '@/ds-components/TextInput';
import useApi from '@/hooks/use-api';
import useSubscriptionPlan from '@/hooks/use-subscription-plan';
import { trySubmitSafe } from '@/utils/form';
import { hasReachedQuotaLimit } from '@/utils/quota';
import * as styles from './index.module.scss';
const radioOptions: Array<{ key: AdminConsoleKey; value: RoleType }> = [
{ key: 'roles.type_user', value: RoleType.User },
{ key: 'roles.type_machine_to_machine', value: RoleType.MachineToMachine },
];
export type Props = {
totalRoleCount: number;
onClose: (createdRole?: Role) => void;
};
type CreateRoleFormData = Pick<Role, 'name' | 'description'> & {
type CreateRoleFormData = Pick<Role, 'name' | 'description' | 'type'> & {
scopes: ScopeResponse[];
};
type CreateRolePayload = Pick<Role, 'name' | 'description'> & {
type CreateRolePayload = Pick<Role, 'name' | 'description' | 'type'> & {
scopeIds?: string[];
};
@ -42,13 +53,13 @@ function CreateRoleForm({ totalRoleCount, onClose }: Props) {
register,
watch,
formState: { isSubmitting, errors },
} = useForm<CreateRoleFormData>();
} = useForm<CreateRoleFormData>({ defaultValues: { type: RoleType.User } });
const api = useApi();
const roleScopes = watch('scopes', []);
const onSubmit = handleSubmit(
trySubmitSafe(async ({ name, description, scopes }) => {
trySubmitSafe(async ({ name, description, type, scopes }) => {
if (isSubmitting) {
return;
}
@ -56,6 +67,7 @@ function CreateRoleForm({ totalRoleCount, onClose }: Props) {
const payload: CreateRolePayload = {
name,
description,
type,
scopeIds: conditional(scopes.length > 0 && scopes.map(({ id }) => id)),
};
@ -146,6 +158,28 @@ function CreateRoleForm({ totalRoleCount, onClose }: Props) {
error={errors.name?.message}
/>
</FormField>
{!isProduction && (
<FormField title="roles.role_type">
<Controller
name="type"
control={control}
render={({ field: { onChange, value, name } }) => (
<RadioGroup
name={name}
className={styles.roleTypes}
value={value}
onChange={(value) => {
onChange(value);
}}
>
{radioOptions.map(({ key, value }) => (
<Radio key={value} title={<DynamicT forKey={key} />} value={value} />
))}
</RadioGroup>
)}
/>
</FormField>
)}
<FormField isRequired title="roles.role_description">
<TextInput
{...register('description', { required: true })}
@ -159,7 +193,7 @@ function CreateRoleForm({ totalRoleCount, onClose }: Props) {
name="scopes"
defaultValue={[]}
render={({ field: { value, onChange } }) => (
<RoleScopesTransfer value={value} onChange={onChange} />
<RoleScopesTransfer roleType={watch('type')} value={value} onChange={onChange} />
)}
/>
</FormField>

View file

@ -4,7 +4,10 @@ const roles = {
subtitle:
'Rollen beinhalten Berechtigungen, die bestimmen, was ein Benutzer tun kann. RBAC verwendet Rollen, um Benutzern Zugriff auf Ressourcen für bestimmte Aktionen zu geben.',
create: 'Rolle erstellen',
role_name: 'Rolle',
role_name: 'Rollenname',
role_type: 'Rollenart',
type_user: 'Benutzerrolle',
type_machine_to_machine: 'Maschinen-zu-Maschinen-App-Rolle',
role_description: 'Beschreibung',
role_name_placeholder: 'Geben Sie Ihren Rollennamen ein',
role_description_placeholder: 'Geben Sie Ihre Rollenbeschreibung ein',

View file

@ -4,7 +4,10 @@ const roles = {
subtitle:
'Roles include permissions that determine what a user can do. RBAC uses roles to give users access to resources for specific actions.',
create: 'Create Role',
role_name: 'Role',
role_name: 'Role name',
role_type: 'Role type',
type_user: 'User role',
type_machine_to_machine: 'Machine-to-machine app role',
role_description: 'Description',
role_name_placeholder: 'Enter your role name',
role_description_placeholder: 'Enter your role description',

View file

@ -4,7 +4,10 @@ const roles = {
subtitle:
'Los roles incluyen permisos que determinan lo que un usuario puede hacer. RBAC utiliza roles para dar acceso a recursos a los usuarios para acciones específicas.',
create: 'Crear Rol',
role_name: 'Rol',
role_name: 'Nombre de rol',
role_type: 'Tipo de rol',
type_user: 'Rol de usuario',
type_machine_to_machine: 'Rol de aplicación de máquina a máquina',
role_description: 'Descripción',
role_name_placeholder: 'Ingrese el nombre de su rol',
role_description_placeholder: 'Ingrese la descripción de su rol',

View file

@ -4,7 +4,10 @@ const roles = {
subtitle:
"Les rôles incluent des autorisations qui déterminent ce qu'un utilisateur peut faire. RBAC utilise des rôles pour donner aux utilisateurs accès à des ressources pour des actions spécifiques.",
create: 'Créer un rôle',
role_name: 'Rôle',
role_name: 'Nom du rôle',
role_type: 'Type de rôle',
type_user: 'Rôle utilisateur',
type_machine_to_machine: "Rôle d'application machine-à-machine",
role_description: 'Description',
role_name_placeholder: 'Entrez le nom de votre rôle',
role_description_placeholder: 'Entrez la description de votre rôle',

View file

@ -4,7 +4,10 @@ const roles = {
subtitle:
"I ruoli includono le autorizzazioni che determinano ciò che un utente può fare. RBAC utilizza i ruoli per dare agli utenti l'accesso alle risorse necessarie per specifiche azioni.",
create: 'Crea Ruolo',
role_name: 'Ruolo',
role_name: 'Nome ruolo',
role_type: 'Tipo ruolo',
type_user: 'Ruolo utente',
type_machine_to_machine: 'Ruolo app M2M',
role_description: 'Descrizione',
role_name_placeholder: 'Inserisci il nome del tuo ruolo',
role_description_placeholder: 'Inserisci la descrizione del tuo ruolo',

View file

@ -4,7 +4,10 @@ const roles = {
subtitle:
'ロールには、ユーザーが実行できるアクションを決定する権限が含まれます。RBACは、特定のアクションのためにリソースにアクセスするためにユーザーに権限を付与するために、ロールを使用します。',
create: 'ロールを作成する',
role_name: 'ロールの名前',
role_name: '役割名',
role_type: '役割タイプ',
type_user: 'ユーザーの役割',
type_machine_to_machine: 'マシン対マシンアプリの役割',
role_description: '説明',
role_name_placeholder: 'ロールの名前を入力してください',
role_description_placeholder: 'ロールの説明を入力してください',

View file

@ -5,6 +5,9 @@ const roles = {
'역할은 사용자가 무엇을 할 수 있는지를 결정하는 권한을 포함해요. RBAC는 사용자에게 특정 행동에 대한 접근 권한을 부여하기 위해 역할을 사용해요.',
create: '역할 생성',
role_name: '역할 이름',
role_type: '역할 유형',
type_user: '사용자 역할',
type_machine_to_machine: '기계 간 앱 역할',
role_description: '설명',
role_name_placeholder: '역할 이름을 입력하세요',
role_description_placeholder: '역할 설명을 입력하세요',

View file

@ -4,7 +4,10 @@ const roles = {
subtitle:
'Role zawiera uprawnienia określające, co użytkownik może robić. RBAC wykorzystuje role do udostępniania użytkownikom zasobów do określonych działań.',
create: 'Utwórz rolę',
role_name: 'Rola',
role_name: 'Nazwa roli',
role_type: 'Typ roli',
type_user: 'Rola użytkownika',
type_machine_to_machine: 'Rola aplikacji Machine-to-Machine',
role_description: 'Opis',
role_name_placeholder: 'Wprowadź nazwę swojej roli',
role_description_placeholder: 'Wprowadź opis swojej roli',

View file

@ -4,7 +4,10 @@ const roles = {
subtitle:
'As funções incluem permissões que determinam o que um usuário pode fazer. O RBAC usa funções para dar aos usuários acesso a recursos para ações específicas.',
create: 'Criar função',
role_name: 'Função',
role_name: 'Nome da função',
role_type: 'Tipo de função',
type_user: 'Função do usuário',
type_machine_to_machine: 'Função do aplicativo de máquina para máquina',
role_description: 'Descrição',
role_name_placeholder: 'Insira o nome da sua função',
role_description_placeholder: 'Insira a descrição da sua função',

View file

@ -5,6 +5,9 @@ const roles = {
'Os papéis incluem permissões que determinam o que um usuário pode fazer. RBAC usa papéis para conceder acesso a recursos para ações específicas.',
create: 'Criar papel',
role_name: 'Nome do papel',
role_type: 'Tipo de papel',
type_user: 'Função de usuário',
type_machine_to_machine: 'Função de aplicação máquina-a-máquina',
role_description: 'Descrição',
role_name_placeholder: 'Digite o nome do papel',
role_description_placeholder: 'Digite a descrição do papel',

View file

@ -4,7 +4,10 @@ const roles = {
subtitle:
'Роли включают права доступа, которые определяют, что может делать пользователь. RBAC использует роли для предоставления пользователям доступа к ресурсам для конкретных действий.',
create: 'Создать роль',
role_name: 'Роль',
role_name: 'Имя роли',
role_type: 'Тип роли',
type_user: 'Роль пользователя',
type_machine_to_machine: 'Роль приложения между машинами',
role_description: 'Описание',
role_name_placeholder: 'Введите название роли',
role_description_placeholder: 'Введите описание роли',

View file

@ -4,7 +4,10 @@ const roles = {
subtitle:
'Roller, bir kullanıcının ne yapabileceğini belirleyen izinleri içerir. RBAC, kullanıcılara belirli işlemler için kaynaklara erişim vermek için roller kullanır.',
create: 'Rol Oluştur',
role_name: 'Rol Adı',
role_name: 'Rol adı',
role_type: 'Rol tipi',
type_user: 'Kullanıcı rolü',
type_machine_to_machine: 'Makine-makine uygulama rolü',
role_description: 'Açıklama',
role_name_placeholder: 'Rol adınızı girin',
role_description_placeholder: 'Rol açıklamanızı girin',

View file

@ -5,6 +5,9 @@ const roles = {
'RBAC 是一种访问控制方法,它使用角色来决定用户可以做什么事情,包括授予用户访问特定资源的权限。',
create: '创建角色',
role_name: '角色名称',
role_type: '角色类型',
type_user: '用户角色',
type_machine_to_machine: '机器对机器应用程序角色',
role_description: '描述',
role_name_placeholder: '输入你的角色名称',
role_description_placeholder: '输入你的角色描述',

View file

@ -5,6 +5,9 @@ const roles = {
'RBAC 是一種訪問控制方法,它使用角色來決定用戶可以做什麼事情,包括授予用戶訪問特定資源的權限。',
create: '創建角色',
role_name: '角色名稱',
role_type: '角色類型',
type_user: '用戶角色',
type_machine_to_machine: '機器到機器應用程式角色',
role_description: '描述',
role_name_placeholder: '輸入你的角色名稱',
role_description_placeholder: '輸入你的角色描述',

View file

@ -5,6 +5,9 @@ const roles = {
'RBAC 是一種訪問控制方法,它使用角色來決定用戶可以做什麼事情,包括授予用戶訪問特定資源的權限。',
create: '建立角色',
role_name: '角色名稱',
role_type: '角色類型',
type_user: '使用者角色',
type_machine_to_machine: '機器對機器應用角色',
role_description: '描述',
role_name_placeholder: '輸入你的角色名稱',
role_description_placeholder: '輸入你的角色描述',