Add button on authentication flows (#1119)

This commit is contained in:
Erik Jan de Wit 2021-09-06 14:43:36 +02:00 committed by GitHub
parent 0fc832e641
commit f1f0c362b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 518 additions and 53 deletions

View file

@ -56,4 +56,26 @@ describe("Authentication test", () => {
cy.get(".react-flow").should("exist"); cy.get(".react-flow").should("exist");
}); });
it("should add a execution", () => {
listingPage.goToItemDetails("Copy of browser");
detailPage.addExecution(
"Copy of browser forms",
"console-username-password"
);
masthead.checkNotificationMessage("Flow successfully updated");
detailPage.executionExists("Username Password Challenge");
});
it("should add a condition", () => {
listingPage.goToItemDetails("Copy of browser");
detailPage.addCondition(
"Copy of browser Browser - Conditional OTP",
"conditional-user-role"
);
masthead.checkNotificationMessage("Flow successfully updated");
detailPage.executionExists("Username Password Challenge");
});
}); });

View file

@ -36,4 +36,28 @@ export default class FlowDetails {
cy.get("#diagramView").click(); cy.get("#diagramView").click();
return this; return this;
} }
private clickEditDropdownForFlow(subFlowName: string, option: string) {
cy.getId(`${subFlowName}-edit-dropdown`).click().contains(option).click();
}
addExecution(subFlowName: string, executionTestId: string) {
this.clickEditDropdownForFlow(subFlowName, "Add step");
cy.get(".pf-c-pagination").should("exist");
cy.getId(executionTestId).click();
cy.getId("modal-add").click();
return this;
}
addCondition(subFlowName: string, executionTestId: string) {
this.clickEditDropdownForFlow(subFlowName, "Add condition");
cy.get(".pf-c-pagination").should("not.exist");
cy.getId(executionTestId).click();
cy.getId("modal-add").click();
return this;
}
} }

93
package-lock.json generated
View file

@ -8,7 +8,7 @@
"version": "0.0.1", "version": "0.0.1",
"license": "Apache", "license": "Apache",
"dependencies": { "dependencies": {
"@keycloak/keycloak-admin-client": "^16.0.0-dev.2", "@keycloak/keycloak-admin-client": "^16.0.0-dev.4",
"@patternfly/patternfly": "^4.132.2", "@patternfly/patternfly": "^4.132.2",
"@patternfly/react-core": "4.152.4", "@patternfly/react-core": "4.152.4",
"@patternfly/react-icons": "4.11.14", "@patternfly/react-icons": "4.11.14",
@ -2541,9 +2541,9 @@
} }
}, },
"node_modules/@keycloak/keycloak-admin-client": { "node_modules/@keycloak/keycloak-admin-client": {
"version": "16.0.0-dev.2", "version": "16.0.0-dev.4",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.2.tgz", "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.4.tgz",
"integrity": "sha512-eLeN4/O5OWjU0fIIvndv0oLSRQN3q0bdMep19E+09/qL4jeNeBR4ltxLKd0mnW1FY53cZJ+QWLaq/lfgRXFfzA==", "integrity": "sha512-8Wp/hqi6TWnt+woxoRwhklPP1SBw+EGzMGQQdErHrkTvfXCd0JFeECRzIZyGcmeFLbvpyLMwLPYFYknC20CENQ==",
"dependencies": { "dependencies": {
"axios": "^0.21.0", "axios": "^0.21.0",
"camelize": "^1.0.0", "camelize": "^1.0.0",
@ -2587,9 +2587,9 @@
} }
}, },
"node_modules/@npmcli/arborist": { "node_modules/@npmcli/arborist": {
"version": "2.7.1", "version": "2.8.2",
"resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.7.1.tgz", "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.8.2.tgz",
"integrity": "sha512-EGDHJs6dna/52BrStr/6aaRcMLrYxGbSjT4V3JzvoTBY9/w5i2+1KNepmsG80CAsGADdo6nuNnFwb7sDRm8ZAw==", "integrity": "sha512-6E1XJ0YXBaI9J+25gcTF110MGNx3jv6npr4Rz1U0UAqkuVV7bbDznVJvNqi6F0p8vgrE+Smf9jDTn1DR+7uBjQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@npmcli/installed-package-contents": "^1.0.7", "@npmcli/installed-package-contents": "^1.0.7",
@ -2608,10 +2608,10 @@
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"mkdirp-infer-owner": "^2.0.0", "mkdirp-infer-owner": "^2.0.0",
"npm-install-checks": "^4.0.0", "npm-install-checks": "^4.0.0",
"npm-package-arg": "^8.1.0", "npm-package-arg": "^8.1.5",
"npm-pick-manifest": "^6.1.0", "npm-pick-manifest": "^6.1.0",
"npm-registry-fetch": "^11.0.0", "npm-registry-fetch": "^11.0.0",
"pacote": "^11.2.6", "pacote": "^11.3.5",
"parse-conflict-json": "^1.1.1", "parse-conflict-json": "^1.1.1",
"proc-log": "^1.0.0", "proc-log": "^1.0.0",
"promise-all-reject-late": "^1.0.0", "promise-all-reject-late": "^1.0.0",
@ -2621,7 +2621,6 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"semver": "^7.3.5", "semver": "^7.3.5",
"ssri": "^8.0.1", "ssri": "^8.0.1",
"tar": "^6.1.0",
"treeverse": "^1.0.4", "treeverse": "^1.0.4",
"walk-up-path": "^1.0.0" "walk-up-path": "^1.0.0"
}, },
@ -8277,6 +8276,20 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true "dev": true
}, },
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.1", "version": "1.1.1",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
@ -14975,6 +14988,21 @@
"@rollup/plugin-inject": "^4.0.0" "@rollup/plugin-inject": "^4.0.0"
} }
}, },
"node_modules/rollup/node_modules/fsevents": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
"integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
"deprecated": "\"Please update to latest v2.3 or v2.2\"",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/rsvp": { "node_modules/rsvp": {
"version": "4.8.5", "version": "4.8.5",
"integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==",
@ -16504,9 +16532,9 @@
} }
}, },
"node_modules/tar": { "node_modules/tar": {
"version": "6.1.8", "version": "6.1.11",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.8.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",
"integrity": "sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A==", "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"chownr": "^2.0.0", "chownr": "^2.0.0",
@ -19360,9 +19388,9 @@
} }
}, },
"@keycloak/keycloak-admin-client": { "@keycloak/keycloak-admin-client": {
"version": "16.0.0-dev.2", "version": "16.0.0-dev.4",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.2.tgz", "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.4.tgz",
"integrity": "sha512-eLeN4/O5OWjU0fIIvndv0oLSRQN3q0bdMep19E+09/qL4jeNeBR4ltxLKd0mnW1FY53cZJ+QWLaq/lfgRXFfzA==", "integrity": "sha512-8Wp/hqi6TWnt+woxoRwhklPP1SBw+EGzMGQQdErHrkTvfXCd0JFeECRzIZyGcmeFLbvpyLMwLPYFYknC20CENQ==",
"requires": { "requires": {
"axios": "^0.21.0", "axios": "^0.21.0",
"camelize": "^1.0.0", "camelize": "^1.0.0",
@ -19397,9 +19425,9 @@
} }
}, },
"@npmcli/arborist": { "@npmcli/arborist": {
"version": "2.7.1", "version": "2.8.2",
"resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.7.1.tgz", "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.8.2.tgz",
"integrity": "sha512-EGDHJs6dna/52BrStr/6aaRcMLrYxGbSjT4V3JzvoTBY9/w5i2+1KNepmsG80CAsGADdo6nuNnFwb7sDRm8ZAw==", "integrity": "sha512-6E1XJ0YXBaI9J+25gcTF110MGNx3jv6npr4Rz1U0UAqkuVV7bbDznVJvNqi6F0p8vgrE+Smf9jDTn1DR+7uBjQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@npmcli/installed-package-contents": "^1.0.7", "@npmcli/installed-package-contents": "^1.0.7",
@ -19418,10 +19446,10 @@
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"mkdirp-infer-owner": "^2.0.0", "mkdirp-infer-owner": "^2.0.0",
"npm-install-checks": "^4.0.0", "npm-install-checks": "^4.0.0",
"npm-package-arg": "^8.1.0", "npm-package-arg": "^8.1.5",
"npm-pick-manifest": "^6.1.0", "npm-pick-manifest": "^6.1.0",
"npm-registry-fetch": "^11.0.0", "npm-registry-fetch": "^11.0.0",
"pacote": "^11.2.6", "pacote": "^11.3.5",
"parse-conflict-json": "^1.1.1", "parse-conflict-json": "^1.1.1",
"proc-log": "^1.0.0", "proc-log": "^1.0.0",
"promise-all-reject-late": "^1.0.0", "promise-all-reject-late": "^1.0.0",
@ -19431,7 +19459,6 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"semver": "^7.3.5", "semver": "^7.3.5",
"ssri": "^8.0.1", "ssri": "^8.0.1",
"tar": "^6.1.0",
"treeverse": "^1.0.4", "treeverse": "^1.0.4",
"walk-up-path": "^1.0.0" "walk-up-path": "^1.0.0"
}, },
@ -23837,6 +23864,13 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true "dev": true
}, },
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"function-bind": { "function-bind": {
"version": "1.1.1", "version": "1.1.1",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
@ -28791,6 +28825,15 @@
"dev": true, "dev": true,
"requires": { "requires": {
"fsevents": "~2.1.2" "fsevents": "~2.1.2"
},
"dependencies": {
"fsevents": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
"integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
"dev": true,
"optional": true
}
} }
}, },
"rollup-plugin-polyfill-node": { "rollup-plugin-polyfill-node": {
@ -30003,9 +30046,9 @@
} }
}, },
"tar": { "tar": {
"version": "6.1.8", "version": "6.1.11",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.8.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",
"integrity": "sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A==", "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==",
"dev": true, "dev": true,
"requires": { "requires": {
"chownr": "^2.0.0", "chownr": "^2.0.0",

View file

@ -24,7 +24,7 @@
"prepare": "husky install" "prepare": "husky install"
}, },
"dependencies": { "dependencies": {
"@keycloak/keycloak-admin-client": "^16.0.0-dev.2", "@keycloak/keycloak-admin-client": "^16.0.0-dev.4",
"@patternfly/patternfly": "^4.132.2", "@patternfly/patternfly": "^4.132.2",
"@patternfly/react-core": "4.152.4", "@patternfly/react-core": "4.152.4",
"@patternfly/react-icons": "4.11.14", "@patternfly/react-icons": "4.11.14",

View file

@ -14,6 +14,7 @@ import {
import { CheckCircleIcon, TableIcon } from "@patternfly/react-icons"; import { CheckCircleIcon, TableIcon } from "@patternfly/react-icons";
import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation"; import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation";
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
import type AuthenticationFlowRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationFlowRepresentation"; import type AuthenticationFlowRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationFlowRepresentation";
import type { FlowParams } from "./routes/Flow"; import type { FlowParams } from "./routes/Flow";
import { ViewHeader } from "../components/view-header/ViewHeader"; import { ViewHeader } from "../components/view-header/ViewHeader";
@ -22,14 +23,23 @@ import { EmptyExecutionState } from "./EmptyExecutionState";
import { toUpperCase } from "../util"; import { toUpperCase } from "../util";
import { FlowHeader } from "./components/FlowHeader"; import { FlowHeader } from "./components/FlowHeader";
import { FlowRow } from "./components/FlowRow"; import { FlowRow } from "./components/FlowRow";
import { ExecutionList, IndexChange, LevelChange } from "./execution-model"; import {
ExecutionList,
ExpandableExecution,
IndexChange,
LevelChange,
} from "./execution-model";
import { FlowDiagram } from "./components/FlowDiagram"; import { FlowDiagram } from "./components/FlowDiagram";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
export const providerConditionFilter = (
value: AuthenticationProviderRepresentation
) => value.displayName?.startsWith("Condition ");
export const FlowDetails = () => { export const FlowDetails = () => {
const { t } = useTranslation("authentication"); const { t } = useTranslation("authentication");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { addAlert } = useAlerts(); const { addAlert, addError } = useAlerts();
const { id, usedBy, builtIn } = useParams<FlowParams>(); const { id, usedBy, builtIn } = useParams<FlowParams>();
const [key, setKey] = useState(0); const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime()); const refresh = () => setKey(new Date().getTime());
@ -94,12 +104,7 @@ export const FlowDetails = () => {
refresh(); refresh();
addAlert(t("updateFlowSuccess"), AlertVariant.success); addAlert(t("updateFlowSuccess"), AlertVariant.success);
} catch (error: any) { } catch (error: any) {
addAlert( addError("authentication:updateFlowError", error);
t("updateFlowError", {
error: error.response?.data?.errorMessage || error,
}),
AlertVariant.danger
);
} }
}; };
@ -114,12 +119,23 @@ export const FlowDetails = () => {
refresh(); refresh();
addAlert(t("updateFlowSuccess"), AlertVariant.success); addAlert(t("updateFlowSuccess"), AlertVariant.success);
} catch (error: any) { } catch (error: any) {
addAlert( addError("authentication:updateFlowError", error);
t("updateFlowError", { }
error: error.response?.data?.errorMessage || error, };
}),
AlertVariant.danger const addExecution = async (
); execution: ExpandableExecution,
type: AuthenticationProviderRepresentation
) => {
try {
await adminClient.authenticationManagement.addExecutionToFlow({
flow: execution.displayName!,
provider: type.id!,
});
refresh();
addAlert(t("updateFlowSuccess"), AlertVariant.success);
} catch (error) {
addError("authentication:updateFlowError", error);
} }
}; };
@ -215,6 +231,7 @@ export const FlowDetails = () => {
setExecutionList(executionList.clone()); setExecutionList(executionList.clone());
}} }}
onRowChange={update} onRowChange={update}
onAddExecution={addExecution}
/> />
))} ))}
</> </>

View file

@ -0,0 +1,91 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Dropdown, DropdownItem, DropdownToggle } from "@patternfly/react-core";
import { PlusIcon } from "@patternfly/react-icons";
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
import type { ExpandableExecution } from "../execution-model";
import { AddStepModal, FlowType } from "./modals/AddStepModal";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { AddSubFlowModal } from "./modals/AddSubFlowModal";
type EditFlowDropdownProps = {
execution: ExpandableExecution;
onAddExecution: (
execution: ExpandableExecution,
type: AuthenticationProviderRepresentation
) => void;
};
export const EditFlowDropdown = ({
execution,
onAddExecution,
}: EditFlowDropdownProps) => {
const { t } = useTranslation("authentication");
const adminClient = useAdminClient();
const [open, setOpen] = useState(false);
const [type, setType] = useState<FlowType>();
const [providerId, setProviderId] = useState<string>();
useFetch(
() =>
adminClient.authenticationManagement.getFlow({
flowId: execution.flowId!,
}),
({ providerId }) => setProviderId(providerId),
[]
);
return (
<>
<Dropdown
isPlain
position="right"
data-testid={`${execution.displayName}-edit-dropdown`}
isOpen={open}
toggle={
<DropdownToggle onToggle={(open) => setOpen(open)}>
<PlusIcon />
</DropdownToggle>
}
dropdownItems={[
<DropdownItem
key="addStep"
onClick={() =>
setType(providerId === "form-flow" ? "form" : "basic")
}
>
{t("addStep")}
</DropdownItem>,
<DropdownItem key="addCondition" onClick={() => setType("condition")}>
{t("addCondition")}
</DropdownItem>,
<DropdownItem key="addSubFlow" onClick={() => setType("subFlow")}>
{t("addSubFlow")}
</DropdownItem>,
]}
onSelect={() => setOpen(false)}
/>
{type && type !== "subFlow" && (
<AddStepModal
name={execution.displayName!}
type={type}
onSelect={(type) => {
if (type) {
onAddExecution(execution, type);
}
setType(undefined);
}}
/>
)}
{type === "subFlow" && (
<AddSubFlowModal
name={execution.displayName!}
onCancel={() => setType(undefined)}
onConfirm={() => setType(undefined)}
/>
)}
</>
);
};

View file

@ -26,6 +26,7 @@ import { EndSubFlowNode, StartSubFlowNode } from "./diagram/SubFlowNode";
import { ConditionalNode } from "./diagram/ConditionalNode"; import { ConditionalNode } from "./diagram/ConditionalNode";
import { ButtonEdge } from "./diagram/ButtonEdge"; import { ButtonEdge } from "./diagram/ButtonEdge";
import { getLayoutedElements } from "./diagram/auto-layout"; import { getLayoutedElements } from "./diagram/auto-layout";
import { providerConditionFilter } from "../FlowDetails";
import "./flow-diagram.css"; import "./flow-diagram.css";
@ -54,7 +55,7 @@ const createNode = (ex: ExpandableExecution) => {
if (ex.executionList) { if (ex.executionList) {
nodeType = "startSubFlow"; nodeType = "startSubFlow";
} }
if (ex.displayName?.startsWith("Condition")) { if (providerConditionFilter(ex)) {
nodeType = "conditional"; nodeType = "conditional";
} }
return { return {

View file

@ -13,26 +13,34 @@ import {
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation"; import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation";
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
import type { ExpandableExecution } from "../execution-model"; import type { ExpandableExecution } from "../execution-model";
import { FlowTitle } from "./FlowTitle"; import { FlowTitle } from "./FlowTitle";
import { FlowRequirementDropdown } from "./FlowRequirementDropdown"; import { FlowRequirementDropdown } from "./FlowRequirementDropdown";
import { ExecutionConfigModal } from "./ExecutionConfigModal";
import { EditFlowDropdown } from "./EditFlowDropdown";
import "./flow-row.css"; import "./flow-row.css";
import { ExecutionConfigModal } from "./ExecutionConfigModal";
type FlowRowProps = { type FlowRowProps = {
execution: ExpandableExecution; execution: ExpandableExecution;
onRowClick: (execution: ExpandableExecution) => void; onRowClick: (execution: ExpandableExecution) => void;
onRowChange: (execution: AuthenticationExecutionInfoRepresentation) => void; onRowChange: (execution: AuthenticationExecutionInfoRepresentation) => void;
onAddExecution: (
execution: ExpandableExecution,
type: AuthenticationProviderRepresentation
) => void;
}; };
export const FlowRow = ({ export const FlowRow = ({
execution, execution,
onRowClick, onRowClick,
onRowChange, onRowChange,
onAddExecution,
}: FlowRowProps) => { }: FlowRowProps) => {
const { t } = useTranslation("authentication"); const { t } = useTranslation("authentication");
const hasSubList = !!execution.executionList?.length; const hasSubList = !!execution.executionList?.length;
return ( return (
<> <>
<DataListItem <DataListItem
@ -86,6 +94,12 @@ export const FlowRow = ({
{execution.configurable && ( {execution.configurable && (
<ExecutionConfigModal execution={execution} /> <ExecutionConfigModal execution={execution} />
)} )}
{execution.authenticationFlow && (
<EditFlowDropdown
execution={execution}
onAddExecution={onAddExecution}
/>
)}
</DataListCell>, </DataListCell>,
]} ]}
/> />
@ -99,6 +113,7 @@ export const FlowRow = ({
execution={execution} execution={execution}
onRowClick={onRowClick} onRowClick={onRowClick}
onRowChange={onRowChange} onRowChange={onRowChange}
onAddExecution={onAddExecution}
/> />
))} ))}
</> </>

View file

@ -0,0 +1,142 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import {
Button,
ButtonVariant,
Form,
Modal,
ModalVariant,
PageSection,
Radio,
} from "@patternfly/react-core";
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
import { PaginatingTableToolbar } from "../../../components/table-toolbar/PaginatingTableToolbar";
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
import { providerConditionFilter } from "../../FlowDetails";
type AuthenticationProviderListProps = {
list?: AuthenticationProviderRepresentation[];
setValue: (provider?: AuthenticationProviderRepresentation) => void;
};
const AuthenticationProviderList = ({
list,
setValue,
}: AuthenticationProviderListProps) => {
return (
<PageSection variant="light" className="pf-u-py-lg">
<Form isHorizontal>
{list?.map((provider) => (
<Radio
id={provider.id!}
key={provider.id}
name="provider"
label={provider.displayName}
data-testid={provider.id}
description={provider.description}
onChange={(_val, event) => {
const { id } = event.currentTarget;
const value = list.find((p) => p.id === id);
setValue(value);
}}
/>
))}
</Form>
</PageSection>
);
};
export type FlowType = "client" | "form" | "basic" | "condition" | "subFlow";
type AddStepModalProps = {
name: string;
type: FlowType;
onSelect: (value?: AuthenticationProviderRepresentation) => void;
};
export const AddStepModal = ({ name, type, onSelect }: AddStepModalProps) => {
const { t } = useTranslation("authentication");
const adminClient = useAdminClient();
const [value, setValue] = useState<AuthenticationProviderRepresentation>();
const [providers, setProviders] =
useState<AuthenticationProviderRepresentation[]>();
const [max, setMax] = useState(10);
const [first, setFirst] = useState(0);
useFetch(
async () => {
switch (type) {
case "client":
return adminClient.authenticationManagement.getClientAuthenticatorProviders();
case "form":
return adminClient.authenticationManagement.getFormActionProviders();
case "condition": {
const providers =
await adminClient.authenticationManagement.getAuthenticatorProviders();
return providers.filter(providerConditionFilter);
}
case "basic":
default: {
const providers =
await adminClient.authenticationManagement.getAuthenticatorProviders();
return providers.filter((p) => !providerConditionFilter(p));
}
}
},
(providers) => setProviders(providers),
[]
);
const page = providers?.slice(first, first + max + 1);
return (
<Modal
variant={ModalVariant.medium}
isOpen={true}
title={t("addStepTo", { name })}
onClose={() => onSelect()}
actions={[
<Button
id="modal-add"
data-testid="modal-add"
key="add"
onClick={() => onSelect(value)}
>
{t("common:add")}
</Button>,
<Button
data-testid="cancel"
id="modal-cancel"
key="cancel"
variant={ButtonVariant.link}
onClick={() => {
onSelect();
}}
>
{t("common:cancel")}
</Button>,
]}
>
{providers && providers.length > max && (
<PaginatingTableToolbar
count={page?.length || 0}
first={first}
max={max}
onNextClick={setFirst}
onPreviousClick={setFirst}
onPerPageSelect={(first, max) => {
setFirst(first);
setMax(max);
}}
>
<AuthenticationProviderList list={page} setValue={setValue} />
</PaginatingTableToolbar>
)}
{providers && providers.length <= max && (
<AuthenticationProviderList list={providers} setValue={setValue} />
)}
</Modal>
);
};

View file

@ -0,0 +1,113 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
import {
Button,
ButtonVariant,
Form,
FormGroup,
Modal,
ModalVariant,
TextInput,
ValidatedOptions,
} from "@patternfly/react-core";
import { HelpItem } from "../../../components/help-enabler/HelpItem";
type AddSubFlowProps = {
name: string;
onConfirm: () => void;
onCancel: () => void;
};
export const AddSubFlowModal = ({
name,
onConfirm,
onCancel,
}: AddSubFlowProps) => {
const { t } = useTranslation("authentication");
const { register, errors, handleSubmit } = useForm();
return (
<Modal
variant={ModalVariant.medium}
isOpen={true}
title={t("addStepTo", { name })}
onClose={() => onCancel()}
actions={[
<Button
id="modal-add"
data-testid="modal-add"
key="add"
onClick={() => onConfirm()}
>
{t("common:add")}
</Button>,
<Button
data-testid="cancel"
id="modal-cancel"
key="cancel"
variant={ButtonVariant.link}
onClick={() => {
onCancel();
}}
>
{t("common:cancel")}
</Button>,
]}
>
<Form
id="execution-config-form"
isHorizontal
onSubmit={handleSubmit(onConfirm)}
>
<FormGroup
label={t("common:name")}
fieldId="name"
helperTextInvalid={t("common:required")}
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
isRequired
labelIcon={
<HelpItem
helpText="authentication-help:name"
forLabel={t("name")}
forID="name"
/>
}
>
<TextInput
type="text"
id="name"
name="name"
data-testid="name"
ref={register({ required: true })}
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
/>
</FormGroup>
<FormGroup
label={t("common:description")}
fieldId="description"
labelIcon={
<HelpItem
helpText="authentication-help:description"
forLabel={t("common:description")}
forID="description"
/>
}
>
<TextInput
type="text"
id="description"
name="description"
data-testid="description"
ref={register}
/>
</FormGroup>
</Form>
</Modal>
);
};

View file

@ -41,6 +41,9 @@ export default {
addExecution: "Add execution", addExecution: "Add execution",
addSubFlowTitle: "Add a sub-flow", addSubFlowTitle: "Add a sub-flow",
addSubFlow: "Add sub-flow", addSubFlow: "Add sub-flow",
addCondition: "Add condition",
addStep: "Add step",
addStepTo: "Add step to {{name}}",
steps: "Steps", steps: "Steps",
requirement: "Requirement", requirement: "Requirement",
requirements: { requirements: {

View file

@ -18,6 +18,7 @@ import {
SplitItem, SplitItem,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import type CredentialRepresentation from "@keycloak/keycloak-admin-client/lib/defs/credentialRepresentation"; import type CredentialRepresentation from "@keycloak/keycloak-admin-client/lib/defs/credentialRepresentation";
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
import { useAlerts } from "../../components/alert/Alerts"; import { useAlerts } from "../../components/alert/Alerts";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
@ -29,11 +30,6 @@ import { ClientSecret } from "./ClientSecret";
import { SignedJWT } from "./SignedJWT"; import { SignedJWT } from "./SignedJWT";
import { X509 } from "./X509"; import { X509 } from "./X509";
type ClientAuthenticatorProviders = {
id: string;
displayName: string;
};
type AccessToken = { type AccessToken = {
registrationAccessToken: string; registrationAccessToken: string;
}; };
@ -48,9 +44,9 @@ export const Credentials = ({ clientId, save }: CredentialsProps) => {
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const [providers, setProviders] = useState<ClientAuthenticatorProviders[]>( const [providers, setProviders] = useState<
[] AuthenticationProviderRepresentation[]
); >([]);
const { const {
control, control,
@ -69,9 +65,7 @@ export const Credentials = ({ clientId, save }: CredentialsProps) => {
useFetch( useFetch(
async () => { async () => {
const providers = const providers =
await adminClient.authenticationManagement.getClientAuthenticatorProviders( await adminClient.authenticationManagement.getClientAuthenticatorProviders();
{ id: clientId }
);
const secret = await adminClient.clients.getClientSecret({ const secret = await adminClient.clients.getClientSecret({
id: clientId, id: clientId,