import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation"; import { AlertVariant, Button, ButtonVariant, Dropdown, DropdownItem, KebabToggle, ToolbarItem, } from "@patternfly/react-core"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { adminClient } from "../../admin-client"; import { ChangeTypeDropdown } from "../../client-scopes/ChangeTypeDropdown"; import { SearchDropdown, SearchToolbar, SearchType, nameFilter, typeFilter, } from "../../client-scopes/details/SearchFilter"; import { useAlerts } from "../../components/alert/Alerts"; import { AllClientScopeType, AllClientScopes, CellDropdown, ClientScope, addClientScope, changeClientScope, removeClientScope, } from "../../components/client-scope/ClientScopeTypes"; import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog"; import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState"; import { Action, KeycloakDataTable, } from "../../components/table-toolbar/KeycloakDataTable"; import { useAccess } from "../../context/access/Access"; import { useRealm } from "../../context/realm-context/RealmContext"; import useLocaleSort, { mapByKey } from "../../utils/useLocaleSort"; import { toDedicatedScope } from "../routes/DedicatedScopeDetails"; import { AddScopeDialog } from "./AddScopeDialog"; import "./client-scopes.css"; export type ClientScopesProps = { clientId: string; protocol: string; clientName: string; fineGrainedAccess?: boolean; }; export type Row = ClientScopeRepresentation & { type: AllClientScopeType; description?: string; }; const DEDICATED_ROW = "dedicated"; type TypeSelectorProps = Row & { clientId: string; fineGrainedAccess?: boolean; refresh: () => void; }; const TypeSelector = ({ clientId, refresh, fineGrainedAccess, ...scope }: TypeSelectorProps) => { const { t } = useTranslation(); const { addAlert, addError } = useAlerts(); const { hasAccess } = useAccess(); const isDedicatedRow = (value: Row) => value.id === DEDICATED_ROW; const isManager = hasAccess("manage-clients") || fineGrainedAccess; return ( { try { await changeClientScope( clientId, scope, scope.type, value as ClientScope, ); addAlert(t("clientScopeSuccess"), AlertVariant.success); refresh(); } catch (error) { addError("clientScopeError", error); } }} /> ); }; export const ClientScopes = ({ clientId, protocol, clientName, fineGrainedAccess, }: ClientScopesProps) => { const { t } = useTranslation(); const { addAlert, addError } = useAlerts(); const { realm } = useRealm(); const localeSort = useLocaleSort(); const [searchType, setSearchType] = useState("name"); const [searchTypeType, setSearchTypeType] = useState( AllClientScopes.none, ); const [addDialogOpen, setAddDialogOpen] = useState(false); const [rest, setRest] = useState(); const [selectedRows, setSelectedRowState] = useState([]); const setSelectedRows = (rows: Row[]) => setSelectedRowState(rows.filter(({ id }) => id !== DEDICATED_ROW)); const [kebabOpen, setKebabOpen] = useState(false); const [key, setKey] = useState(0); const refresh = () => setKey(key + 1); const isDedicatedRow = (value: Row) => value.id === DEDICATED_ROW; const { hasAccess } = useAccess(); const isManager = hasAccess("manage-clients") || fineGrainedAccess; const isViewer = hasAccess("view-clients") || fineGrainedAccess; const loader = async (first?: number, max?: number, search?: string) => { const defaultClientScopes = await adminClient.clients.listDefaultClientScopes({ id: clientId }); const optionalClientScopes = await adminClient.clients.listOptionalClientScopes({ id: clientId }); const clientScopes = await adminClient.clientScopes.find(); const find = (id: string) => clientScopes.find((clientScope) => id === clientScope.id); const optional = optionalClientScopes.map((c) => { const scope = find(c.id!); const row: Row = { ...c, type: ClientScope.optional, description: scope?.description, }; return row; }); const defaultScopes = defaultClientScopes.map((c) => { const scope = find(c.id!); const row: Row = { ...c, type: ClientScope.default, description: scope?.description, }; return row; }); const rows = [...optional, ...defaultScopes]; const names = rows.map((row) => row.name); setRest( clientScopes .filter((scope) => !names.includes(scope.name)) .filter((scope) => scope.protocol === protocol), ); const filter = searchType === "name" ? nameFilter(search) : typeFilter(searchTypeType); const firstNum = Number(first); const page = localeSort(rows.filter(filter), mapByKey("name")); if (isViewer) { page.unshift({ id: DEDICATED_ROW, name: t("dedicatedScopeName", { clientName }), type: AllClientScopes.none, description: t("dedicatedScopeDescription"), }); } return page.slice(firstNum, firstNum + Number(max)); }; const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ titleKey: t("deleteClientScope", { count: selectedRows.length, name: selectedRows[0]?.name, }), messageKey: "deleteConfirmClientScopes", continueButtonLabel: "common:delete", continueButtonVariant: ButtonVariant.danger, onConfirm: async () => { try { await removeClientScope( clientId, selectedRows[0], selectedRows[0].type as ClientScope, ); addAlert(t("clientScopeRemoveSuccess"), AlertVariant.success); refresh(); } catch (error) { addError("clientScopeRemoveError", error); } }, }); return ( <> {rest && ( setAddDialogOpen(!addDialogOpen)} onAdd={async (scopes) => { try { await Promise.all( scopes.map( async (scope) => await addClientScope(clientId, scope.scope, scope.type!), ), ); addAlert(t("clientScopeSuccess"), AlertVariant.success); refresh(); } catch (error) { addError("clientScopeError", error); } }} /> )} setSelectedRows([...rows])} searchTypeComponent={ setSearchType(searchType)} /> } toolbarItem={ <> setSearchType(searchType)} onType={(value) => { setSearchTypeType(value); refresh(); }} /> {isManager && ( <> setKebabOpen(!kebabOpen)} /> } isOpen={kebabOpen} isPlain dropdownItems={[ { try { await Promise.all( selectedRows.map((row) => removeClientScope( clientId, { ...row }, row.type as ClientScope, ), ), ); setKebabOpen(false); setSelectedRows([]); addAlert(t("clientScopeRemoveSuccess")); refresh(); } catch (error) { addError("clientScopeRemoveError", error); } }} > {t("common:remove")} , ]} /> )} } columns={[ { name: "name", displayKey: "assignedClientScope", cellRenderer: (row) => { if (isDedicatedRow(row)) { return ( {row.name} ); } return row.name!; }, }, { name: "type", displayKey: "assignedType", cellRenderer: (row) => ( ), }, { name: "description" }, ]} actions={ isManager ? [ { title: t("common:remove"), onRowClick: async (row) => { setSelectedRows([row]); toggleDeleteDialog(); return true; }, } as Action, ] : [] } emptyState={ setAddDialogOpen(true)} /> } /> ); };