Initial version of config settings for executions (#1015)

* initial version of config settings for executions

still needs to render different controls based on the types

* fixing the types

* Update src/authentication/components/ExecutionConfigModal.tsx

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* fix use effect dependency fix

* added form type

* Add ExecutionConfigModalProps type

Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Erik Jan de Wit 2021-08-25 15:36:04 +02:00 committed by GitHub
parent 5b23354e35
commit 45095fdc42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 237 additions and 10 deletions

View file

@ -26,11 +26,6 @@ import { ExecutionList, IndexChange, LevelChange } from "./execution-model";
import { FlowDiagram } from "./components/FlowDiagram";
import { useAlerts } from "../components/alert/Alerts";
export type ExpandableExecution = AuthenticationExecutionInfoRepresentation & {
executionList: ExpandableExecution[];
isCollapsed: boolean;
};
export const FlowDetails = () => {
const { t } = useTranslation("authentication");
const adminClient = useAdminClient();

View file

@ -0,0 +1,222 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
import {
ActionGroup,
AlertVariant,
Button,
ButtonVariant,
Form,
FormGroup,
Modal,
ModalVariant,
TextInput,
ValidatedOptions,
} from "@patternfly/react-core";
import { CogIcon, TrashIcon } from "@patternfly/react-icons";
import type AuthenticatorConfigRepresentation from "keycloak-admin/lib/defs/authenticatorConfigRepresentation";
import type AuthenticatorConfigInfoRepresentation from "keycloak-admin/lib/defs/authenticatorConfigInfoRepresentation";
import type { ConfigPropertyRepresentation } from "keycloak-admin/lib/defs/authenticatorConfigInfoRepresentation";
import type { ExpandableExecution } from "../execution-model";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { useAlerts } from "../../components/alert/Alerts";
import { HelpItem } from "../../components/help-enabler/HelpItem";
type ExecutionConfigModalForm = {
[index: string]: string;
};
type ExecutionConfigModalProps = {
execution: ExpandableExecution;
};
export const ExecutionConfigModal = ({
execution,
}: ExecutionConfigModalProps) => {
const { t } = useTranslation("authentication");
const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts();
const [show, setShow] = useState(false);
const [config, setConfig] = useState<AuthenticatorConfigRepresentation>();
const [configDescription, setConfigDescription] =
useState<AuthenticatorConfigInfoRepresentation>();
const { register, errors, setValue, handleSubmit } =
useForm<ExecutionConfigModalForm>();
const setupForm = (
configDescription: AuthenticatorConfigInfoRepresentation,
config?: AuthenticatorConfigRepresentation
) => {
configDescription.properties!.map(
(property: ConfigPropertyRepresentation) => {
setValue(
property.name!,
config?.config?.[property.name!] || property.defaultValue || ""
);
}
);
if (config) {
setValue("alias", config.alias);
setValue("id", config.id);
}
};
useFetch(
async () => {
const configDescription =
await adminClient.authenticationManagement.getConfigDescription({
providerId: execution.providerId!,
});
let config: AuthenticatorConfigRepresentation | undefined;
if (execution.authenticationConfig) {
config = await adminClient.authenticationManagement.getConfig({
id: execution.authenticationConfig,
});
}
return { configDescription, config };
},
({ configDescription, config }) => {
setConfigDescription(configDescription);
setConfig(config);
},
[]
);
useEffect(() => {
if (configDescription) setupForm(configDescription, config);
}, [show]);
const save = async (changedConfig: ExecutionConfigModalForm) => {
try {
if (config) {
const newConfig = {
...config,
config: changedConfig,
};
await adminClient.authenticationManagement.updateConfig(newConfig);
setConfig(newConfig);
} else {
const newConfig = {
id: execution.id!,
alias: changedConfig.alias,
config: changedConfig,
};
const { id } = await adminClient.authenticationManagement.createConfig(
newConfig
);
setConfig({ ...newConfig, id });
}
addAlert(t("configSaveSuccess"), AlertVariant.success);
setShow(false);
} catch (error) {
addError("authentication:configSaveError", error);
}
};
return (
<>
<Button
variant="plain"
aria-label={t("common:settings")}
onClick={() => setShow(true)}
>
<CogIcon />
</Button>
{configDescription && (
<Modal
variant={ModalVariant.small}
isOpen={show}
title={t("executionConfig", { name: configDescription.name })}
onClose={() => setShow(false)}
>
<Form id="execution-config-form" onSubmit={handleSubmit(save)}>
<FormGroup
label={t("alias")}
fieldId="alias"
helperTextInvalid={t("common:required")}
validated={
errors.alias ? ValidatedOptions.error : ValidatedOptions.default
}
isRequired
labelIcon={
<HelpItem
helpText="authentication-help:alias"
forLabel={t("alias")}
forID="alias"
/>
}
>
<TextInput
isReadOnly={!!config}
type="text"
id="alias"
name="alias"
data-testid="alias"
ref={register({ required: true })}
validated={
errors.alias
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
</FormGroup>
{configDescription?.properties?.map((property) => (
<FormGroup
key={property.name}
label={property.label}
fieldId={property.name!}
labelIcon={
<HelpItem
helpText={property.helpText}
forLabel={property.name!}
forID={property.name!}
/>
}
>
<TextInput
type="text"
id={property.name}
name={property.name}
ref={register}
/>
</FormGroup>
))}
<ActionGroup>
<Button data-testid="save" variant="primary" type="submit">
{t("common:save")}
</Button>
<Button
data-testid="cancel"
variant={ButtonVariant.link}
onClick={() => {
setShow(false);
}}
>
{t("common:cancel")}
</Button>
{config && (
<Button
className="pf-u-ml-4xl"
data-testid="clear"
variant={ButtonVariant.link}
onClick={async () => {
await adminClient.authenticationManagement.delConfig({
id: config.id!,
});
setConfig(undefined);
setShow(false);
}}
>
{t("common:clear")} <TrashIcon />
</Button>
)}
</ActionGroup>
</Form>
</Modal>
)}
</>
);
};

View file

@ -20,11 +20,10 @@ export const FlowHeader = () => {
className="keycloak__authentication__header"
dataListCells={[
<DataListCell key="step" id="headerName">
<>{t("steps")}</>
</DataListCell>,
<DataListCell key="requirement">
<>{t("requirement")}</>
{t("steps")}
</DataListCell>,
<DataListCell key="requirement">{t("requirement")}</DataListCell>,
<DataListCell key="config"></DataListCell>,
]}
/>
</DataListItemRow>

View file

@ -13,11 +13,12 @@ import {
} from "@patternfly/react-core";
import type AuthenticationExecutionInfoRepresentation from "keycloak-admin/lib/defs/authenticationExecutionInfoRepresentation";
import type { ExpandableExecution } from "../FlowDetails";
import type { ExpandableExecution } from "../execution-model";
import { FlowTitle } from "./FlowTitle";
import { FlowRequirementDropdown } from "./FlowRequirementDropdown";
import "./flow-row.css";
import { ExecutionConfigModal } from "./ExecutionConfigModal";
type FlowRowProps = {
execution: ExpandableExecution;
@ -81,6 +82,11 @@ export const FlowRow = ({
onChange={onRowChange}
/>
</DataListCell>,
<DataListCell key={`${execution.id}-config`}>
{execution.configurable && (
<ExecutionConfigModal execution={execution} />
)}
</DataListCell>,
]}
/>
</DataListItemRow>

View file

@ -9,5 +9,6 @@ export default {
"Execution can have a wide range of actions, from sending a reset email to validating an OTP",
addSubFlow:
"Sub-Flows can be either generic or form. The form type is used to construct a sub-flow that generates a single flow for the user. Sub-flows are a special type of execution that evaluate as successful depending on how the executions they contain evaluate.",
alias: "Name of the configuration",
},
};

View file

@ -49,5 +49,9 @@ export default {
DISABLED: "Disabled",
CONDITIONAL: "Conditional",
},
executionConfig: "{{name}} config",
alias: "Alias",
configSaveSuccess: "Successfully saved the execution config",
configSaveError: "Could not save the execution config: {{error}}",
},
};