import type PolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyRepresentation"; import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation"; import { Button, DescriptionList, PageSection, ToolbarItem, } from "@patternfly/react-core"; import { ExpandableRowContent, TableComposable, Tbody, Td, Th, Thead, Tr, } from "@patternfly/react-table"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Link, useNavigate } from "react-router-dom"; import { adminClient } from "../../admin-client"; import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner"; import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState"; import { PaginatingTableToolbar } from "../../components/table-toolbar/PaginatingTableToolbar"; import { useRealm } from "../../context/realm-context/RealmContext"; import { useFetch } from "../../utils/useFetch"; import useToggle from "../../utils/useToggle"; import { toNewPermission } from "../routes/NewPermission"; import { toNewScope } from "../routes/NewScope"; import { toPermissionDetails } from "../routes/PermissionDetails"; import { toResourceDetails } from "../routes/Resource"; import { toScopeDetails } from "../routes/Scope"; import { DeleteScopeDialog } from "./DeleteScopeDialog"; import { DetailDescriptionLink } from "./DetailDescription"; type ScopesProps = { clientId: string; isDisabled?: boolean; }; export type PermissionScopeRepresentation = ScopeRepresentation & { permissions?: PolicyRepresentation[]; isLoaded: boolean; }; type ExpandableRow = { id: string; isExpanded: boolean; }; export const AuthorizationScopes = ({ clientId, isDisabled = false, }: ScopesProps) => { const { t } = useTranslation(); const navigate = useNavigate(); const { realm } = useRealm(); const [deleteDialog, toggleDeleteDialog] = useToggle(); const [scopes, setScopes] = useState(); const [selectedScope, setSelectedScope] = useState(); const [collapsed, setCollapsed] = useState([]); const [key, setKey] = useState(0); const refresh = () => setKey(key + 1); const [max, setMax] = useState(10); const [first, setFirst] = useState(0); const [search, setSearch] = useState(""); useFetch( () => { const params = { first, max: max + 1, deep: false, name: search, }; return adminClient.clients.listAllScopes({ ...params, id: clientId, }); }, (scopes) => { setScopes(scopes.map((s) => ({ ...s, isLoaded: false }))); setCollapsed(scopes.map((s) => ({ id: s.id!, isExpanded: false }))); }, [key, search, first, max], ); const getScope = (id: string) => scopes?.find((scope) => scope.id === id)!; const isExpanded = (id: string | undefined) => collapsed.find((c) => c.id === id)?.isExpanded || false; useFetch( () => { const newlyOpened = collapsed .filter((row) => row.isExpanded) .map(({ id }) => getScope(id)) .filter((s) => !s.isLoaded); return Promise.all( newlyOpened.map(async (scope) => { const [resources, permissions] = await Promise.all([ adminClient.clients.listAllResourcesByScope({ id: clientId, scopeId: scope.id!, }), adminClient.clients.listAllPermissionsByScope({ id: clientId, scopeId: scope.id!, }), ]); return { ...scope, resources, permissions, 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], ); if (!scopes) { return ; } const noData = scopes.length === 0; const searching = search !== ""; return ( {(!noData || searching) && ( { setFirst(first); setMax(max); }} inputGroupName="search" inputGroupPlaceholder={t("searchByName")} inputGroupOnEnter={setSearch} toolbarItem={ } > {!noData && ( {t("name")} {t("displayName")} {scopes.map((scope, rowIndex) => ( { setCollapsed([ ...collapsed.slice(0, index), { id: scope.id!, isExpanded }, ...collapsed.slice(index + 1), ]); }, }} /> {scope.name} {scope.displayName} { setSelectedScope(scope); toggleDeleteDialog(); }, }, ], }} /> {isExpanded(scope.id) && scope.isLoaded ? ( r.name!} link={(r) => toResourceDetails({ id: clientId, realm, resourceId: r._id!, }) } /> p.name!} link={(p) => toPermissionDetails({ id: clientId, realm, permissionId: p.id!, permissionType: p.type!, }) } /> ) : ( )} ))} )} )} {noData && !searching && ( navigate(toNewScope({ id: clientId, realm }))} primaryActionText={t("createAuthorizationScope")} /> )} {noData && searching && ( )} ); };