added actions to client scope mapping screen (#242)

This commit is contained in:
Erik Jan de Wit 2020-12-04 22:08:11 +01:00 committed by GitHub
parent acbd5a5f18
commit 56db3cfee5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 294 additions and 140 deletions

View file

@ -16,6 +16,10 @@
"clientScopes": "Client scopes", "clientScopes": "Client scopes",
"addClientScope": "Add client scope", "addClientScope": "Add client scope",
"addClientScopesTo": "Add client scopes to {{clientId}}", "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", "searchByName": "Search by name",
"setup": "Setup", "setup": "Setup",
"evaluate": "Evaluate", "evaluate": "Evaluate",
@ -28,6 +32,7 @@
"client": "Client scope", "client": "Client scope",
"assigned": "Assigned type" "assigned": "Assigned type"
}, },
"assignedType": "Assigned type",
"emptyClientScopes": "This client doesn't have any added client scopes", "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.", "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", "emptyClientScopesPrimaryAction": "Add client scopes",

View file

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
Button, Button,
@ -18,25 +18,55 @@ import {
} from "@patternfly/react-table"; } from "@patternfly/react-table";
import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation"; import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
import { clientScopeTypesDropdown } from "./ClientScopeTypes"; import { ClientScopeType, clientScopeTypesDropdown } from "./ClientScopeTypes";
export type AddScopeDialogProps = { export type AddScopeDialogProps = {
clientScopes: ClientScopeRepresentation[]; clientScopes: ClientScopeRepresentation[];
open: boolean; open: boolean;
toggleDialog: () => void; toggleDialog: () => void;
onAdd: (
scopes: { scope: ClientScopeRepresentation; type: ClientScopeType }[]
) => void;
};
type Row = {
selected: boolean;
scope: ClientScopeRepresentation;
cells: (string | undefined)[];
}; };
export const AddScopeDialog = ({ export const AddScopeDialog = ({
clientScopes, clientScopes,
open, open,
toggleDialog, toggleDialog,
onAdd,
}: AddScopeDialogProps) => { }: AddScopeDialogProps) => {
const { t } = useTranslation("clients"); const { t } = useTranslation("clients");
const [addToggle, setAddToggle] = useState(false); const [addToggle, setAddToggle] = useState(false);
const [rows, setRows] = useState<Row[]>([]);
const data = clientScopes.map((scope) => { useEffect(() => {
return { cells: [scope.name, scope.description] }; 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 ( return (
<Modal <Modal
@ -60,13 +90,21 @@ export const AddScopeDialog = ({
{t("common:add")} {t("common:add")}
</DropdownToggle> </DropdownToggle>
} }
dropdownItems={clientScopeTypesDropdown(t)} dropdownItems={clientScopeTypesDropdown(t, action)}
/>, />,
<Button <Button
id="modal-cancel" id="modal-cancel"
key="cancel" key="cancel"
variant={ButtonVariant.secondary} variant={ButtonVariant.secondary}
onClick={toggleDialog} onClick={() => {
setRows(
rows.map((row) => {
row.selected = false;
return row;
})
);
toggleDialog();
}}
> >
{t("common:cancel")} {t("common:cancel")}
</Button>, </Button>,
@ -75,8 +113,20 @@ export const AddScopeDialog = ({
<Table <Table
variant={TableVariant.compact} variant={TableVariant.compact}
cells={[t("name"), t("description")]} cells={[t("name"), t("description")]}
onSelect={(_, isSelected, rowIndex) => {}} onSelect={(_, isSelected, rowIndex) => {
rows={data} 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")} aria-label={t("chooseAMapperType")}
> >
<TableHeader /> <TableHeader />

View file

@ -16,7 +16,12 @@ export const clientScopeTypesSelectOptions = (t: TFunction) =>
</SelectOption> </SelectOption>
)); ));
export const clientScopeTypesDropdown = (t: TFunction) => export const clientScopeTypesDropdown = (
t: TFunction,
onClick: (scope: ClientScopeType) => void
) =>
clientScopeTypes.map((type) => ( clientScopeTypes.map((type) => (
<DropdownItem key={type}>{t(`clientScope.${type}`)}</DropdownItem> <DropdownItem key={type} onClick={() => onClick(type as ClientScopeType)}>
{t(`clientScope.${type}`)}
</DropdownItem>
)); ));

View file

@ -1,6 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TFunction } from "i18next";
import { import {
IFormatter, IFormatter,
IFormatterValueType, IFormatterValueType,
@ -10,6 +9,7 @@ import {
TableVariant, TableVariant,
} from "@patternfly/react-table"; } from "@patternfly/react-table";
import { import {
AlertVariant,
Button, Button,
Dropdown, Dropdown,
DropdownItem, DropdownItem,
@ -32,6 +32,7 @@ import {
ClientScopeType, ClientScopeType,
ClientScope, ClientScope,
} from "./ClientScopeTypes"; } from "./ClientScopeTypes";
import { useAlerts } from "../../components/alert/Alerts";
export type ClientScopesProps = { export type ClientScopesProps = {
clientId: string; clientId: string;
@ -41,6 +42,11 @@ export type ClientScopesProps = {
const firstUpperCase = (name: string) => const firstUpperCase = (name: string) =>
name.charAt(0).toUpperCase() + name.slice(1); name.charAt(0).toUpperCase() + name.slice(1);
const castAdminClient = (adminClient: KeycloakAdminClient) =>
(adminClient.clients as unknown) as {
[index: string]: Function;
};
const changeScope = async ( const changeScope = async (
adminClient: KeycloakAdminClient, adminClient: KeycloakAdminClient,
clientId: string, clientId: string,
@ -48,30 +54,43 @@ const changeScope = async (
type: ClientScopeType, type: ClientScopeType,
changeTo: ClientScopeType changeTo: ClientScopeType
) => { ) => {
const typeToName = firstUpperCase(type); await removeScope(adminClient, clientId, clientScope, type);
const changeToName = firstUpperCase(changeTo); await addScope(adminClient, clientId, clientScope, changeTo);
};
const indexedAdminClient = (adminClient.clients as unknown) as { const removeScope = async (
[index: string]: Function; adminClient: KeycloakAdminClient,
}; clientId: string,
await indexedAdminClient[`del${typeToName}ClientScope`]({ clientScope: ClientScopeRepresentation,
type: ClientScopeType
) => {
const typeToName = firstUpperCase(type);
await castAdminClient(adminClient)[`del${typeToName}ClientScope`]({
id: clientId, id: clientId,
clientScopeId: clientScope.id!, 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, id: clientId,
clientScopeId: clientScope.id!, clientScopeId: clientScope.id!,
}); });
}; };
type CellDropdownProps = { type CellDropdownProps = {
clientId: string;
clientScope: ClientScopeRepresentation; clientScope: ClientScopeRepresentation;
type: ClientScopeType; type: ClientScopeType;
onSelect: (value: ClientScopeType) => void;
}; };
const CellDropdown = ({ clientId, clientScope, type }: CellDropdownProps) => { const CellDropdown = ({ clientScope, type, onSelect }: CellDropdownProps) => {
const adminClient = useAdminClient();
const { t } = useTranslation("clients"); const { t } = useTranslation("clients");
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -82,13 +101,7 @@ const CellDropdown = ({ clientId, clientScope, type }: CellDropdownProps) => {
isOpen={open} isOpen={open}
selections={[type]} selections={[type]}
onSelect={(_, value) => { onSelect={(_, value) => {
changeScope( onSelect(value as ClientScopeType);
adminClient,
clientId,
clientScope,
type,
value as ClientScopeType
);
setOpen(false); setOpen(false);
}} }}
> >
@ -100,6 +113,7 @@ const CellDropdown = ({ clientId, clientScope, type }: CellDropdownProps) => {
type SearchType = "client" | "assigned"; type SearchType = "client" | "assigned";
type TableRow = { type TableRow = {
selected: boolean;
clientScope: ClientScopeRepresentation; clientScope: ClientScopeRepresentation;
type: ClientScopeType; type: ClientScopeType;
cells: (string | undefined)[]; cells: (string | undefined)[];
@ -108,6 +122,8 @@ type TableRow = {
export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => { export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
const { t } = useTranslation("clients"); const { t } = useTranslation("clients");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const [searchToggle, setSearchToggle] = useState(false); const [searchToggle, setSearchToggle] = useState(false);
const [searchType, setSearchType] = useState<SearchType>("client"); const [searchType, setSearchType] = useState<SearchType>("client");
const [addToggle, setAddToggle] = useState(false); const [addToggle, setAddToggle] = useState(false);
@ -131,6 +147,7 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
const optional = optionalClientScopes.map((c) => { const optional = optionalClientScopes.map((c) => {
const scope = find(c.id!); const scope = find(c.id!);
return { return {
selected: false,
clientScope: c, clientScope: c,
type: ClientScope.optional, type: ClientScope.optional,
cells: [c.name, c.id, scope.description], cells: [c.name, c.id, scope.description],
@ -140,29 +157,18 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
const defaultScopes = defaultClientScopes.map((c) => { const defaultScopes = defaultClientScopes.map((c) => {
const scope = find(c.id!); const scope = find(c.id!);
return { return {
selected: false,
clientScope: c, clientScope: c,
type: ClientScope.default, type: ClientScope.default,
cells: [c.name, c.id, scope.description], cells: [c.name, c.id, scope.description],
}; };
}); });
setRows([...optional, ...defaultScopes]); const data = [...optional, ...defaultScopes];
}; setRows(data);
const names = data.map((row) => row.cells[0]);
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]);
console.log("set rest");
setRest( setRest(
clientScopes clientScopes
.filter((scope) => !names.includes(scope.name)) .filter((scope) => !names.includes(scope.name))
@ -170,6 +176,10 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
); );
}; };
useEffect(() => {
loader();
}, []);
const dropdown = (): IFormatter => (data?: IFormatterValueType) => { const dropdown = (): IFormatter => (data?: IFormatterValueType) => {
if (!data) { if (!data) {
return <></>; return <></>;
@ -177,9 +187,23 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
const row = rows?.find((row) => row.clientScope.id === data.toString())!; const row = rows?.find((row) => row.clientScope.id === data.toString())!;
return ( return (
<CellDropdown <CellDropdown
clientId={clientId}
clientScope={row.clientScope} clientScope={row.clientScope}
type={row.type} type={row.type}
onSelect={async (value) => {
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) => {
</div> </div>
)} )}
{rows && rows.length > 0 && ( {rest && (
<> <AddScopeDialog
{rest && ( clientScopes={rest}
<AddScopeDialog open={addDialogOpen}
clientScopes={rest} toggleDialog={() => setAddDialogOpen(!addDialogOpen)}
open={addDialogOpen} onAdd={async (scopes) => {
toggleDialog={() => setAddDialogOpen(!addDialogOpen)} 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);
}
}}
/>
)}
<TableToolbar {rows && rows.length > 0 && (
searchTypeComponent={ <TableToolbar
<Dropdown searchTypeComponent={
toggle={ <Dropdown
<DropdownToggle toggle={
id="toggle-id" <DropdownToggle
onToggle={() => setSearchToggle(!searchToggle)} id="toggle-id"
> onToggle={() => setSearchToggle(!searchToggle)}
<FilterIcon /> {t(`clientScopeSearch.${searchType}`)} >
</DropdownToggle> <FilterIcon /> {t(`clientScopeSearch.${searchType}`)}
} </DropdownToggle>
aria-label="Select Input" }
isOpen={searchToggle} aria-label="Select Input"
dropdownItems={[ isOpen={searchToggle}
<DropdownItem dropdownItems={[
key="client" <DropdownItem
onClick={() => { key="client"
setSearchType("client"); onClick={() => {
setSearchToggle(false); setSearchType("client");
}} setSearchToggle(false);
> }}
{t("clientScopeSearch.client")} >
</DropdownItem>, {t("clientScopeSearch.client")}
<DropdownItem </DropdownItem>,
key="assigned" <DropdownItem
onClick={() => { key="assigned"
setSearchType("assigned"); onClick={() => {
setSearchToggle(false); setSearchType("assigned");
}} setSearchToggle(false);
> }}
{t("clientScopeSearch.assigned")} >
</DropdownItem>, {t("clientScopeSearch.assigned")}
]} </DropdownItem>,
/> ]}
} />
inputGroupName="clientsScopeToolbarTextInput" }
inputGroupPlaceholder={t("searchByName")} inputGroupName="clientsScopeToolbarTextInput"
inputGroupOnChange={filterData} inputGroupPlaceholder={t("searchByName")}
toolbarItem={ inputGroupOnChange={filterData}
<Split hasGutter> toolbarItem={
<SplitItem> <Split hasGutter>
<Button onClick={() => setAddDialogOpen(true)}> <SplitItem>
{t("addClientScope")} <Button onClick={() => setAddDialogOpen(true)}>
</Button> {t("addClientScope")}
</SplitItem> </Button>
<SplitItem> </SplitItem>
<Select <SplitItem>
id="add-dropdown" <Select
key="add-dropdown" id="add-dropdown"
isOpen={addToggle} key="add-dropdown"
selections={[]} isOpen={addToggle}
placeholderText={t("changeTypeTo")} selections={[]}
onToggle={() => setAddToggle(!addToggle)} placeholderText={t("changeTypeTo")}
onSelect={(_, value) => { onToggle={() => setAddToggle(!addToggle)}
console.log(value); 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); setAddToggle(false);
}} await loader();
> addAlert(t("clientScopeSuccess"), AlertVariant.success);
{clientScopeTypesSelectOptions(t)} } catch (error) {
</Select> addAlert(
</SplitItem> t("clientScopeError", { error }),
</Split> AlertVariant.danger
} );
> }
<Table }}
onSelect={() => {}} >
variant={TableVariant.compact} {clientScopeTypesSelectOptions(t)}
cells={[ </Select>
t("name"), </SplitItem>
{ title: t("description"), cellFormatters: [dropdown()] }, </Split>
t("protocol"), }
]} >
rows={rows} <Table
actions={[ onSelect={(_, isSelected, rowIndex) => {
{ if (rowIndex === -1) {
title: t("common:remove"), setRows(
onClick: () => {}, 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")}
<TableHeader /> >
<TableBody /> <TableHeader />
</Table> <TableBody />
</TableToolbar> </Table>
</> </TableToolbar>
)} )}
{rows && rows.length === 0 && ( {rows && rows.length === 0 && (
<ListEmptyState <ListEmptyState
message={t("clients:emptyClientScopes")} message={t("clients:emptyClientScopes")}
instructions={t("clients:emptyClientScopesInstructions")} instructions={t("clients:emptyClientScopesInstructions")}
primaryActionText={t("clients:emptyClientScopesPrimaryAction")} primaryActionText={t("clients:emptyClientScopesPrimaryAction")}
onPrimaryAction={() => {}} onPrimaryAction={() => setAddDialogOpen(true)}
/> />
)} )}
</> </>