0
Fork 0
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:
Darcy Ye 2023-09-26 11:02:05 +08:00 committed by GitHub
parent 827123faa0
commit 19939811c1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 177 additions and 23 deletions

View file

@ -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);
}
}

View file

@ -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} />

View file

@ -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;
}

View file

@ -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>
)}

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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: '説明',

View file

@ -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: '설명',

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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: 'Описание',

View file

@ -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',

View file

@ -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: '描述',

View file

@ -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: '描述',

View file

@ -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: '描述',