From dfc4beced4e9ab21cbded75f863e482f2c7a9a55 Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Fri, 19 Mar 2021 13:43:32 +0100 Subject: [PATCH] changed to use the keycloak datatable (#432) * changed to use the keycloak datatable so it will benifid from a general way tables work in the admin console * null check * changed to use any active tab seems sometimes the tab doesn't get set properly so instead of "mappers" it's still "settings" this change makes it work with both --- src/client-scopes/details/MapperList.tsx | 218 ++++---- src/clients/scopes/ClientScopes.tsx | 474 +++++++----------- .../service-account/ServiceAccount.tsx | 114 ++--- src/route-config.ts | 4 +- 4 files changed, 345 insertions(+), 465 deletions(-) diff --git a/src/client-scopes/details/MapperList.tsx b/src/client-scopes/details/MapperList.tsx index 25ebb67eb1..e9880b1944 100644 --- a/src/client-scopes/details/MapperList.tsx +++ b/src/client-scopes/details/MapperList.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Link, useHistory, useRouteMatch } from "react-router-dom"; import { @@ -8,12 +8,6 @@ import { DropdownItem, DropdownToggle, } from "@patternfly/react-core"; -import { - Table, - TableBody, - TableHeader, - TableVariant, -} from "@patternfly/react-table"; import { CaretDownIcon } from "@patternfly/react-icons"; import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation"; @@ -21,19 +15,18 @@ import ProtocolMapperRepresentation from "keycloak-admin/lib/defs/protocolMapper import { ProtocolMapperTypeRepresentation } from "keycloak-admin/lib/defs/serverInfoRepesentation"; import { useServerInfo } from "../../context/server-info/ServerInfoProvider"; -import { TableToolbar } from "../../components/table-toolbar/TableToolbar"; import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState"; import { useAlerts } from "../../components/alert/Alerts"; import { AddMapperDialog } from "../add/MapperDialog"; import { useAdminClient } from "../../context/auth/AdminClient"; +import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable"; type MapperListProps = { clientScope: ClientScopeRepresentation; refresh: () => void; }; -type Row = { - name: JSX.Element; +type Row = ProtocolMapperRepresentation & { category: string; type: string; priority: number; @@ -46,15 +39,15 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => { const history = useHistory(); const { url } = useRouteMatch(); - const [filteredData, setFilteredData] = useState< - { mapper: ProtocolMapperRepresentation; cells: Row }[] - >(); const [mapperAction, setMapperAction] = useState(false); const mapperList = clientScope.protocolMappers!; const mapperTypes = useServerInfo().protocolMapperTypes![ clientScope.protocol! ]; + const [key, setKey] = useState(0); + useEffect(() => setKey(new Date().getTime()), [mapperList]); + const [addMapperDialogOpen, setAddMapperDialogOpen] = useState(false); const [filter, setFilter] = useState(clientScope.protocolMappers); const toggleAddMapperDialog = (buildIn: boolean) => { @@ -86,98 +79,31 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => { } }; - if (!mapperList) { - return ( - <> - setAddMapperDialogOpen(!addMapperDialogOpen)} - /> - toggleAddMapperDialog(true)} - secondaryActions={[ - { - text: t("emptySecondaryAction"), - onClick: () => toggleAddMapperDialog(false), - type: ButtonVariant.secondary, - }, - ]} - /> - + const loader = async () => + Promise.resolve( + (mapperList || []) + .map((mapper) => { + const mapperType = mapperTypes.filter( + (type) => type.id === mapper.protocolMapper + )[0]; + return { + ...mapper, + category: mapperType.category, + type: mapperType.name, + priority: mapperType.priority, + } as Row; + }) + .sort((a, b) => a.priority - b.priority) ); - } - const data = mapperList - .map((mapper) => { - const mapperType = mapperTypes.filter( - (type) => type.id === mapper.protocolMapper - )[0]; - return { - mapper, - cells: { - name: ( - <> - {mapper.name} - - ), - category: mapperType.category, - type: mapperType.name, - priority: mapperType.priority, - } as Row, - }; - }) - .sort((a, b) => a.cells.priority - b.cells.priority); - - const filterData = (search: string) => { - setFilteredData( - data.filter((column) => - column.mapper.name!.toLowerCase().includes(search.toLowerCase()) - ) - ); - }; + const MapperLink = (mapper: Row) => ( + <> + {mapper.name} + + ); return ( - setMapperAction(false)} - toggle={ - setMapperAction(!mapperAction)} - toggleIndicator={CaretDownIcon} - > - {t("addMapper")} - - } - isOpen={mapperAction} - dropdownItems={[ - toggleAddMapperDialog(true)} - > - {t("fromPredefinedMapper")} - , - toggleAddMapperDialog(false)} - > - {t("byConfiguration")} - , - ]} - /> - } - > + <> { open={addMapperDialogOpen} toggleDialog={() => setAddMapperDialogOpen(!addMapperDialogOpen)} /> - { - return { cells: Object.values(cell.cells), mapper: cell.mapper }; - })} - aria-label={t("clientScopeList")} + + setMapperAction(false)} + toggle={ + setMapperAction(!mapperAction)} + toggleIndicator={CaretDownIcon} + > + {t("addMapper")} + + } + isOpen={mapperAction} + dropdownItems={[ + toggleAddMapperDialog(true)} + > + {t("fromPredefinedMapper")} + , + toggleAddMapperDialog(false)} + > + {t("byConfiguration")} + , + ]} + /> + } actions={[ { title: t("common:delete"), - onClick: async (_, rowId) => { + onRowClick: async (mapper) => { try { await adminClient.clientScopes.delProtocolMapper({ id: clientScope.id!, - mapperId: data[rowId].mapper.id!, + mapperId: mapper.id!, }); - refresh(); addAlert(t("mappingDeletedSuccess"), AlertVariant.success); + refresh(); } catch (error) { addAlert( t("mappingDeletedError", { error }), AlertVariant.danger ); } + return true; }, }, ]} - > - - -
-
+ columns={[ + { + name: "name", + cellRenderer: MapperLink, + }, + { name: "category" }, + { + name: "type", + }, + { + name: "priority", + }, + ]} + emptyState={ + toggleAddMapperDialog(true)} + secondaryActions={[ + { + text: t("emptySecondaryAction"), + onClick: () => toggleAddMapperDialog(false), + type: ButtonVariant.secondary, + }, + ]} + /> + } + /> + ); }; diff --git a/src/clients/scopes/ClientScopes.tsx b/src/clients/scopes/ClientScopes.tsx index 0841ae57e1..fc5bf9db2d 100644 --- a/src/clients/scopes/ClientScopes.tsx +++ b/src/clients/scopes/ClientScopes.tsx @@ -1,14 +1,5 @@ -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import { useTranslation } from "react-i18next"; -import { useErrorHandler } from "react-error-boundary"; -import { - IFormatter, - IFormatterValueType, - Table, - TableBody, - TableHeader, - TableVariant, -} from "@patternfly/react-table"; import { AlertVariant, Button, @@ -17,19 +8,14 @@ import { DropdownToggle, KebabToggle, Select, - Spinner, ToolbarItem, } from "@patternfly/react-core"; import { FilterIcon } from "@patternfly/react-icons"; import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation"; import KeycloakAdminClient from "keycloak-admin"; -import { - useAdminClient, - asyncStateFetch, -} from "../../context/auth/AdminClient"; +import { useAdminClient } from "../../context/auth/AdminClient"; import { toUpperCase } from "../../util"; -import { TableToolbar } from "../../components/table-toolbar/TableToolbar"; import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState"; import { AddScopeDialog } from "./AddScopeDialog"; import { @@ -38,12 +24,18 @@ import { ClientScope, } from "./ClientScopeTypes"; import { useAlerts } from "../../components/alert/Alerts"; +import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable"; export type ClientScopesProps = { clientId: string; protocol: string; }; +type Row = ClientScopeRepresentation & { + type: ClientScopeType; + description: string; +}; + const castAdminClient = (adminClient: KeycloakAdminClient) => (adminClient.clients as unknown) as { [index: string]: Function; @@ -124,7 +116,6 @@ type TableRow = { export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => { const { t } = useTranslation("clients"); const adminClient = useAdminClient(); - const handleError = useErrorHandler(); const { addAlert } = useAlerts(); const [searchToggle, setSearchToggle] = useState(false); @@ -133,100 +124,73 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => { const [addDialogOpen, setAddDialogOpen] = useState(false); const [kebabOpen, setKebabOpen] = useState(false); - const [rows, setRows] = useState(); const [rest, setRest] = useState(); + const [selectedRows, setSelectedRows] = useState([]); const [key, setKey] = useState(0); const refresh = () => setKey(new Date().getTime()); - useEffect(() => { - return asyncStateFetch( - async () => { - 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!); - return { - selected: false, - clientScope: c, - type: ClientScope.optional, - cells: [c.name, c.id, scope.description], - }; - }); - - const defaultScopes = defaultClientScopes.map((c) => { - const scope = find(c.id!); - return { - selected: false, - clientScope: c, - type: ClientScope.default, - cells: [c.name, c.id, scope.description], - }; - }); - - const rows = [...optional, ...defaultScopes]; - const names = rows.map((row) => row.cells[0]); - - const rest = clientScopes - .filter((scope) => !names.includes(scope.name)) - .filter((scope) => scope.protocol === protocol); - return { rows, rest }; - }, - ({ rows, rest }) => { - setRows(rows); - setRest(rest); - }, - handleError + const loader = async () => { + const defaultClientScopes = await adminClient.clients.listDefaultClientScopes( + { id: clientId } ); - }, [key]); + const optionalClientScopes = await adminClient.clients.listOptionalClientScopes( + { id: clientId } + ); + const clientScopes = await adminClient.clientScopes.find(); - const dropdown = (): IFormatter => (data?: IFormatterValueType) => { - if (!data) { - return <>; - } - const row = rows?.find((row) => row.clientScope.id === data.toString())!; - return ( + const find = (id: string) => + clientScopes.find((clientScope) => id === clientScope.id)!; + + const optional = optionalClientScopes.map((c) => { + const scope = find(c.id!); + return { + ...c, + type: ClientScope.optional, + description: scope.description, + } as Row; + }); + + const defaultScopes = defaultClientScopes.map((c) => { + const scope = find(c.id!); + return { + ...c, + type: ClientScope.default, + description: scope.description, + } as 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) + ); + + return rows; + }; + + const TypeSelector = (scope: Row) => ( + <> { try { - await changeScope( - adminClient, - clientId, - row.clientScope, - row.type, - value - ); + await changeScope(adminClient, clientId, scope, scope.type, value); addAlert(t("clientScopeSuccess"), AlertVariant.success); - await refresh(); + refresh(); } catch (error) { addAlert(t("clientScopeError", { error }), AlertVariant.danger); } }} /> - ); - }; - - const filterData = () => {}; + + ); return ( <> - {!rows && ( -
- -
- )} - {rest && ( { /> )} - {rows && rows.length > 0 && ( - setSearchToggle(!searchToggle)} - > - {t(`clientScopeSearch.${searchType}`)} - - } - aria-label="Select Input" - isOpen={searchToggle} - dropdownItems={[ - { - setSearchType("client"); - setSearchToggle(false); - }} - > - {t("clientScopeSearch.client")} - , - { - setSearchType("assigned"); - setSearchToggle(false); - }} - > - {t("clientScopeSearch.assigned")} - , - ]} - /> - } - inputGroupName="clientsScopeToolbarTextInput" - inputGroupPlaceholder={t("searchByName")} - inputGroupOnChange={filterData} - toolbarItem={ - <> - - - - - - - - {}} - toggle={ - setKebabOpen(!kebabOpen)} /> - } - isOpen={kebabOpen} - isPlain - dropdownItems={[ - row.selected).length === 0 - } - onClick={async () => { - try { - await Promise.all( - rows.map(async (row) => { - if (row.selected) { - await removeScope( - adminClient, - clientId, - row.clientScope, - row.type - ); - } - }) - ); - - setKebabOpen(false); - addAlert( - t("clientScopeRemoveSuccess"), - AlertVariant.success - ); - refresh(); - } catch (error) { - addAlert( - t("clientScopeRemoveError", { error }), - AlertVariant.danger - ); - } - }} - > - {t("common:remove")} - , - ]} - /> - - - } - > - { - if (rowIndex === -1) { - setRows( - rows.map((row) => { - row.selected = isSelected; - return row; - }) - ); - } else { - rows[rowIndex].selected = isSelected; - setRows([...rows]); - } - }} - variant={TableVariant.compact} - cells={[ - t("common:name"), - { title: t("assignedType"), cellFormatters: [dropdown()] }, - t("common:description"), + setSelectedRows([...rows])} + searchTypeComponent={ + setSearchToggle(!searchToggle)} + > + {t(`clientScopeSearch.${searchType}`)} + + } + aria-label="Select Input" + isOpen={searchToggle} + dropdownItems={[ + { + setSearchType("client"); + setSearchToggle(false); + }} + > + {t("clientScopeSearch.client")} + , + { + setSearchType("assigned"); + setSearchToggle(false); + }} + > + {t("clientScopeSearch.assigned")} + , ]} - rows={rows} - actions={[ - { - title: t("common:remove"), - onClick: async (_, rowId) => { + /> + } + toolbarItem={ + <> + + + + +
-
- )} - {rows && rows.length === 0 && ( - setAddDialogOpen(true)} - /> - )} + }} + > + {clientScopeTypesSelectOptions(t)} + + + + {}} + toggle={ + setKebabOpen(!kebabOpen)} /> + } + isOpen={kebabOpen} + isPlain + dropdownItems={[ + { + try { + await Promise.all( + selectedRows.map(async (row) => { + await removeScope( + adminClient, + clientId, + { ...row }, + row.type + ); + }) + ); + + setKebabOpen(false); + addAlert( + t("clientScopeRemoveSuccess"), + AlertVariant.success + ); + refresh(); + } catch (error) { + addAlert( + t("clientScopeRemoveError", { error }), + AlertVariant.danger + ); + } + }} + > + {t("common:remove")} + , + ]} + /> + + + } + columns={[ + { + name: "name", + }, + { + name: "type", + displayKey: "clients:assignedType", + cellRenderer: TypeSelector, + }, + { name: "description" }, + ]} + emptyState={ + setAddDialogOpen(true)} + /> + } + /> ); }; diff --git a/src/clients/service-account/ServiceAccount.tsx b/src/clients/service-account/ServiceAccount.tsx index b5a4f435e8..59719a7176 100644 --- a/src/clients/service-account/ServiceAccount.tsx +++ b/src/clients/service-account/ServiceAccount.tsx @@ -1,18 +1,12 @@ -import React, { Fragment, useContext, useState } from "react"; +import React, { useContext, useState } from "react"; import { useTranslation } from "react-i18next"; -import { - Table, - TableBody, - TableHeader, - TableVariant, -} from "@patternfly/react-table"; import { Badge, Button, Checkbox, ToolbarItem } from "@patternfly/react-core"; -import { useAdminClient } from "../../context/auth/AdminClient"; -import { DataLoader } from "../../components/data-loader/DataLoader"; -import { TableToolbar } from "../../components/table-toolbar/TableToolbar"; -import { RealmContext } from "../../context/realm-context/RealmContext"; import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation"; +import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation"; +import { useAdminClient } from "../../context/auth/AdminClient"; +import { RealmContext } from "../../context/realm-context/RealmContext"; +import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable"; import { emptyFormatter } from "../../util"; import "./service-account.css"; @@ -21,6 +15,11 @@ type ServiceAccountProps = { clientId: string; }; +type Row = { + client: ClientRepresentation; + role: CompositeRole; +}; + type CompositeRole = RoleRepresentation & { parent: RoleRepresentation; }; @@ -66,6 +65,7 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => { }; const clientRolesFlat = clientRoles.map((row) => row.roles).flat(); + console.log(clientRolesFlat); const addInherentData = await (async () => Promise.all( @@ -92,35 +92,34 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => { ] as CompositeRole[]) .sort((r1, r2) => r1.name!.localeCompare(r2.name!)) .map((role) => { - const client = findClient(role); return { - cells: [ - - {client && ( - - {client.clientId} - - )} - {role.name} - , - role.parent ? role.parent.name : "", - role.description, - ], - }; + client: findClient(role), + role, + } as Row; }); }; - const filterData = () => {}; + const RoleLink = ({ role, client }: Row) => ( + <> + {client && ( + + {client.clientId} + + )} + {role.name} + + ); return ( - {}} + searchPlaceholderKey="clients:searchByName" + ariaLabelKey="clients:clientScopeList" toolbarItem={ <> @@ -136,34 +135,23 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => { } - > - - {(clientRoles) => ( - <> - {hide ? "" : " "} - {}} - variant={TableVariant.compact} - cells={[ - t("roles:roleName"), - { - title: t("inherentFrom"), - cellFormatters: [emptyFormatter()], - }, - { - title: t("common:description"), - cellFormatters: [emptyFormatter()], - }, - ]} - rows={clientRoles} - aria-label="roleList" - > - - -
- - )} -
-
+ columns={[ + { + name: "role.name", + displayKey: t("name"), + cellRenderer: RoleLink, + }, + { + name: "role.parent.name", + displayKey: t("inherentFrom"), + cellFormatters: [emptyFormatter()], + }, + { + name: "role.description", + displayKey: t("description"), + cellFormatters: [emptyFormatter()], + }, + ]} + /> ); }; diff --git a/src/route-config.ts b/src/route-config.ts index c74a656118..31a958a97c 100644 --- a/src/route-config.ts +++ b/src/route-config.ts @@ -92,13 +92,13 @@ export const routes: RoutesFn = (t: TFunction) => [ access: "manage-clients", }, { - path: "/:realm/client-scopes/:id/mappers/oidc-role-name-mapper", + path: "/:realm/client-scopes/:id/:tab/oidc-role-name-mapper", component: RoleMappingForm, breadcrumb: t("client-scopes:mappingDetails"), access: "view-clients", }, { - path: "/:realm/client-scopes/:id/mappers/:mapperId", + path: "/:realm/client-scopes/:id/:tab/:mapperId", component: MappingDetails, breadcrumb: t("client-scopes:mappingDetails"), access: "view-clients",