hooked up the build in mapper dialog (#197)

* hooked up the build in mapper dialog

* spelling

* useEffect for setTimout

* fix state

* fix bug with alerts clearing checked items

* fixed tests

* Update src/client-scopes/messages.json

Co-authored-by: Stan Silvert <ssilvert@redhat.com>

* simplified dialog usage

Co-authored-by: Stan Silvert <ssilvert@redhat.com>
This commit is contained in:
Erik Jan de Wit 2020-11-05 22:26:43 +01:00 committed by GitHub
parent 7ebe695921
commit d6e1161c83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 13814 additions and 13741 deletions

View file

@ -1,4 +1,4 @@
import React, { ReactElement, useState } from "react"; import React, { useState } from "react";
import { import {
Button, Button,
ButtonVariant, ButtonVariant,
@ -8,6 +8,7 @@ import {
DataListItemCells, DataListItemCells,
DataListItemRow, DataListItemRow,
Modal, Modal,
ModalVariant,
Text, Text,
TextContent, TextContent,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
@ -24,75 +25,67 @@ import {
ProtocolMapperRepresentation, ProtocolMapperRepresentation,
ProtocolMapperTypeRepresentation, ProtocolMapperTypeRepresentation,
} from "../../context/server-info/server-info"; } from "../../context/server-info/server-info";
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
export type AddMapperDialogProps = { export type AddMapperDialogModalProps = {
protocol: string; protocol: string;
buildIn: boolean; filter?: ProtocolMapperRepresentation[];
onConfirm: ( onConfirm: (
value: ProtocolMapperTypeRepresentation | ProtocolMapperRepresentation[] value: ProtocolMapperTypeRepresentation | ProtocolMapperRepresentation[]
) => {}; ) => void;
}; };
type AddMapperDialogModalProps = AddMapperDialogProps & { export type AddMapperDialogProps = AddMapperDialogModalProps & {
open: boolean; open: boolean;
toggleDialog: () => void; toggleDialog: () => void;
}; };
export const useAddMapperDialog = ( export const AddMapperDialog = (props: AddMapperDialogProps) => {
props: AddMapperDialogProps const { t } = useTranslation("client-scopes");
): [() => void, () => ReactElement] => {
const [show, setShow] = useState(false);
function toggleDialog() { const serverInfo = useServerInfo();
setShow((show) => !show); const protocol = props.protocol;
const protocolMappers = serverInfo.protocolMapperTypes[protocol];
const builtInMappers = serverInfo.builtinProtocolMappers[protocol];
const [filter, setFilter] = useState<ProtocolMapperRepresentation[]>([]);
const allRows = builtInMappers.map((mapper) => {
const mapperType = protocolMappers.filter(
(type) => type.id === mapper.protocolMapper
)[0];
return {
item: mapper,
selected: false,
cells: [mapper.name, mapperType.helpText],
};
});
const [rows, setRows] = useState(allRows);
if (props.filter && props.filter.length !== filter.length) {
setFilter(props.filter);
const nameFilter = props.filter.map((f) => f.name);
setRows([...allRows.filter((row) => !nameFilter.includes(row.item.name))]);
} }
const Dialog = () => ( const isBuiltIn = !!props.filter;
<AddMapperDialog {...props} open={show} toggleDialog={toggleDialog} />
);
return [toggleDialog, Dialog];
};
export const AddMapperDialog = ({
protocol,
buildIn,
open,
toggleDialog,
onConfirm,
}: AddMapperDialogModalProps) => {
const serverInfo = useServerInfo();
const protocolMappers = serverInfo.protocolMapperTypes[protocol];
const buildInMappers = serverInfo.builtinProtocolMappers[protocol];
const { t } = useTranslation("client-scopes");
const [rows, setRows] = useState(
buildInMappers.map((mapper) => {
const mapperType = protocolMappers.filter(
(type) => type.id === mapper.protocolMapper
)[0];
return {
item: mapper,
selected: false,
cells: [mapper.name, mapperType.helpText],
};
})
);
return ( return (
<Modal <Modal
variant={ModalVariant.medium}
title={t("chooseAMapperType")} title={t("chooseAMapperType")}
isOpen={open} isOpen={props.open}
onClose={toggleDialog}
actions={ actions={
buildIn isBuiltIn
? [ ? [
<Button <Button
id="modal-confirm" id="modal-confirm"
key="confirm" key="confirm"
isDisabled={rows.length === 0}
onClick={() => { onClick={() => {
onConfirm( props.onConfirm(
rows.filter((row) => row.selected).map((row) => row.item) rows.filter((row) => row.selected).map((row) => row.item)
); );
toggleDialog(); props.toggleDialog();
}} }}
> >
{t("common:add")} {t("common:add")}
@ -102,7 +95,7 @@ export const AddMapperDialog = ({
key="cancel" key="cancel"
variant={ButtonVariant.secondary} variant={ButtonVariant.secondary}
onClick={() => { onClick={() => {
toggleDialog(); props.toggleDialog();
}} }}
> >
{t("common:cancel")} {t("common:cancel")}
@ -114,12 +107,12 @@ export const AddMapperDialog = ({
<TextContent> <TextContent>
<Text>{t("predefinedMappingDescription")}</Text> <Text>{t("predefinedMappingDescription")}</Text>
</TextContent> </TextContent>
{!buildIn && ( {!isBuiltIn && (
<DataList <DataList
onSelectDataListItem={(id) => { onSelectDataListItem={(id) => {
const mapper = protocolMappers.find((mapper) => mapper.id === id); const mapper = protocolMappers.find((mapper) => mapper.id === id);
onConfirm(mapper!); props.onConfirm(mapper!);
toggleDialog(); props.toggleDialog();
}} }}
aria-label={t("chooseAMapperType")} aria-label={t("chooseAMapperType")}
isCompact isCompact
@ -146,7 +139,7 @@ export const AddMapperDialog = ({
))} ))}
</DataList> </DataList>
)} )}
{buildIn && ( {isBuiltIn && rows.length > 0 && (
<Table <Table
variant={TableVariant.compact} variant={TableVariant.compact}
cells={[t("name"), t("description")]} cells={[t("name"), t("description")]}
@ -162,6 +155,12 @@ export const AddMapperDialog = ({
<TableBody /> <TableBody />
</Table> </Table>
)} )}
{isBuiltIn && rows.length === 0 && (
<ListEmptyState
message={t("emptyMappers")}
instructions={t("emptyBuiltInMappersInstructions")}
/>
)}
</Modal> </Modal>
); );
}; };

View file

@ -1,18 +1,22 @@
import React from "react"; import React, { useState } from "react";
import { mount } from "enzyme"; import { mount } from "enzyme";
import { Button } from "@patternfly/react-core"; import { Button } from "@patternfly/react-core";
import serverInfo from "../../../context/server-info/__tests__/mock.json"; import serverInfo from "../../../context/server-info/__tests__/mock.json";
import { ServerInfoContext } from "../../../context/server-info/ServerInfoProvider"; import { ServerInfoContext } from "../../../context/server-info/ServerInfoProvider";
import { AddMapperDialogProps, useAddMapperDialog } from "../MapperDialog"; import { AddMapperDialogModalProps, AddMapperDialog } from "../MapperDialog";
describe("<MapperDialog/>", () => { describe("<MapperDialog/>", () => {
const Test = (args: AddMapperDialogProps) => { const Test = (args: AddMapperDialogModalProps) => {
const [toggle, Dialog] = useAddMapperDialog(args); const [open, setOpen] = useState(false);
return ( return (
<ServerInfoContext.Provider value={serverInfo}> <ServerInfoContext.Provider value={serverInfo}>
<Dialog /> <AddMapperDialog
<Button id="open" onClick={toggle}> {...args}
open={open}
toggleDialog={() => setOpen(!open)}
/>
<Button id="open" onClick={() => setOpen(true)}>
Show Show
</Button> </Button>
</ServerInfoContext.Provider> </ServerInfoContext.Provider>
@ -22,7 +26,7 @@ describe("<MapperDialog/>", () => {
it("should return empty array when selecting nothing", () => { it("should return empty array when selecting nothing", () => {
const onConfirm = jest.fn(); const onConfirm = jest.fn();
const container = mount( const container = mount(
<Test buildIn={true} protocol="openid-connect" onConfirm={onConfirm} /> <Test filter={[]} protocol="openid-connect" onConfirm={onConfirm} />
); );
container.find("button#open").simulate("click"); container.find("button#open").simulate("click");
@ -36,7 +40,7 @@ describe("<MapperDialog/>", () => {
const onConfirm = jest.fn(); const onConfirm = jest.fn();
const protocol = "openid-connect"; const protocol = "openid-connect";
const container = mount( const container = mount(
<Test buildIn={true} protocol={protocol} onConfirm={onConfirm} /> <Test filter={[]} protocol={protocol} onConfirm={onConfirm} />
); );
container.find("button#open").simulate("click"); container.find("button#open").simulate("click");
@ -57,9 +61,7 @@ describe("<MapperDialog/>", () => {
it("should return selected protocol mapping type on click", () => { it("should return selected protocol mapping type on click", () => {
const onConfirm = jest.fn(); const onConfirm = jest.fn();
const protocol = "openid-connect"; const protocol = "openid-connect";
const container = mount( const container = mount(<Test protocol={protocol} onConfirm={onConfirm} />);
<Test buildIn={false} protocol={protocol} onConfirm={onConfirm} />
);
container.find("button#open").simulate("click"); container.find("button#open").simulate("click");
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();

View file

@ -1,5 +1,6 @@
import React, { useContext, useState } from "react"; import React, { useContext, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { import {
AlertVariant, AlertVariant,
ButtonVariant, ButtonVariant,
@ -16,6 +17,11 @@ import {
import { CaretDownIcon } from "@patternfly/react-icons"; import { CaretDownIcon } from "@patternfly/react-icons";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider"; import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import {
ProtocolMapperRepresentation as ServerInfoProtocolMapper,
ProtocolMapperTypeRepresentation,
} from "../../context/server-info/server-info";
import { import {
ClientScopeRepresentation, ClientScopeRepresentation,
ProtocolMapperRepresentation, ProtocolMapperRepresentation,
@ -25,10 +31,11 @@ import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState
import { HttpClientContext } from "../../context/http-service/HttpClientContext"; import { HttpClientContext } from "../../context/http-service/HttpClientContext";
import { RealmContext } from "../../context/realm-context/RealmContext"; import { RealmContext } from "../../context/realm-context/RealmContext";
import { useAlerts } from "../../components/alert/Alerts"; import { useAlerts } from "../../components/alert/Alerts";
import { Link } from "react-router-dom"; import { AddMapperDialog } from "../add/MapperDialog";
type MapperListProps = { type MapperListProps = {
clientScope: ClientScopeRepresentation; clientScope: ClientScopeRepresentation;
refresh: () => void;
}; };
type Row = { type Row = {
@ -38,7 +45,7 @@ type Row = {
priority: number; priority: number;
}; };
export const MapperList = ({ clientScope }: MapperListProps) => { export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
const { t } = useTranslation("client-scopes"); const { t } = useTranslation("client-scopes");
const httpClient = useContext(HttpClientContext)!; const httpClient = useContext(HttpClientContext)!;
const { realm } = useContext(RealmContext); const { realm } = useContext(RealmContext);
@ -53,21 +60,48 @@ export const MapperList = ({ clientScope }: MapperListProps) => {
clientScope.protocol! clientScope.protocol!
]; ];
const [builtInDialogOpen, setBuiltInDialogOpen] = useState(false);
const toggleBuiltInMapperDialog = () =>
setBuiltInDialogOpen(!builtInDialogOpen);
const addMappers = async (
mappers: ProtocolMapperTypeRepresentation | ProtocolMapperRepresentation[]
) => {
try {
await httpClient.doPost(
`/admin/realms/${realm}/client-scopes/${clientScope.id}/protocol-mappers/add-models`,
mappers
);
refresh();
addAlert(t("mappingCreatedSuccess"), AlertVariant.success);
} catch (error) {
addAlert(t("mappingCreatedError", { error }), AlertVariant.danger);
}
};
if (!mapperList) { if (!mapperList) {
return ( return (
<ListEmptyState <>
message={t("emptyMappers")} <AddMapperDialog
instructions={t("emptyMappersInstructions")} protocol={clientScope.protocol!}
primaryActionText={t("emptyPrimaryAction")} filter={(mapperList as ServerInfoProtocolMapper[]) || []}
onPrimaryAction={() => {}} onConfirm={addMappers}
secondaryActions={[ open={builtInDialogOpen}
{ toggleDialog={toggleBuiltInMapperDialog}
text: t("emptySecondaryAction"), />
onClick: () => {}, <ListEmptyState
type: ButtonVariant.secondary, message={t("emptyMappers")}
}, instructions={t("emptyMappersInstructions")}
]} primaryActionText={t("emptyPrimaryAction")}
/> onPrimaryAction={toggleBuiltInMapperDialog}
secondaryActions={[
{
text: t("emptySecondaryAction"),
onClick: () => {},
type: ButtonVariant.secondary,
},
]}
/>
</>
); );
} }
@ -122,7 +156,7 @@ export const MapperList = ({ clientScope }: MapperListProps) => {
} }
isOpen={mapperAction} isOpen={mapperAction}
dropdownItems={[ dropdownItems={[
<DropdownItem key="predefined"> <DropdownItem key="predefined" onClick={toggleBuiltInMapperDialog}>
{t("fromPredefinedMapper")} {t("fromPredefinedMapper")}
</DropdownItem>, </DropdownItem>,
<DropdownItem key="byConfiguration"> <DropdownItem key="byConfiguration">
@ -132,6 +166,13 @@ export const MapperList = ({ clientScope }: MapperListProps) => {
/> />
} }
> >
<AddMapperDialog
protocol={clientScope.protocol!}
filter={(mapperList as ServerInfoProtocolMapper[]) || []}
onConfirm={addMappers}
open={builtInDialogOpen}
toggleDialog={toggleBuiltInMapperDialog}
/>
<Table <Table
variant={TableVariant.compact} variant={TableVariant.compact}
cells={[t("name"), t("category"), t("type"), t("priority")]} cells={[t("name"), t("category"), t("type"), t("priority")]}
@ -147,6 +188,7 @@ export const MapperList = ({ clientScope }: MapperListProps) => {
await httpClient.doDelete( await httpClient.doDelete(
`/admin/realms/${realm}/client-scopes/${clientScope.id}/protocol-mappers/models/${data[rowId].mapper.id}` `/admin/realms/${realm}/client-scopes/${clientScope.id}/protocol-mappers/models/${data[rowId].mapper.id}`
); );
refresh();
addAlert(t("mappingDeletedSuccess"), AlertVariant.success); addAlert(t("mappingDeletedSuccess"), AlertVariant.success);
} catch (error) { } catch (error) {
addAlert( addAlert(

View file

@ -46,24 +46,26 @@ export const ClientScopeForm = () => {
const [open, isOpen] = useState(false); const [open, isOpen] = useState(false);
const { addAlert } = useAlerts(); const { addAlert } = useAlerts();
useEffect(() => { const load = async () => {
(async () => { if (id) {
if (id) { const response = await httpClient.doGet<ClientScopeRepresentation>(
const response = await httpClient.doGet<ClientScopeRepresentation>( `/admin/realms/${realm}/client-scopes/${id}`
`/admin/realms/${realm}/client-scopes/${id}` );
); if (response.data) {
if (response.data) { Object.entries(response.data).map((entry) => {
Object.entries(response.data).map((entry) => { if (entry[0] === "attributes") {
if (entry[0] === "attributes") { convertToFormValues(entry[1], "attributes", setValue);
convertToFormValues(entry[1], "attributes", setValue); }
} setValue(entry[0], entry[1]);
setValue(entry[0], entry[1]); });
});
}
setClientScope(response.data);
} }
})();
setClientScope(response.data);
}
};
useEffect(() => {
load();
}, []); }, []);
const save = async (clientScopes: ClientScopeRepresentation) => { const save = async (clientScopes: ClientScopeRepresentation) => {
@ -297,7 +299,9 @@ export const ClientScopeForm = () => {
</Form> </Form>
</Tab> </Tab>
<Tab eventKey={1} title={<TabTitleText>{t("mappers")}</TabTitleText>}> <Tab eventKey={1} title={<TabTitleText>{t("mappers")}</TabTitleText>}>
{clientScope && <MapperList clientScope={clientScope} />} {clientScope && (
<MapperList clientScope={clientScope} refresh={load} />
)}
</Tab> </Tab>
</Tabs> </Tabs>
</PageSection> </PageSection>

View file

@ -13,6 +13,8 @@
"protocol": "Protocol", "protocol": "Protocol",
"includeInTokenScope": "Include in token scope", "includeInTokenScope": "Include in token scope",
"mappingDetails": "Mapper details", "mappingDetails": "Mapper details",
"mappingCreatedSuccess": "Mapping successfully created",
"mappingCreatedError": "Could not create mapping: '{{error}}'",
"deleteMappingTitle": "Delete mapping?", "deleteMappingTitle": "Delete mapping?",
"deleteMappingConfirm": "Are you sure you want to delete this mapping?", "deleteMappingConfirm": "Are you sure you want to delete this mapping?",
"mappingUpdatedSuccess": "Mapping successfully updated", "mappingUpdatedSuccess": "Mapping successfully updated",
@ -49,6 +51,7 @@
"byConfiguration": "By configuration", "byConfiguration": "By configuration",
"emptyMappers": "No mappers", "emptyMappers": "No mappers",
"emptyMappersInstructions": "If you want to add mappers, please click the button below to add some predefined mappers or to configure a new mapper.", "emptyMappersInstructions": "If you want to add mappers, please click the button below to add some predefined mappers or to configure a new mapper.",
"emptyBuiltInMappersInstructions": "All built in mappers were added to this client",
"emptyPrimaryAction": "Add predefined mapper", "emptyPrimaryAction": "Add predefined mapper",
"emptySecondaryAction": "Configure a new mapper", "emptySecondaryAction": "Configure a new mapper",
"mappingDeletedSuccess": "Mapping successfully deleted", "mappingDeletedSuccess": "Mapping successfully deleted",

View file

@ -1,4 +1,10 @@
import React, { useState, createContext, ReactNode, useContext } from "react"; import React, {
useState,
createContext,
ReactNode,
useContext,
useEffect,
} from "react";
import { AlertType, AlertPanel } from "./AlertPanel"; import { AlertType, AlertPanel } from "./AlertPanel";
import { AlertVariant } from "@patternfly/react-core"; import { AlertVariant } from "@patternfly/react-core";
@ -12,21 +18,39 @@ export const AlertContext = createContext<AlertProps>({
export const useAlerts = () => useContext(AlertContext); export const useAlerts = () => useContext(AlertContext);
type TimeOut = {
key: number;
timeOut: NodeJS.Timeout;
};
export const AlertProvider = ({ children }: { children: ReactNode }) => { export const AlertProvider = ({ children }: { children: ReactNode }) => {
const [alerts, setAlerts] = useState<AlertType[]>([]); const [alerts, setAlerts] = useState<AlertType[]>([]);
const [timers, setTimers] = useState<TimeOut[]>([]);
useEffect(() => {
const timersKeys = timers.map((timer) => timer.key);
const timeOuts = alerts
.filter((alert) => !timersKeys.includes(alert.key))
.map((alert) => {
const timeOut = setTimeout(() => hideAlert(alert.key), 8000);
return { key: alert.key, timeOut };
});
setTimers([...timers, ...timeOuts]);
return () => timers.forEach((timer) => clearTimeout(timer.timeOut));
}, [alerts]);
const createId = () => new Date().getTime(); const createId = () => new Date().getTime();
const hideAlert = (key: number) => { const hideAlert = (key: number) => {
setAlerts((alerts) => [...alerts.filter((el) => el.key !== key)]); setAlerts((alerts) => [...alerts.filter((el) => el.key !== key)]);
setTimers((timers) => [...timers.filter((timer) => timer.key === key)]);
}; };
const addAlert = ( const addAlert = (
message: string, message: string,
variant: AlertVariant = AlertVariant.default variant: AlertVariant = AlertVariant.default
) => { ) => {
const key = createId(); setAlerts([...alerts, { key: createId(), message, variant }]);
setAlerts([...alerts, { key, message, variant }]);
setTimeout(() => hideAlert(key), 8000);
}; };
return ( return (

View file

@ -1,4 +1,4 @@
import React from "react"; import React, { useState } from "react";
import { Button } from "@patternfly/react-core"; import { Button } from "@patternfly/react-core";
import { Meta, Story } from "@storybook/react"; import { Meta, Story } from "@storybook/react";
@ -7,7 +7,6 @@ import { ServerInfoContext } from "../context/server-info/ServerInfoProvider";
import { import {
AddMapperDialog, AddMapperDialog,
AddMapperDialogProps, AddMapperDialogProps,
useAddMapperDialog,
} from "../client-scopes/add/MapperDialog"; } from "../client-scopes/add/MapperDialog";
export default { export default {
@ -16,11 +15,15 @@ export default {
} as Meta; } as Meta;
const Template: Story<AddMapperDialogProps> = (args) => { const Template: Story<AddMapperDialogProps> = (args) => {
const [toggle, Dialog] = useAddMapperDialog(args); const [open, setOpen] = useState(false);
return ( return (
<ServerInfoContext.Provider value={serverInfo}> <ServerInfoContext.Provider value={serverInfo}>
<Dialog /> <AddMapperDialog
<Button onClick={toggle}>Show</Button> {...args}
open={open}
toggleDialog={() => setOpen(!open)}
/>
<Button onClick={() => setOpen(true)}>Show</Button>
</ServerInfoContext.Provider> </ServerInfoContext.Provider>
); );
}; };
@ -28,11 +31,10 @@ const Template: Story<AddMapperDialogProps> = (args) => {
export const BuildInDialog = Template.bind({}); export const BuildInDialog = Template.bind({});
BuildInDialog.args = { BuildInDialog.args = {
protocol: "openid-connect", protocol: "openid-connect",
buildIn: true, filter: [],
}; };
export const ProtocolMapperDialog = Template.bind({}); export const ProtocolMapperDialog = Template.bind({});
ProtocolMapperDialog.args = { ProtocolMapperDialog.args = {
protocol: "openid-connect", protocol: "openid-connect",
buildIn: false,
}; };

View file

@ -13,6 +13,6 @@ export default {
export const MapperListExample = () => ( export const MapperListExample = () => (
<ServerInfoContext.Provider value={serverInfo}> <ServerInfoContext.Provider value={serverInfo}>
<MapperList clientScope={clientScopeMock} /> <MapperList clientScope={clientScopeMock} refresh={() => {}} />
</ServerInfoContext.Provider> </ServerInfoContext.Provider>
); );