Add button on authentication flows (#1119)
This commit is contained in:
parent
0fc832e641
commit
f1f0c362b4
12 changed files with 518 additions and 53 deletions
|
@ -56,4 +56,26 @@ describe("Authentication test", () => {
|
|||
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -36,4 +36,28 @@ export default class FlowDetails {
|
|||
cy.get("#diagramView").click();
|
||||
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
93
package-lock.json
generated
|
@ -8,7 +8,7 @@
|
|||
"version": "0.0.1",
|
||||
"license": "Apache",
|
||||
"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/react-core": "4.152.4",
|
||||
"@patternfly/react-icons": "4.11.14",
|
||||
|
@ -2541,9 +2541,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@keycloak/keycloak-admin-client": {
|
||||
"version": "16.0.0-dev.2",
|
||||
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.2.tgz",
|
||||
"integrity": "sha512-eLeN4/O5OWjU0fIIvndv0oLSRQN3q0bdMep19E+09/qL4jeNeBR4ltxLKd0mnW1FY53cZJ+QWLaq/lfgRXFfzA==",
|
||||
"version": "16.0.0-dev.4",
|
||||
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.4.tgz",
|
||||
"integrity": "sha512-8Wp/hqi6TWnt+woxoRwhklPP1SBw+EGzMGQQdErHrkTvfXCd0JFeECRzIZyGcmeFLbvpyLMwLPYFYknC20CENQ==",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.0",
|
||||
"camelize": "^1.0.0",
|
||||
|
@ -2587,9 +2587,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@npmcli/arborist": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.7.1.tgz",
|
||||
"integrity": "sha512-EGDHJs6dna/52BrStr/6aaRcMLrYxGbSjT4V3JzvoTBY9/w5i2+1KNepmsG80CAsGADdo6nuNnFwb7sDRm8ZAw==",
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.8.2.tgz",
|
||||
"integrity": "sha512-6E1XJ0YXBaI9J+25gcTF110MGNx3jv6npr4Rz1U0UAqkuVV7bbDznVJvNqi6F0p8vgrE+Smf9jDTn1DR+7uBjQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@npmcli/installed-package-contents": "^1.0.7",
|
||||
|
@ -2608,10 +2608,10 @@
|
|||
"mkdirp": "^1.0.4",
|
||||
"mkdirp-infer-owner": "^2.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-registry-fetch": "^11.0.0",
|
||||
"pacote": "^11.2.6",
|
||||
"pacote": "^11.3.5",
|
||||
"parse-conflict-json": "^1.1.1",
|
||||
"proc-log": "^1.0.0",
|
||||
"promise-all-reject-late": "^1.0.0",
|
||||
|
@ -2621,7 +2621,6 @@
|
|||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.5",
|
||||
"ssri": "^8.0.1",
|
||||
"tar": "^6.1.0",
|
||||
"treeverse": "^1.0.4",
|
||||
"walk-up-path": "^1.0.0"
|
||||
},
|
||||
|
@ -8277,6 +8276,20 @@
|
|||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
|
@ -14975,6 +14988,21 @@
|
|||
"@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": {
|
||||
"version": "4.8.5",
|
||||
"integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==",
|
||||
|
@ -16504,9 +16532,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.1.8",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.8.tgz",
|
||||
"integrity": "sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A==",
|
||||
"version": "6.1.11",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",
|
||||
"integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
|
@ -19360,9 +19388,9 @@
|
|||
}
|
||||
},
|
||||
"@keycloak/keycloak-admin-client": {
|
||||
"version": "16.0.0-dev.2",
|
||||
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.2.tgz",
|
||||
"integrity": "sha512-eLeN4/O5OWjU0fIIvndv0oLSRQN3q0bdMep19E+09/qL4jeNeBR4ltxLKd0mnW1FY53cZJ+QWLaq/lfgRXFfzA==",
|
||||
"version": "16.0.0-dev.4",
|
||||
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.4.tgz",
|
||||
"integrity": "sha512-8Wp/hqi6TWnt+woxoRwhklPP1SBw+EGzMGQQdErHrkTvfXCd0JFeECRzIZyGcmeFLbvpyLMwLPYFYknC20CENQ==",
|
||||
"requires": {
|
||||
"axios": "^0.21.0",
|
||||
"camelize": "^1.0.0",
|
||||
|
@ -19397,9 +19425,9 @@
|
|||
}
|
||||
},
|
||||
"@npmcli/arborist": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.7.1.tgz",
|
||||
"integrity": "sha512-EGDHJs6dna/52BrStr/6aaRcMLrYxGbSjT4V3JzvoTBY9/w5i2+1KNepmsG80CAsGADdo6nuNnFwb7sDRm8ZAw==",
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.8.2.tgz",
|
||||
"integrity": "sha512-6E1XJ0YXBaI9J+25gcTF110MGNx3jv6npr4Rz1U0UAqkuVV7bbDznVJvNqi6F0p8vgrE+Smf9jDTn1DR+7uBjQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@npmcli/installed-package-contents": "^1.0.7",
|
||||
|
@ -19418,10 +19446,10 @@
|
|||
"mkdirp": "^1.0.4",
|
||||
"mkdirp-infer-owner": "^2.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-registry-fetch": "^11.0.0",
|
||||
"pacote": "^11.2.6",
|
||||
"pacote": "^11.3.5",
|
||||
"parse-conflict-json": "^1.1.1",
|
||||
"proc-log": "^1.0.0",
|
||||
"promise-all-reject-late": "^1.0.0",
|
||||
|
@ -19431,7 +19459,6 @@
|
|||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.5",
|
||||
"ssri": "^8.0.1",
|
||||
"tar": "^6.1.0",
|
||||
"treeverse": "^1.0.4",
|
||||
"walk-up-path": "^1.0.0"
|
||||
},
|
||||
|
@ -23837,6 +23864,13 @@
|
|||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
|
@ -28791,6 +28825,15 @@
|
|||
"dev": true,
|
||||
"requires": {
|
||||
"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": {
|
||||
|
@ -30003,9 +30046,9 @@
|
|||
}
|
||||
},
|
||||
"tar": {
|
||||
"version": "6.1.8",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.8.tgz",
|
||||
"integrity": "sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A==",
|
||||
"version": "6.1.11",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",
|
||||
"integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chownr": "^2.0.0",
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
"prepare": "husky install"
|
||||
},
|
||||
"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/react-core": "4.152.4",
|
||||
"@patternfly/react-icons": "4.11.14",
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
import { CheckCircleIcon, TableIcon } from "@patternfly/react-icons";
|
||||
|
||||
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 { FlowParams } from "./routes/Flow";
|
||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||
|
@ -22,14 +23,23 @@ import { EmptyExecutionState } from "./EmptyExecutionState";
|
|||
import { toUpperCase } from "../util";
|
||||
import { FlowHeader } from "./components/FlowHeader";
|
||||
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 { useAlerts } from "../components/alert/Alerts";
|
||||
|
||||
export const providerConditionFilter = (
|
||||
value: AuthenticationProviderRepresentation
|
||||
) => value.displayName?.startsWith("Condition ");
|
||||
|
||||
export const FlowDetails = () => {
|
||||
const { t } = useTranslation("authentication");
|
||||
const adminClient = useAdminClient();
|
||||
const { addAlert } = useAlerts();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const { id, usedBy, builtIn } = useParams<FlowParams>();
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
|
@ -94,12 +104,7 @@ export const FlowDetails = () => {
|
|||
refresh();
|
||||
addAlert(t("updateFlowSuccess"), AlertVariant.success);
|
||||
} catch (error: any) {
|
||||
addAlert(
|
||||
t("updateFlowError", {
|
||||
error: error.response?.data?.errorMessage || error,
|
||||
}),
|
||||
AlertVariant.danger
|
||||
);
|
||||
addError("authentication:updateFlowError", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -114,12 +119,23 @@ export const FlowDetails = () => {
|
|||
refresh();
|
||||
addAlert(t("updateFlowSuccess"), AlertVariant.success);
|
||||
} catch (error: any) {
|
||||
addAlert(
|
||||
t("updateFlowError", {
|
||||
error: error.response?.data?.errorMessage || error,
|
||||
}),
|
||||
AlertVariant.danger
|
||||
);
|
||||
addError("authentication:updateFlowError", error);
|
||||
}
|
||||
};
|
||||
|
||||
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());
|
||||
}}
|
||||
onRowChange={update}
|
||||
onAddExecution={addExecution}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
|
91
src/authentication/components/EditFlowDropdown.tsx
Normal file
91
src/authentication/components/EditFlowDropdown.tsx
Normal 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)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -26,6 +26,7 @@ import { EndSubFlowNode, StartSubFlowNode } from "./diagram/SubFlowNode";
|
|||
import { ConditionalNode } from "./diagram/ConditionalNode";
|
||||
import { ButtonEdge } from "./diagram/ButtonEdge";
|
||||
import { getLayoutedElements } from "./diagram/auto-layout";
|
||||
import { providerConditionFilter } from "../FlowDetails";
|
||||
|
||||
import "./flow-diagram.css";
|
||||
|
||||
|
@ -54,7 +55,7 @@ const createNode = (ex: ExpandableExecution) => {
|
|||
if (ex.executionList) {
|
||||
nodeType = "startSubFlow";
|
||||
}
|
||||
if (ex.displayName?.startsWith("Condition")) {
|
||||
if (providerConditionFilter(ex)) {
|
||||
nodeType = "conditional";
|
||||
}
|
||||
return {
|
||||
|
|
|
@ -13,26 +13,34 @@ import {
|
|||
} from "@patternfly/react-core";
|
||||
|
||||
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 { FlowTitle } from "./FlowTitle";
|
||||
import { FlowRequirementDropdown } from "./FlowRequirementDropdown";
|
||||
import { ExecutionConfigModal } from "./ExecutionConfigModal";
|
||||
import { EditFlowDropdown } from "./EditFlowDropdown";
|
||||
|
||||
import "./flow-row.css";
|
||||
import { ExecutionConfigModal } from "./ExecutionConfigModal";
|
||||
|
||||
type FlowRowProps = {
|
||||
execution: ExpandableExecution;
|
||||
onRowClick: (execution: ExpandableExecution) => void;
|
||||
onRowChange: (execution: AuthenticationExecutionInfoRepresentation) => void;
|
||||
onAddExecution: (
|
||||
execution: ExpandableExecution,
|
||||
type: AuthenticationProviderRepresentation
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const FlowRow = ({
|
||||
execution,
|
||||
onRowClick,
|
||||
onRowChange,
|
||||
onAddExecution,
|
||||
}: FlowRowProps) => {
|
||||
const { t } = useTranslation("authentication");
|
||||
const hasSubList = !!execution.executionList?.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataListItem
|
||||
|
@ -86,6 +94,12 @@ export const FlowRow = ({
|
|||
{execution.configurable && (
|
||||
<ExecutionConfigModal execution={execution} />
|
||||
)}
|
||||
{execution.authenticationFlow && (
|
||||
<EditFlowDropdown
|
||||
execution={execution}
|
||||
onAddExecution={onAddExecution}
|
||||
/>
|
||||
)}
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
|
@ -99,6 +113,7 @@ export const FlowRow = ({
|
|||
execution={execution}
|
||||
onRowClick={onRowClick}
|
||||
onRowChange={onRowChange}
|
||||
onAddExecution={onAddExecution}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
|
142
src/authentication/components/modals/AddStepModal.tsx
Normal file
142
src/authentication/components/modals/AddStepModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
113
src/authentication/components/modals/AddSubFlowModal.tsx
Normal file
113
src/authentication/components/modals/AddSubFlowModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -41,6 +41,9 @@ export default {
|
|||
addExecution: "Add execution",
|
||||
addSubFlowTitle: "Add a sub-flow",
|
||||
addSubFlow: "Add sub-flow",
|
||||
addCondition: "Add condition",
|
||||
addStep: "Add step",
|
||||
addStepTo: "Add step to {{name}}",
|
||||
steps: "Steps",
|
||||
requirement: "Requirement",
|
||||
requirements: {
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
SplitItem,
|
||||
} from "@patternfly/react-core";
|
||||
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 { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
|
||||
|
@ -29,11 +30,6 @@ import { ClientSecret } from "./ClientSecret";
|
|||
import { SignedJWT } from "./SignedJWT";
|
||||
import { X509 } from "./X509";
|
||||
|
||||
type ClientAuthenticatorProviders = {
|
||||
id: string;
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
type AccessToken = {
|
||||
registrationAccessToken: string;
|
||||
};
|
||||
|
@ -48,9 +44,9 @@ export const Credentials = ({ clientId, save }: CredentialsProps) => {
|
|||
const adminClient = useAdminClient();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
|
||||
const [providers, setProviders] = useState<ClientAuthenticatorProviders[]>(
|
||||
[]
|
||||
);
|
||||
const [providers, setProviders] = useState<
|
||||
AuthenticationProviderRepresentation[]
|
||||
>([]);
|
||||
|
||||
const {
|
||||
control,
|
||||
|
@ -69,9 +65,7 @@ export const Credentials = ({ clientId, save }: CredentialsProps) => {
|
|||
useFetch(
|
||||
async () => {
|
||||
const providers =
|
||||
await adminClient.authenticationManagement.getClientAuthenticatorProviders(
|
||||
{ id: clientId }
|
||||
);
|
||||
await adminClient.authenticationManagement.getClientAuthenticatorProviders();
|
||||
|
||||
const secret = await adminClient.clients.getClientSecret({
|
||||
id: clientId,
|
||||
|
|
Loading…
Reference in a new issue