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:
parent
7ebe695921
commit
d6e1161c83
9 changed files with 13814 additions and 13741 deletions
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -13,6 +13,6 @@ export default {
|
|||
|
||||
export const MapperListExample = () => (
|
||||
<ServerInfoContext.Provider value={serverInfo}>
|
||||
<MapperList clientScope={clientScopeMock} />
|
||||
<MapperList clientScope={clientScopeMock} refresh={() => {}} />
|
||||
</ServerInfoContext.Provider>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue