import { useState } from "react"; import { Link, useNavigate } from "react-router-dom-v5-compat"; import { useTranslation } from "react-i18next"; import { Button, DescriptionList, PageSection, ToolbarItem, } from "@patternfly/react-core"; import { ExpandableRowContent, TableComposable, Tbody, Td, Th, Thead, Tr, } from "@patternfly/react-table"; import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation"; import type PolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyRepresentation"; import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner"; import { PaginatingTableToolbar } from "../../components/table-toolbar/PaginatingTableToolbar"; import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; import { useRealm } from "../../context/realm-context/RealmContext"; import { MoreLabel } from "./MoreLabel"; import { toScopeDetails } from "../routes/Scope"; import { toNewScope } from "../routes/NewScope"; import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState"; import useToggle from "../../utils/useToggle"; import { DeleteScopeDialog } from "./DeleteScopeDialog"; import { DetailDescriptionLink } from "./DetailDescription"; import { toNewPermission } from "../routes/NewPermission"; import { toResourceDetails } from "../routes/Resource"; import { toPermissionDetails } from "../routes/PermissionDetails"; type ScopesProps = { clientId: string; }; export type ExpandableScopeRepresentation = ScopeRepresentation & { permissions?: PolicyRepresentation[]; isExpanded: boolean; }; export const AuthorizationScopes = ({ clientId }: ScopesProps) => { const { t } = useTranslation("clients"); const navigate = useNavigate(); const { adminClient } = useAdminClient(); const { realm } = useRealm(); const [deleteDialog, toggleDeleteDialog] = useToggle(); const [scopes, setScopes] = useState(); const [selectedScope, setSelectedScope] = 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( async () => { const params = { first, max: max + 1, deep: false, name: search, }; const scopes = await adminClient.clients.listAllScopes({ ...params, id: clientId, }); 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), ]); return { ...scope, resources, permissions, isExpanded: false, }; }) ); }, setScopes, [key, search, first, max] ); const ResourceRenderer = ({ row, }: { row: ExpandableScopeRepresentation; }) => ( <> {row.resources?.[0]?.name ? row.resources[0]?.name : "—"}{" "} ); const PermissionsRenderer = ({ row, }: { row: ExpandableScopeRepresentation; }) => ( <> {row.permissions?.[0]?.name ? row.permissions[0]?.name : "—"}{" "} ); 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("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); }, }} /> {scope.name} { setSelectedScope(scope); toggleDeleteDialog(); }, }, ], }} /> {scope.isExpanded && ( 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 && ( )} ); };