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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,8 @@
"protocol": "Protocol",
"includeInTokenScope": "Include in token scope",
"mappingDetails": "Mapper details",
"mappingCreatedSuccess": "Mapping successfully created",
"mappingCreatedError": "Could not create mapping: '{{error}}'",
"deleteMappingTitle": "Delete mapping?",
"deleteMappingConfirm": "Are you sure you want to delete this mapping?",
"mappingUpdatedSuccess": "Mapping successfully updated",
@ -49,6 +51,7 @@
"byConfiguration": "By configuration",
"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.",
"emptyBuiltInMappersInstructions": "All built in mappers were added to this client",
"emptyPrimaryAction": "Add predefined mapper",
"emptySecondaryAction": "Configure a new mapper",
"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 { AlertVariant } from "@patternfly/react-core";
@ -12,21 +18,39 @@ export const AlertContext = createContext<AlertProps>({
export const useAlerts = () => useContext(AlertContext);
type TimeOut = {
key: number;
timeOut: NodeJS.Timeout;
};
export const AlertProvider = ({ children }: { children: ReactNode }) => {
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 hideAlert = (key: number) => {
setAlerts((alerts) => [...alerts.filter((el) => el.key !== key)]);
setTimers((timers) => [...timers.filter((timer) => timer.key === key)]);
};
const addAlert = (
message: string,
variant: AlertVariant = AlertVariant.default
) => {
const key = createId();
setAlerts([...alerts, { key, message, variant }]);
setTimeout(() => hideAlert(key), 8000);
setAlerts([...alerts, { key: createId(), message, variant }]);
};
return (

View file

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

View file

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