-
-
Notifications
You must be signed in to change notification settings - Fork 477
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(console): assign permissions for org roles
- Loading branch information
Showing
23 changed files
with
416 additions
and
14 deletions.
There are no files selected for viewing
125 changes: 125 additions & 0 deletions
125
packages/console/src/components/OrganizationRolePermissionsAssignmentModal/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,125 @@ | ||
import { type AdminConsoleKey } from '@logto/phrases'; | ||
import { useCallback, useMemo } from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
|
||
import ConfirmModal from '@/ds-components/ConfirmModal'; | ||
import DataTransferBox from '@/ds-components/DataTransferBox'; | ||
import TabNav, { TabNavItem } from '@/ds-components/TabNav'; | ||
import TabWrapper from '@/ds-components/TabWrapper'; | ||
|
||
import { PermissionType } from './types'; | ||
import useOrganizationRolePermissionsAssignment from './use-organization-role-permissions-assignment'; | ||
|
||
const permissionTabs = { | ||
[PermissionType.Organization]: { | ||
title: 'organization_role_details.permissions.organization_permissions', | ||
key: PermissionType.Organization, | ||
}, | ||
[PermissionType.Api]: { | ||
title: 'organization_role_details.permissions.api_permissions', | ||
key: PermissionType.Api, | ||
}, | ||
} satisfies { | ||
[key in PermissionType]: { | ||
title: AdminConsoleKey; | ||
key: key; | ||
}; | ||
}; | ||
|
||
type Props = { | ||
organizationRoleId: string; | ||
isOpen: boolean; | ||
onClose: () => void; | ||
}; | ||
|
||
function OrganizationRolePermissionsAssignmentModal({ | ||
organizationRoleId, | ||
isOpen, | ||
onClose, | ||
}: Props) { | ||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); | ||
|
||
const { | ||
activeTab, | ||
setActiveTab, | ||
onSubmit, | ||
organizationScopesAssignment, | ||
resourceScopesAssignment, | ||
clearSelectedData, | ||
isLoading, | ||
} = useOrganizationRolePermissionsAssignment(organizationRoleId); | ||
|
||
const onCloseHandler = useCallback(() => { | ||
onClose(); | ||
clearSelectedData(); | ||
setActiveTab(PermissionType.Organization); | ||
}, [clearSelectedData, onClose, setActiveTab]); | ||
|
||
const onSubmitHandler = useCallback(async () => { | ||
await onSubmit(); | ||
onCloseHandler(); | ||
}, [onCloseHandler, onSubmit]); | ||
|
||
const tabs = useMemo( | ||
() => | ||
Object.values(permissionTabs).map(({ title, key }) => { | ||
const selectedDataCount = | ||
key === PermissionType.Organization | ||
? organizationScopesAssignment.selectedData.length | ||
: resourceScopesAssignment.selectedData.length; | ||
|
||
return ( | ||
<TabNavItem | ||
key={key} | ||
isActive={key === activeTab} | ||
onClick={() => { | ||
setActiveTab(key); | ||
}} | ||
> | ||
{`${t(title)}${selectedDataCount ? ` (${selectedDataCount})` : ''}`} | ||
</TabNavItem> | ||
); | ||
}), | ||
[ | ||
activeTab, | ||
organizationScopesAssignment.selectedData.length, | ||
resourceScopesAssignment.selectedData.length, | ||
setActiveTab, | ||
t, | ||
] | ||
); | ||
|
||
return ( | ||
<ConfirmModal | ||
isOpen={isOpen} | ||
isLoading={isLoading} | ||
title="organization_role_details.permissions.assign_permissions" | ||
subtitle="organization_role_details.permissions.assign_description" | ||
confirmButtonType="primary" | ||
confirmButtonText="general.save" | ||
cancelButtonText="general.discard" | ||
size="large" | ||
onCancel={onCloseHandler} | ||
onConfirm={onSubmitHandler} | ||
> | ||
<TabNav>{tabs}</TabNav> | ||
<TabWrapper | ||
key={PermissionType.Organization} | ||
isActive={PermissionType.Organization === activeTab} | ||
> | ||
<DataTransferBox | ||
title="organization_role_details.permissions.assign_organization_permissions" | ||
{...organizationScopesAssignment} | ||
/> | ||
</TabWrapper> | ||
<TabWrapper key={PermissionType.Api} isActive={PermissionType.Api === activeTab}> | ||
<DataTransferBox | ||
title="organization_role_details.permissions.assign_api_permissions" | ||
{...resourceScopesAssignment} | ||
/> | ||
</TabWrapper> | ||
</ConfirmModal> | ||
); | ||
} | ||
|
||
export default OrganizationRolePermissionsAssignmentModal; |
4 changes: 4 additions & 0 deletions
4
packages/console/src/components/OrganizationRolePermissionsAssignmentModal/types.ts
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,4 @@ | ||
export enum PermissionType { | ||
Organization = 'Organization', | ||
Api = 'Api', | ||
} |
82 changes: 82 additions & 0 deletions
82
...rganizationRolePermissionsAssignmentModal/use-organization-role-permissions-assignment.ts
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,82 @@ | ||
import { cond } from '@silverhand/essentials'; | ||
import { useCallback, useMemo, useState } from 'react'; | ||
|
||
import useApi from '@/hooks/use-api'; | ||
import useOrganizationRoleScopes from '@/pages/OrganizationRoleDetails/Permissions/use-organization-role-scopes'; | ||
|
||
import { PermissionType } from './types'; | ||
import useOrganizationScopesAssignment from './use-organization-scopes-assignment'; | ||
import useResourceScopesAssignment from './use-resource-scopes-assignment'; | ||
|
||
function useOrganizationRolePermissionsAssignment(organizationRoleId: string) { | ||
const organizationRolePath = `api/organization-roles/${organizationRoleId}`; | ||
const [activeTab, setActiveTab] = useState<PermissionType>(PermissionType.Organization); | ||
const [isLoading, setIsLoading] = useState(false); | ||
const api = useApi(); | ||
|
||
const { organizationScopes, resourceScopes, mutate } = | ||
useOrganizationRoleScopes(organizationRoleId); | ||
|
||
const organizationScopesAssignment = useOrganizationScopesAssignment(organizationScopes); | ||
const resourceScopesAssignment = useResourceScopesAssignment(resourceScopes); | ||
|
||
const clearSelectedData = useCallback(() => { | ||
organizationScopesAssignment.setSelectedData([]); | ||
resourceScopesAssignment.setSelectedData([]); | ||
}, [organizationScopesAssignment, resourceScopesAssignment]); | ||
|
||
const onSubmit = useCallback(async () => { | ||
setIsLoading(true); | ||
const newOrganizationScopes = organizationScopesAssignment.selectedData.map(({ id }) => id); | ||
const newResourceScopes = resourceScopesAssignment.selectedData.map(({ id }) => id); | ||
|
||
await Promise.all( | ||
[ | ||
cond( | ||
newOrganizationScopes.length > 0 && | ||
api.post(`${organizationRolePath}/scopes`, { | ||
json: { organizationScopeIds: newOrganizationScopes }, | ||
}) | ||
), | ||
cond( | ||
newResourceScopes.length > 0 && | ||
api.post(`${organizationRolePath}/resource-scopes`, { | ||
json: { scopeIds: newResourceScopes }, | ||
}) | ||
), | ||
].filter(Boolean) | ||
).finally(() => { | ||
setIsLoading(false); | ||
}); | ||
|
||
mutate(); | ||
}, [ | ||
api, | ||
mutate, | ||
organizationRolePath, | ||
organizationScopesAssignment.selectedData, | ||
resourceScopesAssignment.selectedData, | ||
]); | ||
|
||
return useMemo( | ||
() => ({ | ||
activeTab, | ||
setActiveTab, | ||
isLoading, | ||
organizationScopesAssignment, | ||
resourceScopesAssignment, | ||
clearSelectedData, | ||
onSubmit, | ||
}), | ||
[ | ||
activeTab, | ||
clearSelectedData, | ||
isLoading, | ||
onSubmit, | ||
organizationScopesAssignment, | ||
resourceScopesAssignment, | ||
] | ||
); | ||
} | ||
|
||
export default useOrganizationRolePermissionsAssignment; |
28 changes: 28 additions & 0 deletions
28
...mponents/OrganizationRolePermissionsAssignmentModal/use-organization-scopes-assignment.ts
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,28 @@ | ||
import { type OrganizationScope } from '@logto/schemas'; | ||
import { useMemo, useState } from 'react'; | ||
import useSWR from 'swr'; | ||
|
||
function useOrganizationScopesAssignment(assignedScopes: OrganizationScope[] = []) { | ||
const [selectedData, setSelectedData] = useState<OrganizationScope[]>([]); | ||
|
||
const { data: organizationScopes } = useSWR<OrganizationScope[]>('api/organization-scopes'); | ||
|
||
const availableDataList = useMemo( | ||
() => | ||
(organizationScopes ?? []).filter( | ||
({ id }) => !assignedScopes.some((scope) => scope.id === id) | ||
), | ||
[organizationScopes, assignedScopes] | ||
); | ||
|
||
return useMemo( | ||
() => ({ | ||
selectedData, | ||
setSelectedData, | ||
availableDataList, | ||
}), | ||
[selectedData, setSelectedData, availableDataList] | ||
); | ||
} | ||
|
||
export default useOrganizationScopesAssignment; |
42 changes: 42 additions & 0 deletions
42
...c/components/OrganizationRolePermissionsAssignmentModal/use-resource-scopes-assignment.ts
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,42 @@ | ||
import { isManagementApi, type Scope, type ResourceResponse } from '@logto/schemas'; | ||
import { useMemo, useState } from 'react'; | ||
import useSWR from 'swr'; | ||
|
||
import { type DataGroup } from '@/ds-components/DataTransferBox/type'; | ||
|
||
function useResourceScopesAssignment(assignedScopes?: Scope[]) { | ||
const [selectedData, setSelectedData] = useState<Scope[]>([]); | ||
|
||
const { data: allResources } = useSWR<ResourceResponse[]>('api/resources?includeScopes=true'); | ||
|
||
const availableDataGroups: Array<DataGroup<Scope>> = useMemo(() => { | ||
if (!allResources) { | ||
return []; | ||
} | ||
|
||
const resourcesWithScopes = allResources | ||
// Filter out the management APIs | ||
.filter((resource) => !isManagementApi(resource.indicator)) | ||
.map(({ name, scopes, id: resourceId }) => ({ | ||
groupId: resourceId, | ||
groupName: name, | ||
dataList: scopes | ||
// Filter out the scopes that have been assigned | ||
.filter(({ id: scopeId }) => !assignedScopes?.some((scope) => scope.id === scopeId)), | ||
})); | ||
|
||
// Filter out the resources that have no scopes | ||
return resourcesWithScopes.filter(({ dataList }) => dataList.length > 0); | ||
}, [allResources, assignedScopes]); | ||
|
||
return useMemo( | ||
() => ({ | ||
selectedData, | ||
setSelectedData, | ||
availableDataGroups, | ||
}), | ||
[availableDataGroups, selectedData] | ||
); | ||
} | ||
|
||
export default useResourceScopesAssignment; |
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.