mirror of
https://github.com/logto-io/logto.git
synced 2025-03-03 22:15:32 -05:00
refactor(console,phrases): hide role type selection on creation modal by default (#4581)
This commit is contained in:
parent
827123faa0
commit
19939811c1
19 changed files with 177 additions and 23 deletions
|
@ -31,7 +31,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
.icon,
|
||||
.trailingIcon {
|
||||
margin-right: _.unit(2);
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
|
@ -39,6 +40,11 @@
|
|||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.trailingIcon {
|
||||
margin-right: unset;
|
||||
margin-left: _.unit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,7 +80,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
.icon,
|
||||
.trailingIcon {
|
||||
margin-right: _.unit(2);
|
||||
vertical-align: middle;
|
||||
color: var(--color-text-secondary);
|
||||
|
@ -84,6 +91,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.trailingIcon {
|
||||
margin-right: unset;
|
||||
margin-left: _.unit(2);
|
||||
}
|
||||
|
||||
.disabledLabel {
|
||||
background: var(--color-neutral-90);
|
||||
padding: _.unit(0.5) _.unit(2);
|
||||
|
@ -123,6 +135,10 @@
|
|||
.icon {
|
||||
margin-right: _.unit(4);
|
||||
}
|
||||
|
||||
.trailingIcon {
|
||||
margin-left: _.unit(4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -188,7 +204,8 @@
|
|||
background-color: var(--color-hover-variant);
|
||||
|
||||
.content {
|
||||
.icon {
|
||||
.icon,
|
||||
.trailingIcon {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
@ -248,7 +265,8 @@
|
|||
background-color: var(--color-layer-2);
|
||||
|
||||
.content {
|
||||
.icon {
|
||||
.icon,
|
||||
.trailingIcon {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
@ -272,7 +290,8 @@
|
|||
border-color: var(--color-primary);
|
||||
|
||||
.content {
|
||||
.icon {
|
||||
.icon,
|
||||
.trailingIcon {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ export type Props = {
|
|||
isDisabled?: boolean;
|
||||
disabledLabel?: AdminConsoleKey;
|
||||
icon?: ReactNode;
|
||||
trailingIcon?: ReactNode;
|
||||
hasCheckIconForCard?: boolean;
|
||||
};
|
||||
|
||||
|
@ -48,6 +49,7 @@ function Radio({
|
|||
isDisabled,
|
||||
disabledLabel,
|
||||
icon,
|
||||
trailingIcon,
|
||||
hasCheckIconForCard = true,
|
||||
}: Props) {
|
||||
const handleKeyPress: KeyboardEventHandler<HTMLDivElement> = useCallback(
|
||||
|
@ -90,6 +92,7 @@ function Radio({
|
|||
{type === 'plain' && <div className={styles.indicator} />}
|
||||
{icon && <span className={styles.icon}>{icon}</span>}
|
||||
{title && (typeof title === 'string' ? <DynamicT forKey={title} /> : title)}
|
||||
{trailingIcon && <span className={styles.trailingIcon}>{trailingIcon}</span>}
|
||||
{isDisabled && disabledLabel && (
|
||||
<div className={classNames(styles.indicator, styles.disabledLabel)}>
|
||||
<DynamicT forKey={disabledLabel} />
|
||||
|
|
|
@ -4,3 +4,23 @@
|
|||
display: flex;
|
||||
gap: _.unit(6);
|
||||
}
|
||||
|
||||
.roleTypeSelectionSwitch {
|
||||
margin-top: _.unit(2);
|
||||
}
|
||||
|
||||
.trailingIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
> svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.proTag {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
@ -2,15 +2,18 @@ import { type AdminConsoleKey } from '@logto/phrases';
|
|||
import type { Role, ScopeResponse } from '@logto/schemas';
|
||||
import { RoleType, internalRolePrefix } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { useContext } from 'react';
|
||||
import { useContext, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import KeyboardArrowDown from '@/assets/icons/keyboard-arrow-down.svg';
|
||||
import KeyboardArrowUp from '@/assets/icons/keyboard-arrow-up.svg';
|
||||
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import ProTag from '@/components/ProTag';
|
||||
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
|
||||
import RoleScopesTransfer from '@/components/RoleScopesTransfer';
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
|
@ -20,14 +23,15 @@ 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 { ReservedPlanName } from '@/types/subscriptions';
|
||||
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 },
|
||||
const radioOptions: Array<{ key: AdminConsoleKey; value: RoleType; proTagCheck: boolean }> = [
|
||||
{ key: 'roles.type_user', value: RoleType.User, proTagCheck: false },
|
||||
{ key: 'roles.type_machine_to_machine', value: RoleType.MachineToMachine, proTagCheck: true },
|
||||
];
|
||||
|
||||
export type Props = {
|
||||
|
@ -45,8 +49,10 @@ type CreateRolePayload = Pick<Role, 'name' | 'description' | 'type'> & {
|
|||
|
||||
function CreateRoleForm({ totalRoleCount, onClose }: Props) {
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
const [isTypeSelectorVisible, setIsTypeSelectorVisible] = useState(false);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { data: currentPlan } = useSubscriptionPlan(currentTenantId);
|
||||
const isM2mDisabledForCurrentPlan = isCloud && currentPlan?.quota.machineToMachineLimit === 0;
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
|
@ -100,9 +106,25 @@ function CreateRoleForm({ totalRoleCount, onClose }: Props) {
|
|||
subtitle="roles.create_role_description"
|
||||
learnMoreLink="https://docs.logto.io/docs/recipes/rbac/manage-permissions-and-roles#manage-roles"
|
||||
size="large"
|
||||
footer={
|
||||
<>
|
||||
{isRolesReachLimit && currentPlan && (
|
||||
footer={(() => {
|
||||
if (
|
||||
currentPlan?.name === ReservedPlanName.Free &&
|
||||
watch('type') === RoleType.MachineToMachine
|
||||
) {
|
||||
return (
|
||||
<QuotaGuardFooter>
|
||||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.paywall.machine_to_machine_feature')}
|
||||
</Trans>
|
||||
</QuotaGuardFooter>
|
||||
);
|
||||
}
|
||||
if (isRolesReachLimit && currentPlan) {
|
||||
return (
|
||||
<QuotaGuardFooter>
|
||||
<Trans
|
||||
components={{
|
||||
|
@ -113,8 +135,10 @@ function CreateRoleForm({ totalRoleCount, onClose }: Props) {
|
|||
{t('upsell.paywall.roles', { count: currentPlan.quota.rolesLimit ?? 0 })}
|
||||
</Trans>
|
||||
</QuotaGuardFooter>
|
||||
)}
|
||||
{isScopesPerReachLimit && currentPlan && !isRolesReachLimit && (
|
||||
);
|
||||
}
|
||||
if (isScopesPerReachLimit && currentPlan && !isRolesReachLimit) {
|
||||
return (
|
||||
<QuotaGuardFooter>
|
||||
<Trans
|
||||
components={{
|
||||
|
@ -127,8 +151,10 @@ function CreateRoleForm({ totalRoleCount, onClose }: Props) {
|
|||
})}
|
||||
</Trans>
|
||||
</QuotaGuardFooter>
|
||||
)}
|
||||
{!isRolesReachLimit && !isScopesPerReachLimit && (
|
||||
);
|
||||
}
|
||||
if (!isRolesReachLimit && !isScopesPerReachLimit) {
|
||||
return (
|
||||
<Button
|
||||
isLoading={isSubmitting}
|
||||
htmlType="submit"
|
||||
|
@ -137,9 +163,9 @@ function CreateRoleForm({ totalRoleCount, onClose }: Props) {
|
|||
type="primary"
|
||||
onClick={onSubmit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
})()}
|
||||
onClose={onClose}
|
||||
>
|
||||
<form>
|
||||
|
@ -157,8 +183,26 @@ function CreateRoleForm({ totalRoleCount, onClose }: Props) {
|
|||
placeholder={t('roles.role_name_placeholder')}
|
||||
error={errors.name?.message}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
title={
|
||||
isTypeSelectorVisible
|
||||
? 'roles.hide_role_type_button_text'
|
||||
: 'roles.show_role_type_button_text'
|
||||
}
|
||||
trailingIcon={
|
||||
<div className={styles.trailingIcon}>
|
||||
{isTypeSelectorVisible ? <KeyboardArrowUp /> : <KeyboardArrowDown />}
|
||||
</div>
|
||||
}
|
||||
className={styles.roleTypeSelectionSwitch}
|
||||
onClick={() => {
|
||||
setIsTypeSelectorVisible(!isTypeSelectorVisible);
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
{isDevFeaturesEnabled && (
|
||||
{isDevFeaturesEnabled && isTypeSelectorVisible && (
|
||||
<FormField title="roles.role_type">
|
||||
<Controller
|
||||
name="type"
|
||||
|
@ -172,8 +216,16 @@ function CreateRoleForm({ totalRoleCount, onClose }: Props) {
|
|||
onChange(value);
|
||||
}}
|
||||
>
|
||||
{radioOptions.map(({ key, value }) => (
|
||||
<Radio key={value} title={<DynamicT forKey={key} />} value={value} />
|
||||
{radioOptions.map(({ key, value, proTagCheck }) => (
|
||||
<Radio
|
||||
key={value}
|
||||
title={<DynamicT forKey={key} />}
|
||||
value={value}
|
||||
trailingIcon={
|
||||
proTagCheck &&
|
||||
isM2mDisabledForCurrentPlan && <ProTag className={styles.proTag} />
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)}
|
||||
|
|
|
@ -6,6 +6,10 @@ const roles = {
|
|||
create: 'Rolle erstellen',
|
||||
role_name: 'Rollenname',
|
||||
role_type: 'Rollenart',
|
||||
/** UNTRANSLATED */
|
||||
show_role_type_button_text: 'Show more options',
|
||||
/** UNTRANSLATED */
|
||||
hide_role_type_button_text: 'Hide more options',
|
||||
type_user: 'Benutzerrolle',
|
||||
type_machine_to_machine: 'Maschinen-zu-Maschinen-App-Rolle',
|
||||
role_description: 'Beschreibung',
|
||||
|
|
|
@ -6,6 +6,10 @@ const roles = {
|
|||
create: 'Create Role',
|
||||
role_name: 'Role name',
|
||||
role_type: 'Role type',
|
||||
/** UNTRANSLATED */
|
||||
show_role_type_button_text: 'Show more options',
|
||||
/** UNTRANSLATED */
|
||||
hide_role_type_button_text: 'Hide more options',
|
||||
type_user: 'User role',
|
||||
type_machine_to_machine: 'Machine-to-machine app role',
|
||||
role_description: 'Description',
|
||||
|
|
|
@ -6,6 +6,10 @@ const roles = {
|
|||
create: 'Crear Rol',
|
||||
role_name: 'Nombre de rol',
|
||||
role_type: 'Tipo de rol',
|
||||
/** UNTRANSLATED */
|
||||
show_role_type_button_text: 'Show more options',
|
||||
/** UNTRANSLATED */
|
||||
hide_role_type_button_text: 'Hide more options',
|
||||
type_user: 'Rol de usuario',
|
||||
type_machine_to_machine: 'Rol de aplicación de máquina a máquina',
|
||||
role_description: 'Descripción',
|
||||
|
|
|
@ -6,6 +6,10 @@ const roles = {
|
|||
create: 'Créer un rôle',
|
||||
role_name: 'Nom du rôle',
|
||||
role_type: 'Type de rôle',
|
||||
/** UNTRANSLATED */
|
||||
show_role_type_button_text: 'Show more options',
|
||||
/** UNTRANSLATED */
|
||||
hide_role_type_button_text: 'Hide more options',
|
||||
type_user: 'Rôle utilisateur',
|
||||
type_machine_to_machine: "Rôle d'application machine-à-machine",
|
||||
role_description: 'Description',
|
||||
|
|
|
@ -6,6 +6,10 @@ const roles = {
|
|||
create: 'Crea Ruolo',
|
||||
role_name: 'Nome ruolo',
|
||||
role_type: 'Tipo ruolo',
|
||||
/** UNTRANSLATED */
|
||||
show_role_type_button_text: 'Show more options',
|
||||
/** UNTRANSLATED */
|
||||
hide_role_type_button_text: 'Hide more options',
|
||||
type_user: 'Ruolo utente',
|
||||
type_machine_to_machine: 'Ruolo app M2M',
|
||||
role_description: 'Descrizione',
|
||||
|
|
|
@ -6,6 +6,10 @@ const roles = {
|
|||
create: 'ロールを作成する',
|
||||
role_name: '役割名',
|
||||
role_type: '役割タイプ',
|
||||
/** UNTRANSLATED */
|
||||
show_role_type_button_text: 'Show more options',
|
||||
/** UNTRANSLATED */
|
||||
hide_role_type_button_text: 'Hide more options',
|
||||
type_user: 'ユーザーの役割',
|
||||
type_machine_to_machine: 'マシン対マシンアプリの役割',
|
||||
role_description: '説明',
|
||||
|
|
|
@ -6,6 +6,10 @@ const roles = {
|
|||
create: '역할 생성',
|
||||
role_name: '역할 이름',
|
||||
role_type: '역할 유형',
|
||||
/** UNTRANSLATED */
|
||||
show_role_type_button_text: 'Show more options',
|
||||
/** UNTRANSLATED */
|
||||
hide_role_type_button_text: 'Hide more options',
|
||||
type_user: '사용자 역할',
|
||||
type_machine_to_machine: '기계 간 앱 역할',
|
||||
role_description: '설명',
|
||||
|
|
|
@ -6,6 +6,10 @@ const roles = {
|
|||
create: 'Utwórz rolę',
|
||||
role_name: 'Nazwa roli',
|
||||
role_type: 'Typ roli',
|
||||
/** UNTRANSLATED */
|
||||
show_role_type_button_text: 'Show more options',
|
||||
/** UNTRANSLATED */
|
||||
hide_role_type_button_text: 'Hide more options',
|
||||
type_user: 'Rola użytkownika',
|
||||
type_machine_to_machine: 'Rola aplikacji Machine-to-Machine',
|
||||
role_description: 'Opis',
|
||||
|
|
|
@ -6,6 +6,10 @@ const roles = {
|
|||
create: 'Criar função',
|
||||
role_name: 'Nome da função',
|
||||
role_type: 'Tipo de função',
|
||||
/** UNTRANSLATED */
|
||||
show_role_type_button_text: 'Show more options',
|
||||
/** UNTRANSLATED */
|
||||
hide_role_type_button_text: 'Hide more options',
|
||||
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',
|
||||
|
|
|
@ -6,6 +6,10 @@ const roles = {
|
|||
create: 'Criar papel',
|
||||
role_name: 'Nome do papel',
|
||||
role_type: 'Tipo de papel',
|
||||
/** UNTRANSLATED */
|
||||
show_role_type_button_text: 'Show more options',
|
||||
/** UNTRANSLATED */
|
||||
hide_role_type_button_text: 'Hide more options',
|
||||
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',
|
||||
|
|
|
@ -6,6 +6,10 @@ const roles = {
|
|||
create: 'Создать роль',
|
||||
role_name: 'Имя роли',
|
||||
role_type: 'Тип роли',
|
||||
/** UNTRANSLATED */
|
||||
show_role_type_button_text: 'Show more options',
|
||||
/** UNTRANSLATED */
|
||||
hide_role_type_button_text: 'Hide more options',
|
||||
type_user: 'Роль пользователя',
|
||||
type_machine_to_machine: 'Роль приложения между машинами',
|
||||
role_description: 'Описание',
|
||||
|
|
|
@ -6,6 +6,10 @@ const roles = {
|
|||
create: 'Rol Oluştur',
|
||||
role_name: 'Rol adı',
|
||||
role_type: 'Rol tipi',
|
||||
/** UNTRANSLATED */
|
||||
show_role_type_button_text: 'Show more options',
|
||||
/** UNTRANSLATED */
|
||||
hide_role_type_button_text: 'Hide more options',
|
||||
type_user: 'Kullanıcı rolü',
|
||||
type_machine_to_machine: 'Makine-makine uygulama rolü',
|
||||
role_description: 'Açıklama',
|
||||
|
|
|
@ -6,6 +6,10 @@ const roles = {
|
|||
create: '创建角色',
|
||||
role_name: '角色名称',
|
||||
role_type: '角色类型',
|
||||
/** UNTRANSLATED */
|
||||
show_role_type_button_text: 'Show more options',
|
||||
/** UNTRANSLATED */
|
||||
hide_role_type_button_text: 'Hide more options',
|
||||
type_user: '用户角色',
|
||||
type_machine_to_machine: '机器对机器应用程序角色',
|
||||
role_description: '描述',
|
||||
|
|
|
@ -6,6 +6,10 @@ const roles = {
|
|||
create: '創建角色',
|
||||
role_name: '角色名稱',
|
||||
role_type: '角色類型',
|
||||
/** UNTRANSLATED */
|
||||
show_role_type_button_text: 'Show more options',
|
||||
/** UNTRANSLATED */
|
||||
hide_role_type_button_text: 'Hide more options',
|
||||
type_user: '用戶角色',
|
||||
type_machine_to_machine: '機器到機器應用程式角色',
|
||||
role_description: '描述',
|
||||
|
|
|
@ -6,6 +6,10 @@ const roles = {
|
|||
create: '建立角色',
|
||||
role_name: '角色名稱',
|
||||
role_type: '角色類型',
|
||||
/** UNTRANSLATED */
|
||||
show_role_type_button_text: 'Show more options',
|
||||
/** UNTRANSLATED */
|
||||
hide_role_type_button_text: 'Hide more options',
|
||||
type_user: '使用者角色',
|
||||
type_machine_to_machine: '機器對機器應用角色',
|
||||
role_description: '描述',
|
||||
|
|
Loading…
Add table
Reference in a new issue