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",
"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",

View file

@ -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<Row[]>([]);
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 (
<Modal
@ -60,13 +90,21 @@ export const AddScopeDialog = ({
{t("common:add")}
</DropdownToggle>
}
dropdownItems={clientScopeTypesDropdown(t)}
dropdownItems={clientScopeTypesDropdown(t, action)}
/>,
<Button
id="modal-cancel"
key="cancel"
variant={ButtonVariant.secondary}
onClick={toggleDialog}
onClick={() => {
setRows(
rows.map((row) => {
row.selected = false;
return row;
})
);
toggleDialog();
}}
>
{t("common:cancel")}
</Button>,
@ -75,8 +113,20 @@ export const AddScopeDialog = ({
<Table
variant={TableVariant.compact}
cells={[t("name"), t("description")]}
onSelect={(_, isSelected, rowIndex) => {}}
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")}
>
<TableHeader />

View file

@ -16,7 +16,12 @@ export const clientScopeTypesSelectOptions = (t: TFunction) =>
</SelectOption>
));
export const clientScopeTypesDropdown = (t: TFunction) =>
export const clientScopeTypesDropdown = (
t: TFunction,
onClick: (scope: ClientScopeType) => void
) =>
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 { 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);
const indexedAdminClient = (adminClient.clients as unknown) as {
[index: string]: Function;
await removeScope(adminClient, clientId, clientScope, type);
await addScope(adminClient, clientId, clientScope, changeTo);
};
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<SearchType>("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 (
<CellDropdown
clientId={clientId}
clientScope={row.clientScope}
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,16 +218,34 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
</div>
)}
{rows && rows.length > 0 && (
<>
{rest && (
<AddScopeDialog
clientScopes={rest}
open={addDialogOpen}
toggleDialog={() => 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);
}
}}
/>
)}
{rows && rows.length > 0 && (
<TableToolbar
searchTypeComponent={
<Dropdown
@ -257,9 +299,31 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
selections={[]}
placeholderText={t("changeTypeTo")}
onToggle={() => setAddToggle(!addToggle)}
onSelect={(_, value) => {
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);
await loader();
addAlert(t("clientScopeSuccess"), AlertVariant.success);
} catch (error) {
addAlert(
t("clientScopeError", { error }),
AlertVariant.danger
);
}
}}
>
{clientScopeTypesSelectOptions(t)}
@ -269,18 +333,49 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
}
>
<Table
onSelect={() => {}}
onSelect={(_, isSelected, rowIndex) => {
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("description"), cellFormatters: [dropdown()] },
t("protocol"),
{ title: t("assignedType"), cellFormatters: [dropdown()] },
t("description"),
]}
rows={rows}
actions={[
{
title: t("common:remove"),
onClick: () => {},
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")}
@ -289,14 +384,13 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
<TableBody />
</Table>
</TableToolbar>
</>
)}
{rows && rows.length === 0 && (
<ListEmptyState
message={t("clients:emptyClientScopes")}
instructions={t("clients:emptyClientScopesInstructions")}
primaryActionText={t("clients:emptyClientScopesPrimaryAction")}
onPrimaryAction={() => {}}
onPrimaryAction={() => setAddDialogOpen(true)}
/>
)}
</>