diff --git a/packages/console/src/components/RoleUsersTransfer/components/SourceUsersBox/index.module.scss b/packages/console/src/components/RoleEntitiesTransfer/components/SourceEntitiesBox/index.module.scss similarity index 100% rename from packages/console/src/components/RoleUsersTransfer/components/SourceUsersBox/index.module.scss rename to packages/console/src/components/RoleEntitiesTransfer/components/SourceEntitiesBox/index.module.scss diff --git a/packages/console/src/components/RoleUsersTransfer/components/SourceUsersBox/index.tsx b/packages/console/src/components/RoleEntitiesTransfer/components/SourceEntitiesBox/index.tsx similarity index 61% rename from packages/console/src/components/RoleUsersTransfer/components/SourceUsersBox/index.tsx rename to packages/console/src/components/RoleEntitiesTransfer/components/SourceEntitiesBox/index.tsx index c8a7ec733..b68d22a2c 100644 --- a/packages/console/src/components/RoleUsersTransfer/components/SourceUsersBox/index.tsx +++ b/packages/console/src/components/RoleEntitiesTransfer/components/SourceEntitiesBox/index.tsx @@ -1,4 +1,5 @@ -import type { User } from '@logto/schemas'; +import type { Application, User } from '@logto/schemas'; +import { ApplicationType, RoleType } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import classNames from 'classnames'; import type { ChangeEvent } from 'react'; @@ -16,32 +17,48 @@ import useDebounce from '@/hooks/use-debounce'; import * as transferLayout from '@/scss/transfer.module.scss'; import { buildUrl, formatSearchKeyword } from '@/utils/url'; -import SourceUserItem from '../SourceUserItem'; +import SourceEntityItem from '../SourceEntityItem'; import * as styles from './index.module.scss'; -type Props = { +type Props = { roleId: string; - onChange: (value: User[]) => void; - selectedUsers: User[]; + roleType: RoleType; + onChange: (value: T[]) => void; + selectedEntities: T[]; }; const pageSize = defaultPageSize; -function SourceUsersBox({ roleId, selectedUsers, onChange }: Props) { +function SourceEntitiesBox({ + roleId, + roleType, + selectedEntities, + onChange, +}: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const [page, setPage] = useState(1); const [keyword, setKeyword] = useState(''); const debounce = useDebounce(); - const url = buildUrl('api/users', { - excludeRoleId: roleId, + const commonSearchParams = { page: String(page), page_size: String(pageSize), ...conditional(keyword && { search: formatSearchKeyword(keyword) }), - }); + }; - const { data, error } = useSWR<[User[], number], RequestError>(url); + const { data, error } = useSWR<[Props['selectedEntities'], number], RequestError>( + roleType === RoleType.User + ? buildUrl('api/users', { + excludeRoleId: roleId, + ...commonSearchParams, + }) + : buildUrl(`api/applications`, { + ...commonSearchParams, + 'search.type': ApplicationType.MachineToMachine, + 'mode.type': 'exact', + }) + ); const isLoading = !data && !error; @@ -54,7 +71,8 @@ function SourceUsersBox({ roleId, selectedUsers, onChange }: Props) { }); }; - const isUserAdded = (user: User) => selectedUsers.findIndex(({ id }) => id === user.id) >= 0; + const isEntityAdded = (entity: User | Application) => + selectedEntities.findIndex(({ id }) => id === entity.id) >= 0; const isEmpty = !isLoading && !error && dataSource.length === 0; @@ -72,21 +90,28 @@ function SourceUsersBox({ roleId, selectedUsers, onChange }: Props) { className={classNames(transferLayout.boxContent, isEmpty && transferLayout.emptyBoxContent)} > {isEmpty ? ( - + ) : ( - dataSource.map((user) => { - const isSelected = isUserAdded(user); + dataSource.map((entity) => { + const isSelected = isEntityAdded(entity); return ( - { onChange( isSelected - ? selectedUsers.filter(({ id }) => user.id !== id) - : [user, ...selectedUsers] + ? selectedEntities.filter(({ id }) => entity.id !== id) + : [entity, ...selectedEntities] ); }} /> @@ -107,4 +132,4 @@ function SourceUsersBox({ roleId, selectedUsers, onChange }: Props) { ); } -export default SourceUsersBox; +export default SourceEntitiesBox; diff --git a/packages/console/src/components/RoleUsersTransfer/components/SourceUserItem/index.module.scss b/packages/console/src/components/RoleEntitiesTransfer/components/SourceEntityItem/index.module.scss similarity index 84% rename from packages/console/src/components/RoleUsersTransfer/components/SourceUserItem/index.module.scss rename to packages/console/src/components/RoleEntitiesTransfer/components/SourceEntityItem/index.module.scss index 6c344501d..1f4ccdfe2 100644 --- a/packages/console/src/components/RoleUsersTransfer/components/SourceUserItem/index.module.scss +++ b/packages/console/src/components/RoleEntitiesTransfer/components/SourceEntityItem/index.module.scss @@ -7,6 +7,12 @@ cursor: pointer; user-select: none; + .icon { + width: 20px; + height: 20px; + border-radius: 6px; + } + .title { flex: 1 1 0; font: var(--font-body-2); diff --git a/packages/console/src/components/RoleEntitiesTransfer/components/SourceEntityItem/index.tsx b/packages/console/src/components/RoleEntitiesTransfer/components/SourceEntityItem/index.tsx new file mode 100644 index 000000000..2dcc6209f --- /dev/null +++ b/packages/console/src/components/RoleEntitiesTransfer/components/SourceEntityItem/index.tsx @@ -0,0 +1,55 @@ +import type { User, Application } from '@logto/schemas'; +import { ApplicationType } from '@logto/schemas'; + +import ApplicationIcon from '@/components/ApplicationIcon'; +import UserAvatar from '@/components/UserAvatar'; +import Checkbox from '@/ds-components/Checkbox'; +import SuspendedTag from '@/pages/Users/components/SuspendedTag'; +import { onKeyDownHandler } from '@/utils/a11y'; +import { getUserTitle } from '@/utils/user'; + +import { isUser } from '../../utils'; + +import * as styles from './index.module.scss'; + +type Props = { + entity: T; + isSelected: boolean; + onSelect: () => void; +}; + +function SourceEntityItem({ + entity, + isSelected, + onSelect, +}: Props) { + return ( +
{ + onSelect(); + })} + onClick={() => { + onSelect(); + }} + > + { + onSelect(); + }} + /> + {isUser(entity) ? ( + + ) : ( + + )} +
{isUser(entity) ? getUserTitle(entity) : entity.name}
+ {isUser(entity) && entity.isSuspended && } +
+ ); +} + +export default SourceEntityItem; diff --git a/packages/console/src/components/RoleUsersTransfer/components/TargetUsersBox/index.module.scss b/packages/console/src/components/RoleEntitiesTransfer/components/TargetEntitiesBox/index.module.scss similarity index 100% rename from packages/console/src/components/RoleUsersTransfer/components/TargetUsersBox/index.module.scss rename to packages/console/src/components/RoleEntitiesTransfer/components/TargetEntitiesBox/index.module.scss diff --git a/packages/console/src/components/RoleUsersTransfer/components/TargetUsersBox/index.tsx b/packages/console/src/components/RoleEntitiesTransfer/components/TargetEntitiesBox/index.tsx similarity index 51% rename from packages/console/src/components/RoleUsersTransfer/components/TargetUsersBox/index.tsx rename to packages/console/src/components/RoleEntitiesTransfer/components/TargetEntitiesBox/index.tsx index df9b11f15..356e5ac23 100644 --- a/packages/console/src/components/RoleUsersTransfer/components/TargetUsersBox/index.tsx +++ b/packages/console/src/components/RoleEntitiesTransfer/components/TargetEntitiesBox/index.tsx @@ -1,35 +1,35 @@ -import type { User } from '@logto/schemas'; +import type { User, Application } from '@logto/schemas'; import { useTranslation } from 'react-i18next'; import * as transferLayout from '@/scss/transfer.module.scss'; -import TargetUserItem from '../TargetUserItem'; +import TargetEntityItem from '../TargetEntityItem'; import * as styles from './index.module.scss'; -type Props = { - selectedUsers: User[]; - onChange: (value: User[]) => void; +type Props = { + selectedEntities: T[]; + onChange: (value: T[]) => void; }; -function TargetUsersBox({ selectedUsers, onChange }: Props) { +function TargetEntitiesBox({ selectedEntities, onChange }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); return (
- {`${selectedUsers.length} `} + {`${selectedEntities.length} `} {t('general.added')}
- {selectedUsers.map((user) => ( - ( + { - onChange(selectedUsers.filter(({ id }) => id !== user.id)); + onChange(selectedEntities.filter(({ id }) => id !== entity.id)); }} /> ))} @@ -38,4 +38,4 @@ function TargetUsersBox({ selectedUsers, onChange }: Props) { ); } -export default TargetUsersBox; +export default TargetEntitiesBox; diff --git a/packages/console/src/components/RoleUsersTransfer/components/TargetUserItem/index.module.scss b/packages/console/src/components/RoleEntitiesTransfer/components/TargetEntityItem/index.module.scss similarity index 90% rename from packages/console/src/components/RoleUsersTransfer/components/TargetUserItem/index.module.scss rename to packages/console/src/components/RoleEntitiesTransfer/components/TargetEntityItem/index.module.scss index 5d5c0926f..c190fd9a0 100644 --- a/packages/console/src/components/RoleUsersTransfer/components/TargetUserItem/index.module.scss +++ b/packages/console/src/components/RoleEntitiesTransfer/components/TargetEntityItem/index.module.scss @@ -6,6 +6,13 @@ padding: _.unit(2.5) _.unit(4); user-select: none; + .icon { + width: 20px; + height: 20px; + border-radius: 6px; + color: var(--color-text-secondary); + } + .meta { flex: 1; display: flex; @@ -25,10 +32,6 @@ } } - .icon { - color: var(--color-text-secondary); - } - &:hover { background: var(--color-hover); } diff --git a/packages/console/src/components/RoleEntitiesTransfer/components/TargetEntityItem/index.tsx b/packages/console/src/components/RoleEntitiesTransfer/components/TargetEntityItem/index.tsx new file mode 100644 index 000000000..899ad3280 --- /dev/null +++ b/packages/console/src/components/RoleEntitiesTransfer/components/TargetEntityItem/index.tsx @@ -0,0 +1,45 @@ +import type { User, Application } from '@logto/schemas'; +import { ApplicationType } from '@logto/schemas'; + +import Close from '@/assets/icons/close.svg'; +import ApplicationIcon from '@/components/ApplicationIcon'; +import UserAvatar from '@/components/UserAvatar'; +import IconButton from '@/ds-components/IconButton'; +import SuspendedTag from '@/pages/Users/components/SuspendedTag'; +import { getUserTitle } from '@/utils/user'; + +import { isUser } from '../../utils'; + +import * as styles from './index.module.scss'; + +type Props = { + entity: T; + onDelete: () => void; +}; + +function TargetEntityItem({ entity, onDelete }: Props) { + return ( +
+
+ {isUser(entity) ? ( + + ) : ( + + )} +
{isUser(entity) ? getUserTitle(entity) : entity.name}
+ {isUser(entity) && entity.isSuspended && } +
+ { + onDelete(); + }} + > + + +
+ ); +} + +export default TargetEntityItem; diff --git a/packages/console/src/components/RoleUsersTransfer/index.module.scss b/packages/console/src/components/RoleEntitiesTransfer/index.module.scss similarity index 70% rename from packages/console/src/components/RoleUsersTransfer/index.module.scss rename to packages/console/src/components/RoleEntitiesTransfer/index.module.scss index c06ff8c82..7fe601a01 100644 --- a/packages/console/src/components/RoleUsersTransfer/index.module.scss +++ b/packages/console/src/components/RoleEntitiesTransfer/index.module.scss @@ -1,5 +1,5 @@ @use '@/scss/underscore' as _; -.roleUsersTransfer { +.rolesTransfer { height: 360px; } diff --git a/packages/console/src/components/RoleEntitiesTransfer/index.tsx b/packages/console/src/components/RoleEntitiesTransfer/index.tsx new file mode 100644 index 000000000..1f81cfd2b --- /dev/null +++ b/packages/console/src/components/RoleEntitiesTransfer/index.tsx @@ -0,0 +1,37 @@ +import type { Application, User, RoleType } from '@logto/schemas'; +import classNames from 'classnames'; + +import * as transferLayout from '@/scss/transfer.module.scss'; + +import SourceEntitiesBox from './components/SourceEntitiesBox'; +import TargetEntitiesBox from './components/TargetEntitiesBox'; +import * as styles from './index.module.scss'; + +type Props = { + roleId: string; + roleType: RoleType; + value: T[]; + onChange: (value: T[]) => void; +}; + +function RoleEntitiesTransfer({ + roleId, + roleType, + value, + onChange, +}: Props) { + return ( +
+ +
+ +
+ ); +} + +export default RoleEntitiesTransfer; diff --git a/packages/console/src/components/RoleEntitiesTransfer/utils.ts b/packages/console/src/components/RoleEntitiesTransfer/utils.ts new file mode 100644 index 000000000..9af97e22e --- /dev/null +++ b/packages/console/src/components/RoleEntitiesTransfer/utils.ts @@ -0,0 +1,4 @@ +import type { User, Application } from '@logto/schemas'; + +export const isUser = (entity: User | Application): entity is User => + 'customData' in entity || 'identities' in entity; diff --git a/packages/console/src/components/RoleScopesTransfer/components/SourceScopesBox/index.tsx b/packages/console/src/components/RoleScopesTransfer/components/SourceScopesBox/index.tsx index 817fc725d..375fd7580 100644 --- a/packages/console/src/components/RoleScopesTransfer/components/SourceScopesBox/index.tsx +++ b/packages/console/src/components/RoleScopesTransfer/components/SourceScopesBox/index.tsx @@ -10,7 +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 { isDevFeaturesEnabled } from '@/consts/env'; import TextInput from '@/ds-components/TextInput'; import type { RequestError } from '@/hooks/use-api'; import * as transferLayout from '@/scss/transfer.module.scss'; @@ -88,7 +88,7 @@ function SourceScopesBox({ roleId, roleType, selectedScopes, onChange }: Props) .filter( ({ indicator, scopes }) => /** Should show management API scopes for machine-to-machine roles */ - ((!isProduction && roleType === RoleType.MachineToMachine) || + ((isDevFeaturesEnabled && roleType === RoleType.MachineToMachine) || !isManagementApi(indicator)) && scopes.some(({ id }) => !excludeScopeIds.has(id)) ) diff --git a/packages/console/src/components/RoleUsersTransfer/components/SourceUserItem/index.tsx b/packages/console/src/components/RoleUsersTransfer/components/SourceUserItem/index.tsx deleted file mode 100644 index 4a9e6047f..000000000 --- a/packages/console/src/components/RoleUsersTransfer/components/SourceUserItem/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import type { User } from '@logto/schemas'; - -import UserAvatar from '@/components/UserAvatar'; -import Checkbox from '@/ds-components/Checkbox'; -import SuspendedTag from '@/pages/Users/components/SuspendedTag'; -import { onKeyDownHandler } from '@/utils/a11y'; -import { getUserTitle } from '@/utils/user'; - -import * as styles from './index.module.scss'; - -type Props = { - user: User; - isSelected: boolean; - onSelect: () => void; -}; - -function SourceUserItem({ user, isSelected, onSelect }: Props) { - return ( -
{ - onSelect(); - })} - onClick={() => { - onSelect(); - }} - > - { - onSelect(); - }} - /> - -
{getUserTitle(user)}
- {user.isSuspended && } -
- ); -} - -export default SourceUserItem; diff --git a/packages/console/src/components/RoleUsersTransfer/components/TargetUserItem/index.tsx b/packages/console/src/components/RoleUsersTransfer/components/TargetUserItem/index.tsx deleted file mode 100644 index 11d96f174..000000000 --- a/packages/console/src/components/RoleUsersTransfer/components/TargetUserItem/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import type { User } from '@logto/schemas'; - -import Close from '@/assets/icons/close.svg'; -import UserAvatar from '@/components/UserAvatar'; -import IconButton from '@/ds-components/IconButton'; -import SuspendedTag from '@/pages/Users/components/SuspendedTag'; -import { getUserTitle } from '@/utils/user'; - -import * as styles from './index.module.scss'; - -type Props = { - user: User; - onDelete: () => void; -}; - -function TargetUserItem({ user, onDelete }: Props) { - return ( -
-
- -
{getUserTitle(user)}
- {user.isSuspended && } -
- { - onDelete(); - }} - > - - -
- ); -} - -export default TargetUserItem; diff --git a/packages/console/src/components/RoleUsersTransfer/index.tsx b/packages/console/src/components/RoleUsersTransfer/index.tsx deleted file mode 100644 index a8abde791..000000000 --- a/packages/console/src/components/RoleUsersTransfer/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { User } from '@logto/schemas'; -import classNames from 'classnames'; - -import * as transferLayout from '@/scss/transfer.module.scss'; - -import SourceUsersBox from './components/SourceUsersBox'; -import TargetUsersBox from './components/TargetUsersBox'; -import * as styles from './index.module.scss'; - -type Props = { - roleId: string; - value: User[]; - onChange: (value: User[]) => void; -}; - -function RoleUsersTransfer({ roleId, value, onChange }: Props) { - return ( -
- -
- -
- ); -} - -export default RoleUsersTransfer; diff --git a/packages/console/src/consts/env.ts b/packages/console/src/consts/env.ts index b2406a527..bdfe997af 100644 --- a/packages/console/src/consts/env.ts +++ b/packages/console/src/consts/env.ts @@ -1,5 +1,6 @@ import { yes } from '@silverhand/essentials'; +// eslint-disable-next-line import/no-unused-modules export const isProduction = process.env.NODE_ENV === 'production'; export const isCloud = yes(process.env.IS_CLOUD); export const adminEndpoint = process.env.ADMIN_ENDPOINT; diff --git a/packages/console/src/pages/RoleDetails/RoleUsers/components/AssignUsersModal/index.tsx b/packages/console/src/pages/RoleDetails/RoleUsers/components/AssignUsersModal/index.tsx deleted file mode 100644 index b7faf9289..000000000 --- a/packages/console/src/pages/RoleDetails/RoleUsers/components/AssignUsersModal/index.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import type { User } from '@logto/schemas'; -import { useState } from 'react'; -import { toast } from 'react-hot-toast'; -import { useTranslation } from 'react-i18next'; -import ReactModal from 'react-modal'; - -import RoleUsersTransfer from '@/components/RoleUsersTransfer'; -import Button from '@/ds-components/Button'; -import FormField from '@/ds-components/FormField'; -import ModalLayout from '@/ds-components/ModalLayout'; -import useApi from '@/hooks/use-api'; -import * as modalStyles from '@/scss/modal.module.scss'; - -type Props = { - roleId: string; - isRemindSkip?: boolean; - onClose: (success?: boolean) => void; -}; - -function AssignUsersModal({ roleId, isRemindSkip = false, onClose }: Props) { - const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const [isLoading, setIsLoading] = useState(false); - const [users, setUsers] = useState([]); - - const api = useApi(); - - const handleAssign = async () => { - if (isLoading || users.length === 0) { - return; - } - - setIsLoading(true); - - try { - await api.post(`api/roles/${roleId}/users`, { - json: { userIds: users.map(({ id }) => id) }, - }); - toast.success(t('role_details.users.users_assigned')); - onClose(true); - } finally { - setIsLoading(false); - } - }; - - return ( - { - onClose(); - }} - > - -