-
-
Notifications
You must be signed in to change notification settings - Fork 466
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6088 from logto-io/gao-console-m2m-organizations
feat(console): show organization list for m2m apps
- Loading branch information
Showing
24 changed files
with
164 additions
and
122 deletions.
There are no files selected for viewing
13 changes: 13 additions & 0 deletions
13
packages/console/src/components/OrganizationList/index.module.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
@use '@/scss/underscore' as _; | ||
|
||
.roles { | ||
display: flex; | ||
flex-wrap: wrap; | ||
gap: _.unit(2); | ||
} | ||
|
||
.rolesHeader { | ||
display: flex; | ||
align-items: center; | ||
gap: _.unit(0.5); | ||
} |
109 changes: 109 additions & 0 deletions
109
packages/console/src/components/OrganizationList/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { type OrganizationWithRoles } from '@logto/schemas'; | ||
import { useState } from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import useSWR from 'swr'; | ||
|
||
import OrganizationIcon from '@/assets/icons/organization-preview.svg'; | ||
import Tip from '@/assets/icons/tip.svg'; | ||
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder'; | ||
import ItemPreview from '@/components/ItemPreview'; | ||
import { RoleOption } from '@/components/OrganizationRolesSelect'; | ||
import ThemedIcon from '@/components/ThemedIcon'; | ||
import CopyToClipboard from '@/ds-components/CopyToClipboard'; | ||
import IconButton from '@/ds-components/IconButton'; | ||
import Search from '@/ds-components/Search'; | ||
import Table from '@/ds-components/Table'; | ||
import Tag from '@/ds-components/Tag'; | ||
import { ToggleTip } from '@/ds-components/Tip'; | ||
import { type RequestError } from '@/hooks/use-api'; | ||
import useTenantPathname from '@/hooks/use-tenant-pathname'; | ||
|
||
import * as styles from './index.module.scss'; | ||
|
||
type Props = { | ||
readonly type: 'user' | 'application'; | ||
readonly data: { id: string }; | ||
}; | ||
|
||
function OrganizationList({ type, data: { id } }: Props) { | ||
const [keyword, setKeyword] = useState(''); | ||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); | ||
const { getPathname } = useTenantPathname(); | ||
|
||
// Since these APIs' pagination are optional or disabled (to align with ID token claims): | ||
// - We don't need to use the `page` state. | ||
// - We can perform frontend filtering. | ||
const { data: rawData, error } = useSWR<OrganizationWithRoles[], RequestError>( | ||
`api/${type}s/${id}/organizations` | ||
); | ||
const isLoading = !rawData && !error; | ||
const data = rawData?.filter(({ name }) => name.toLowerCase().includes(keyword.toLowerCase())); | ||
|
||
return ( | ||
<Table | ||
isLoading={isLoading} | ||
rowIndexKey="id" | ||
rowGroups={[{ key: 'data', data }]} | ||
placeholder={<EmptyDataPlaceholder />} | ||
columns={[ | ||
{ | ||
title: t('general.name'), | ||
dataIndex: 'name', | ||
render: ({ name, id }) => ( | ||
<ItemPreview | ||
title={name} | ||
icon={<ThemedIcon for={OrganizationIcon} />} | ||
to={getPathname(`/organizations/${id}`)} | ||
/> | ||
), | ||
}, | ||
{ | ||
title: t('organizations.organization_id'), | ||
dataIndex: 'id', | ||
render: ({ id }) => <CopyToClipboard value={id} variant="text" />, | ||
}, | ||
{ | ||
title: ( | ||
<div className={styles.rolesHeader}> | ||
{t('organizations.organization_role_other')} | ||
<ToggleTip | ||
content={t('organization_details.organization_roles_tooltip', { | ||
type: t(`organization_details.${type}`), | ||
})} | ||
horizontalAlign="start" | ||
> | ||
<IconButton size="small"> | ||
<Tip /> | ||
</IconButton> | ||
</ToggleTip> | ||
</div> | ||
), | ||
dataIndex: 'roles', | ||
render: ({ organizationRoles }) => ( | ||
<div className={styles.roles}> | ||
{organizationRoles.map(({ id, name }) => ( | ||
<Tag key={id} variant="cell"> | ||
<RoleOption value={id} title={name} /> | ||
</Tag> | ||
))} | ||
{organizationRoles.length === 0 && '-'} | ||
</div> | ||
), | ||
}, | ||
]} | ||
filter={ | ||
<Search | ||
defaultValue={keyword} | ||
isClearable={Boolean(keyword)} | ||
placeholder={t(`organization_details.search_${type}_placeholder`)} | ||
onSearch={setKeyword} | ||
onClearSearch={() => { | ||
setKeyword(''); | ||
}} | ||
/> | ||
} | ||
/> | ||
); | ||
} | ||
|
||
export default OrganizationList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
102 changes: 3 additions & 99 deletions
102
packages/console/src/pages/UserDetails/UserOrganizations/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,108 +1,12 @@ | ||
import { type OrganizationWithRoles } from '@logto/schemas'; | ||
import { useState } from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { useOutletContext } from 'react-router-dom'; | ||
import useSWR from 'swr'; | ||
|
||
import OrganizationIcon from '@/assets/icons/organization-preview.svg'; | ||
import Tip from '@/assets/icons/tip.svg'; | ||
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder'; | ||
import ItemPreview from '@/components/ItemPreview'; | ||
import { RoleOption } from '@/components/OrganizationRolesSelect'; | ||
import ThemedIcon from '@/components/ThemedIcon'; | ||
import CopyToClipboard from '@/ds-components/CopyToClipboard'; | ||
import IconButton from '@/ds-components/IconButton'; | ||
import Search from '@/ds-components/Search'; | ||
import Table from '@/ds-components/Table'; | ||
import Tag from '@/ds-components/Tag'; | ||
import { ToggleTip } from '@/ds-components/Tip'; | ||
import { type RequestError } from '@/hooks/use-api'; | ||
import useTenantPathname from '@/hooks/use-tenant-pathname'; | ||
import { buildUrl } from '@/utils/url'; | ||
import OrganizationList from '@/components/OrganizationList'; | ||
|
||
import { type UserDetailsOutletContext } from '../types'; | ||
|
||
import * as styles from './index.module.scss'; | ||
|
||
function UserOrganizations() { | ||
const [keyword, setKeyword] = useState(''); | ||
const { | ||
user: { id }, | ||
} = useOutletContext<UserDetailsOutletContext>(); | ||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); | ||
const { getPathname } = useTenantPathname(); | ||
|
||
// Since this API has no pagination (to align with ID token claims): | ||
// - We don't need to use the `page` state. | ||
// - We can perform frontend filtering. | ||
const { data: rawData, error } = useSWR<OrganizationWithRoles[], RequestError>( | ||
buildUrl(`api/users/${id}/organizations`, { showFeatured: '1' }) | ||
); | ||
const isLoading = !rawData && !error; | ||
const data = rawData?.filter(({ name }) => name.toLowerCase().includes(keyword.toLowerCase())); | ||
|
||
return ( | ||
<Table | ||
isLoading={isLoading} | ||
rowIndexKey="id" | ||
rowGroups={[{ key: 'data', data }]} | ||
placeholder={<EmptyDataPlaceholder />} | ||
columns={[ | ||
{ | ||
title: t('general.name'), | ||
dataIndex: 'name', | ||
render: ({ name, id }) => ( | ||
<ItemPreview | ||
title={name} | ||
icon={<ThemedIcon for={OrganizationIcon} />} | ||
to={getPathname(`/organizations/${id}`)} | ||
/> | ||
), | ||
}, | ||
{ | ||
title: t('organizations.organization_id'), | ||
dataIndex: 'id', | ||
render: ({ id }) => <CopyToClipboard value={id} variant="text" />, | ||
}, | ||
{ | ||
title: ( | ||
<div className={styles.rolesHeader}> | ||
{t('organizations.organization_role_other')} | ||
<ToggleTip | ||
content={t('user_details.organization_roles_tooltip')} | ||
horizontalAlign="start" | ||
> | ||
<IconButton size="small"> | ||
<Tip /> | ||
</IconButton> | ||
</ToggleTip> | ||
</div> | ||
), | ||
dataIndex: 'roles', | ||
render: ({ organizationRoles }) => ( | ||
<div className={styles.roles}> | ||
{organizationRoles.map(({ id, name }) => ( | ||
<Tag key={id} variant="cell"> | ||
<RoleOption value={id} title={name} /> | ||
</Tag> | ||
))} | ||
</div> | ||
), | ||
}, | ||
]} | ||
filter={ | ||
<Search | ||
defaultValue={keyword} | ||
isClearable={Boolean(keyword)} | ||
placeholder={t('organization_details.search_user_placeholder')} | ||
onSearch={setKeyword} | ||
onClearSearch={() => { | ||
setKeyword(''); | ||
}} | ||
/> | ||
} | ||
/> | ||
); | ||
const { user } = useOutletContext<UserDetailsOutletContext>(); | ||
return <OrganizationList type="user" data={user} />; | ||
} | ||
|
||
export default UserOrganizations; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.