added actions to client scope mapping screen (#242)
This commit is contained in:
parent
acbd5a5f18
commit
56db3cfee5
4 changed files with 294 additions and 140 deletions
|
@ -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",
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
));
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
Loading…
Reference in a new issue