From 56db3cfee5881a4a422b121307f5f2003e9721ab Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Fri, 4 Dec 2020 22:08:11 +0100 Subject: [PATCH] added actions to client scope mapping screen (#242) --- src/clients/messages.json | 5 + src/clients/scopes/AddScopeDialog.tsx | 68 ++++- src/clients/scopes/ClientScopeTypes.tsx | 9 +- src/clients/scopes/ClientScopes.tsx | 352 +++++++++++++++--------- 4 files changed, 294 insertions(+), 140 deletions(-) diff --git a/src/clients/messages.json b/src/clients/messages.json index 4f89fac7fb..26766ef3aa 100644 --- a/src/clients/messages.json +++ b/src/clients/messages.json @@ -16,6 +16,10 @@ "clientScopes": "Client scopes", "addClientScope": "Add client scope", "addClientScopesTo": "Add client scopes to {{clientId}}", + "clientScopeRemoveSuccess": "Scope mapping successfully removed", + "clientScopeRemoveError": "Could not remove the scope mapping {{error}}", + "clientScopeSuccess": "Scope mapping successfully updated", + "clientScopeError": "Could not update the scope mapping {{error}}", "searchByName": "Search by name", "setup": "Setup", "evaluate": "Evaluate", @@ -28,6 +32,7 @@ "client": "Client scope", "assigned": "Assigned type" }, + "assignedType": "Assigned type", "emptyClientScopes": "This client doesn't have any added client scopes", "emptyClientScopesInstructions": "There are currently no client scopes linked to this client. You can add existing client scopes to this client to share protocol mappers and roles.", "emptyClientScopesPrimaryAction": "Add client scopes", diff --git a/src/clients/scopes/AddScopeDialog.tsx b/src/clients/scopes/AddScopeDialog.tsx index 16bde24dba..bb0b2df37f 100644 --- a/src/clients/scopes/AddScopeDialog.tsx +++ b/src/clients/scopes/AddScopeDialog.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Button, @@ -18,25 +18,55 @@ import { } from "@patternfly/react-table"; import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation"; -import { clientScopeTypesDropdown } from "./ClientScopeTypes"; +import { ClientScopeType, clientScopeTypesDropdown } from "./ClientScopeTypes"; export type AddScopeDialogProps = { clientScopes: ClientScopeRepresentation[]; open: boolean; toggleDialog: () => void; + onAdd: ( + scopes: { scope: ClientScopeRepresentation; type: ClientScopeType }[] + ) => void; +}; + +type Row = { + selected: boolean; + scope: ClientScopeRepresentation; + cells: (string | undefined)[]; }; export const AddScopeDialog = ({ clientScopes, open, toggleDialog, + onAdd, }: AddScopeDialogProps) => { const { t } = useTranslation("clients"); const [addToggle, setAddToggle] = useState(false); + const [rows, setRows] = useState([]); - const data = clientScopes.map((scope) => { - return { cells: [scope.name, scope.description] }; - }); + useEffect(() => { + setRows( + clientScopes.map((scope) => { + return { + selected: false, + scope, + cells: [scope.name, scope.description], + }; + }) + ); + }, [clientScopes]); + + const action = (scope: ClientScopeType) => { + const scopes = rows + .filter((row) => row.selected) + .map((row) => { + return { scope: row.scope, type: scope }; + }); + onAdd(scopes); + setAddToggle(false); + toggleDialog(); + }; return ( } - dropdownItems={clientScopeTypesDropdown(t)} + dropdownItems={clientScopeTypesDropdown(t, action)} />, , @@ -75,8 +113,20 @@ export const AddScopeDialog = ({ {}} - rows={data} + onSelect={(_, isSelected, rowIndex) => { + if (rowIndex === -1) { + setRows( + rows.map((row) => { + row.selected = isSelected; + return row; + }) + ); + } else { + rows[rowIndex].selected = isSelected; + setRows([...rows]); + } + }} + rows={rows} aria-label={t("chooseAMapperType")} > diff --git a/src/clients/scopes/ClientScopeTypes.tsx b/src/clients/scopes/ClientScopeTypes.tsx index 9585b35226..cdaefaba93 100644 --- a/src/clients/scopes/ClientScopeTypes.tsx +++ b/src/clients/scopes/ClientScopeTypes.tsx @@ -16,7 +16,12 @@ export const clientScopeTypesSelectOptions = (t: TFunction) => )); -export const clientScopeTypesDropdown = (t: TFunction) => +export const clientScopeTypesDropdown = ( + t: TFunction, + onClick: (scope: ClientScopeType) => void +) => clientScopeTypes.map((type) => ( - {t(`clientScope.${type}`)} + onClick(type as ClientScopeType)}> + {t(`clientScope.${type}`)} + )); diff --git a/src/clients/scopes/ClientScopes.tsx b/src/clients/scopes/ClientScopes.tsx index 725fe969e4..eb32bc251a 100644 --- a/src/clients/scopes/ClientScopes.tsx +++ b/src/clients/scopes/ClientScopes.tsx @@ -1,6 +1,5 @@ import React, { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { TFunction } from "i18next"; import { IFormatter, IFormatterValueType, @@ -10,6 +9,7 @@ import { TableVariant, } from "@patternfly/react-table"; import { + AlertVariant, Button, Dropdown, DropdownItem, @@ -32,6 +32,7 @@ import { ClientScopeType, ClientScope, } from "./ClientScopeTypes"; +import { useAlerts } from "../../components/alert/Alerts"; export type ClientScopesProps = { clientId: string; @@ -41,6 +42,11 @@ export type ClientScopesProps = { const firstUpperCase = (name: string) => name.charAt(0).toUpperCase() + name.slice(1); +const castAdminClient = (adminClient: KeycloakAdminClient) => + (adminClient.clients as unknown) as { + [index: string]: Function; + }; + const changeScope = async ( adminClient: KeycloakAdminClient, clientId: string, @@ -48,30 +54,43 @@ const changeScope = async ( type: ClientScopeType, changeTo: ClientScopeType ) => { - const typeToName = firstUpperCase(type); - const changeToName = firstUpperCase(changeTo); + await removeScope(adminClient, clientId, clientScope, type); + await addScope(adminClient, clientId, clientScope, changeTo); +}; - const indexedAdminClient = (adminClient.clients as unknown) as { - [index: string]: Function; - }; - await indexedAdminClient[`del${typeToName}ClientScope`]({ +const removeScope = async ( + adminClient: KeycloakAdminClient, + clientId: string, + clientScope: ClientScopeRepresentation, + type: ClientScopeType +) => { + const typeToName = firstUpperCase(type); + await castAdminClient(adminClient)[`del${typeToName}ClientScope`]({ id: clientId, clientScopeId: clientScope.id!, }); - await indexedAdminClient[`add${changeToName}ClientScope`]({ +}; + +const addScope = async ( + adminClient: KeycloakAdminClient, + clientId: string, + clientScope: ClientScopeRepresentation, + type: ClientScopeType +) => { + const typeToName = firstUpperCase(type); + await castAdminClient(adminClient)[`add${typeToName}ClientScope`]({ id: clientId, clientScopeId: clientScope.id!, }); }; type CellDropdownProps = { - clientId: string; clientScope: ClientScopeRepresentation; type: ClientScopeType; + onSelect: (value: ClientScopeType) => void; }; -const CellDropdown = ({ clientId, clientScope, type }: CellDropdownProps) => { - const adminClient = useAdminClient(); +const CellDropdown = ({ clientScope, type, onSelect }: CellDropdownProps) => { const { t } = useTranslation("clients"); const [open, setOpen] = useState(false); @@ -82,13 +101,7 @@ const CellDropdown = ({ clientId, clientScope, type }: CellDropdownProps) => { isOpen={open} selections={[type]} onSelect={(_, value) => { - changeScope( - adminClient, - clientId, - clientScope, - type, - value as ClientScopeType - ); + onSelect(value as ClientScopeType); setOpen(false); }} > @@ -100,6 +113,7 @@ const CellDropdown = ({ clientId, clientScope, type }: CellDropdownProps) => { type SearchType = "client" | "assigned"; type TableRow = { + selected: boolean; clientScope: ClientScopeRepresentation; type: ClientScopeType; cells: (string | undefined)[]; @@ -108,6 +122,8 @@ type TableRow = { export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => { const { t } = useTranslation("clients"); const adminClient = useAdminClient(); + const { addAlert } = useAlerts(); + const [searchToggle, setSearchToggle] = useState(false); const [searchType, setSearchType] = useState("client"); const [addToggle, setAddToggle] = useState(false); @@ -131,6 +147,7 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => { 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], @@ -140,29 +157,18 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => { 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], }; }); - setRows([...optional, ...defaultScopes]); - }; - - useEffect(() => { - loader(); - }, []); - - useEffect(() => { - if (rows) { - loadRest(rows); - } - }, [rows]); - - const loadRest = async (rows: { cells: (string | undefined)[] }[]) => { - const clientScopes = await adminClient.clientScopes.find(); - const names = rows.map((row) => row.cells[0]); + const data = [...optional, ...defaultScopes]; + setRows(data); + const names = data.map((row) => row.cells[0]); + console.log("set rest"); setRest( clientScopes .filter((scope) => !names.includes(scope.name)) @@ -170,6 +176,10 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => { ); }; + useEffect(() => { + loader(); + }, []); + const dropdown = (): IFormatter => (data?: IFormatterValueType) => { if (!data) { return <>; @@ -177,9 +187,23 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => { const row = rows?.find((row) => row.clientScope.id === data.toString())!; return ( { + try { + await changeScope( + adminClient, + clientId, + row.clientScope, + row.type, + value + ); + addAlert(t("clientScopeSuccess"), AlertVariant.success); + await loader(); + } catch (error) { + addAlert(t("clientScopeError", { error }), AlertVariant.danger); + } + }} /> ); }; @@ -194,109 +218,179 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => { )} - {rows && rows.length > 0 && ( - <> - {rest && ( - setAddDialogOpen(!addDialogOpen)} - /> - )} + {rest && ( + setAddDialogOpen(!addDialogOpen)} + onAdd={async (scopes) => { + try { + await Promise.all( + scopes.map( + async (scope) => + await addScope( + adminClient, + clientId, + scope.scope, + scope.type + ) + ) + ); + addAlert(t("clientScopeSuccess"), AlertVariant.success); + loader(); + } catch (error) { + addAlert(t("clientScopeError", { error }), AlertVariant.danger); + } + }} + /> + )} - 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={ - - - - - - setAddToggle(!addToggle)} + onSelect={async (_, value) => { + try { + await Promise.all( + rows.map((row) => { + if (row.selected) { + return changeScope( + adminClient, + clientId, + row.clientScope, + row.type, + value as ClientScope + ); + } + return Promise.resolve(); + }) + ); setAddToggle(false); - }} - > - {clientScopeTypesSelectOptions(t)} - - - - } - > -
{}} - variant={TableVariant.compact} - cells={[ - t("name"), - { title: t("description"), cellFormatters: [dropdown()] }, - t("protocol"), - ]} - rows={rows} - actions={[ - { - title: t("common:remove"), - onClick: () => {}, + await loader(); + addAlert(t("clientScopeSuccess"), AlertVariant.success); + } catch (error) { + addAlert( + t("clientScopeError", { error }), + AlertVariant.danger + ); + } + }} + > + {clientScopeTypesSelectOptions(t)} + + + + } + > +
{ + if (rowIndex === -1) { + setRows( + rows.map((row) => { + row.selected = isSelected; + return row; + }) + ); + } else { + rows[rowIndex].selected = isSelected; + setRows([...rows]); + } + }} + variant={TableVariant.compact} + cells={[ + t("name"), + { title: t("assignedType"), cellFormatters: [dropdown()] }, + t("description"), + ]} + rows={rows} + actions={[ + { + title: t("common:remove"), + onClick: async (_, rowId) => { + try { + await removeScope( + adminClient, + clientId, + rows[rowId].clientScope, + rows[rowId].type + ); + addAlert( + t("clientScopeRemoveSuccess"), + AlertVariant.success + ); + loader(); + } catch (error) { + addAlert( + t("clientScopeRemoveError", { error }), + AlertVariant.danger + ); + } }, - ]} - aria-label={t("clientScopeList")} - > - - -
- - + }, + ]} + aria-label={t("clientScopeList")} + > + + + + )} {rows && rows.length === 0 && ( {}} + onPrimaryAction={() => setAddDialogOpen(true)} /> )}