diff --git a/apps/admin-ui/src/clients/authorization/DeleteScopeDialog.tsx b/apps/admin-ui/src/clients/authorization/DeleteScopeDialog.tsx index abce991a8c..f740a4cf89 100644 --- a/apps/admin-ui/src/clients/authorization/DeleteScopeDialog.tsx +++ b/apps/admin-ui/src/clients/authorization/DeleteScopeDialog.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import { Alert, AlertVariant } from "@patternfly/react-core"; -import type { ExpandableScopeRepresentation } from "./Scopes"; +import type { PermissionScopeRepresentation } from "./Scopes"; import { useAlerts } from "../../components/alert/Alerts"; import { ConfirmDialogModal } from "../../components/confirm-dialog/ConfirmDialog"; import { useAdminClient } from "../../context/auth/AdminClient"; @@ -10,7 +10,7 @@ import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/s type DeleteScopeDialogProps = { clientId: string; selectedScope: - | ExpandableScopeRepresentation + | PermissionScopeRepresentation | ScopeRepresentation | undefined; refresh: () => void; diff --git a/apps/admin-ui/src/clients/authorization/Scopes.tsx b/apps/admin-ui/src/clients/authorization/Scopes.tsx index 1fe65a0227..37add16ef7 100644 --- a/apps/admin-ui/src/clients/authorization/Scopes.tsx +++ b/apps/admin-ui/src/clients/authorization/Scopes.tsx @@ -39,8 +39,13 @@ type ScopesProps = { clientId: string; }; -export type ExpandableScopeRepresentation = ScopeRepresentation & { +export type PermissionScopeRepresentation = ScopeRepresentation & { permissions?: PolicyRepresentation[]; + isLoaded: boolean; +}; + +type ExpandableRow = { + id: string; isExpanded: boolean; }; @@ -51,9 +56,10 @@ export const AuthorizationScopes = ({ clientId }: ScopesProps) => { const { realm } = useRealm(); const [deleteDialog, toggleDeleteDialog] = useToggle(); - const [scopes, setScopes] = useState(); + const [scopes, setScopes] = useState(); const [selectedScope, setSelectedScope] = - useState(); + useState(); + const [collapsed, setCollapsed] = useState([]); const [key, setKey] = useState(0); const refresh = () => setKey(key + 1); @@ -78,39 +84,68 @@ export const AuthorizationScopes = ({ clientId }: ScopesProps) => { return await Promise.all( scopes.map(async (scope) => { const options = { id: clientId, scopeId: scope.id! }; - const [resources, permissions] = await Promise.all([ - adminClient.clients.listAllResourcesByScope(options), - adminClient.clients.listAllPermissionsByScope(options), - ]); + const permissions = + await adminClient.clients.listAllPermissionsByScope(options); return { ...scope, - resources, permissions, - isExpanded: false, + isLoaded: false, }; }) ); }, - setScopes, + (scopes) => { + setScopes(scopes); + setCollapsed(scopes.map((s) => ({ id: s.id!, isExpanded: false }))); + }, [key, search, first, max] ); - const ResourceRenderer = ({ - row, - }: { - row: ExpandableScopeRepresentation; - }) => ( - <> - {row.resources?.[0]?.name ? row.resources[0]?.name : "—"}{" "} - - + const getScope = (id: string) => scopes?.find((scope) => scope.id === id)!; + const isExpanded = (id: string | undefined) => + collapsed.find((c) => c.id === id)?.isExpanded || false; + + useFetch( + async () => { + const newlyOpened = collapsed + .filter((row) => row.isExpanded) + .map(({ id }) => getScope(id)) + .filter((s) => !s.isLoaded); + + return await Promise.all( + newlyOpened.map(async (scope) => ({ + ...scope, + resources: await adminClient.clients.listAllResourcesByScope({ + id: clientId, + scopeId: scope.id!, + }), + isLoaded: true, + })) + ); + }, + (resourcesScopes) => { + let result = [...(scopes || [])]; + resourcesScopes.forEach((resourceScope) => { + const index = scopes?.findIndex( + (scope) => resourceScope.id === scope.id + )!; + result = [ + ...result.slice(0, index), + resourceScope, + ...result.slice(index + 1), + ]; + }); + + setScopes(result); + }, + [collapsed] ); const PermissionsRenderer = ({ row, }: { - row: ExpandableScopeRepresentation; + row: PermissionScopeRepresentation; }) => ( <> {row.permissions?.[0]?.name ? row.permissions[0]?.name : "—"}{" "} @@ -166,29 +201,24 @@ export const AuthorizationScopes = ({ clientId }: ScopesProps) => { {t("common:name")} - {t("resources")} {t("common:permissions")} {scopes.map((scope, rowIndex) => ( - + { - const rows = scopes.map((resource, index) => - index === rowIndex - ? { - ...resource, - isExpanded: !resource.isExpanded, - } - : resource - ); - setScopes(rows); + isExpanded: isExpanded(scope.id), + onToggle: (_event, index, isExpanded) => { + setCollapsed([ + ...collapsed.slice(0, index), + { id: scope.id!, isExpanded }, + ...collapsed.slice(index + 1), + ]); }, }} /> @@ -203,9 +233,6 @@ export const AuthorizationScopes = ({ clientId }: ScopesProps) => { {scope.name} - - - @@ -242,11 +269,14 @@ export const AuthorizationScopes = ({ clientId }: ScopesProps) => { }} /> - + - {scope.isExpanded && ( + {isExpanded(scope.id) && (