From cc31f0853cbb0adef5c595353d0267fe71bcd54e Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Mon, 9 Aug 2021 10:47:34 +0200 Subject: [PATCH] Initial version of the authentication section (#887) * initial version of create authentication screen * initial version of authentication details * added flow details labels to view header * not in use fix * create execution tree * fixed collapsable row layout * fix drag and drop expand * fix merge error * move to modal * diff and post drag and drop changes * fixed locating the parent row * move "live text" for d&d to common messages * firefox fix * initial version of the diagram * use dagre to layout automatically * moved to sperate file * conditional node * now renders subflows sequential * changed to render sequential or parallel flows * fixed render of sub flows * added button edge, drawer and selectable nodes * add requirement dropdown * also do move so we can merge * also do move so we can merge * fixed merge * added refresh * change requirement * fixed merge error * now uses the new routes * Split out routes into multiple files * Update src/authentication/AuthenticationSection.tsx Co-authored-by: Jon Koops * Update src/authentication/FlowDetails.tsx Co-authored-by: Jon Koops * Update src/authentication/FlowDetails.tsx Co-authored-by: Jon Koops * Update src/authentication/FlowDetails.tsx Co-authored-by: Jon Koops * Update src/authentication/FlowDetails.tsx Co-authored-by: Jon Koops * fixed labels * merge fix * make execution of these parrallel * added some tests * Update src/authentication/components/FlowRequirementDropdown.tsx Co-authored-by: Jon Koops * more review changes * fixed merge error Co-authored-by: Jon Koops --- .../integration/authentication_test.spec.ts | 59 ++ .../authentication/DuplicateFlowModal.ts | 15 + .../manage/authentication/FlowDetail.ts | 39 + package-lock.json | 883 +++++++++++++++++- package.json | 3 + src/authentication/AuthenticationSection.tsx | 51 +- src/authentication/DuplicateFlowModal.tsx | 53 +- src/authentication/EmptyExecutionState.tsx | 45 + src/authentication/FlowDetails.tsx | 242 +++++ .../__tests__/ExecutionList.test.tsx | 150 +++ src/authentication/authentication-section.css | 4 + src/authentication/components/FlowDiagram.tsx | 264 ++++++ src/authentication/components/FlowHeader.tsx | 33 + .../components/FlowRequirementDropdown.tsx | 48 + src/authentication/components/FlowRow.tsx | 102 ++ src/authentication/components/FlowTitle.tsx | 20 + .../components/diagram/ButtonEdge.tsx | 91 ++ .../components/diagram/ConditionalNode.tsx | 26 + .../components/diagram/SubFlowNode.tsx | 40 + .../components/diagram/auto-layout.ts | 41 + .../components/flow-diagram.css | 52 ++ src/authentication/components/flow-header.css | 7 + src/authentication/components/flow-row.css | 55 ++ src/authentication/components/flow-title.css | 9 + src/authentication/empty-execution-state.css | 11 + src/authentication/execution-model.ts | 143 +++ src/authentication/form/CreateFlow.tsx | 81 ++ src/authentication/form/FlowType.tsx | 62 ++ src/authentication/form/NameDescription.tsx | 81 ++ src/authentication/help.ts | 13 + src/authentication/messages.ts | 28 + src/authentication/routes.ts | 4 +- src/authentication/routes/CreateFlow.ts | 19 + src/authentication/routes/Flow.ts | 22 + src/client-scopes/details/MappingDetails.tsx | 2 +- src/client-scopes/form/ClientScopeForm.tsx | 2 +- src/clients/ClientDetails.tsx | 2 +- src/common-help.ts | 2 + src/common-messages.ts | 6 + src/components/view-header/ViewHeader.tsx | 42 +- src/i18n.ts | 2 + src/identity-providers/ManageOrderDialog.tsx | 8 +- src/identity-providers/messages.ts | 4 - src/realm-roles/RealmRoleTabs.tsx | 8 +- src/realm-settings/KeysProvidersTab.tsx | 8 +- src/realm-settings/messages.ts | 4 - 46 files changed, 2781 insertions(+), 105 deletions(-) create mode 100644 cypress/integration/authentication_test.spec.ts create mode 100644 cypress/support/pages/admin_console/manage/authentication/DuplicateFlowModal.ts create mode 100644 cypress/support/pages/admin_console/manage/authentication/FlowDetail.ts create mode 100644 src/authentication/EmptyExecutionState.tsx create mode 100644 src/authentication/FlowDetails.tsx create mode 100644 src/authentication/__tests__/ExecutionList.test.tsx create mode 100644 src/authentication/components/FlowDiagram.tsx create mode 100644 src/authentication/components/FlowHeader.tsx create mode 100644 src/authentication/components/FlowRequirementDropdown.tsx create mode 100644 src/authentication/components/FlowRow.tsx create mode 100644 src/authentication/components/FlowTitle.tsx create mode 100644 src/authentication/components/diagram/ButtonEdge.tsx create mode 100644 src/authentication/components/diagram/ConditionalNode.tsx create mode 100644 src/authentication/components/diagram/SubFlowNode.tsx create mode 100644 src/authentication/components/diagram/auto-layout.ts create mode 100644 src/authentication/components/flow-diagram.css create mode 100644 src/authentication/components/flow-header.css create mode 100644 src/authentication/components/flow-row.css create mode 100644 src/authentication/components/flow-title.css create mode 100644 src/authentication/empty-execution-state.css create mode 100644 src/authentication/execution-model.ts create mode 100644 src/authentication/form/CreateFlow.tsx create mode 100644 src/authentication/form/FlowType.tsx create mode 100644 src/authentication/form/NameDescription.tsx create mode 100644 src/authentication/help.ts create mode 100644 src/authentication/routes/CreateFlow.ts create mode 100644 src/authentication/routes/Flow.ts diff --git a/cypress/integration/authentication_test.spec.ts b/cypress/integration/authentication_test.spec.ts new file mode 100644 index 0000000000..8ec680ae6d --- /dev/null +++ b/cypress/integration/authentication_test.spec.ts @@ -0,0 +1,59 @@ +import { keycloakBefore } from "../support/util/keycloak_before"; +import LoginPage from "../support/pages/LoginPage"; +import SidebarPage from "../support/pages/admin_console/SidebarPage"; +import Masthead from "../support/pages/admin_console/Masthead"; +import ListingPage from "../support/pages/admin_console/ListingPage"; +import DuplicateFlowModal from "../support/pages/admin_console/manage/authentication/DuplicateFlowModal"; +import FlowDetails from "../support/pages/admin_console/manage/authentication/FlowDetail"; + +describe("Authentication test", () => { + const loginPage = new LoginPage(); + const masthead = new Masthead(); + const sidebarPage = new SidebarPage(); + const listingPage = new ListingPage(); + + const detailPage = new FlowDetails(); + + beforeEach(function () { + keycloakBefore(); + loginPage.logIn(); + sidebarPage.goToAuthentication(); + }); + + it("should create duplicate of existing flow", () => { + const modalDialog = new DuplicateFlowModal(); + listingPage.clickRowDetails("Browser").clickDetailMenu("Duplicate"); + modalDialog.fill("Copy of browser"); + + masthead.checkNotificationMessage("Flow successfully duplicated"); + listingPage.itemExist("Copy of browser"); + }); + + it("should show the details of a flow as a table", () => { + listingPage.goToItemDetails("Copy of browser"); + + detailPage.executionExists("Cookie"); + }); + + it("should move kerberos down", () => { + listingPage.goToItemDetails("Copy of browser"); + + detailPage.moveRowTo("Kerberos", "Identity Provider Redirector"); + }); + + it("should change requirement of cookie", () => { + listingPage.goToItemDetails("Copy of browser"); + + detailPage.changeRequirement("Cookie", "Required"); + + masthead.checkNotificationMessage("Flow successfully updated"); + }); + + it("should switch to diagram mode", () => { + listingPage.goToItemDetails("Copy of browser"); + + detailPage.goToDiagram(); + + cy.get(".react-flow").should("exist"); + }); +}); diff --git a/cypress/support/pages/admin_console/manage/authentication/DuplicateFlowModal.ts b/cypress/support/pages/admin_console/manage/authentication/DuplicateFlowModal.ts new file mode 100644 index 0000000000..3e2728c59a --- /dev/null +++ b/cypress/support/pages/admin_console/manage/authentication/DuplicateFlowModal.ts @@ -0,0 +1,15 @@ +export default class DuplicateFlowModal { + private aliasInput = "alias"; + private descriptionInput = "description"; + private confirmButton = "confirm"; + + fill(name?: string, description?: string) { + if (name) { + cy.getId(this.aliasInput).type(name); + if (description) cy.get(this.descriptionInput).type(description); + } + + cy.getId(this.confirmButton).click(); + return this; + } +} diff --git a/cypress/support/pages/admin_console/manage/authentication/FlowDetail.ts b/cypress/support/pages/admin_console/manage/authentication/FlowDetail.ts new file mode 100644 index 0000000000..36679d013b --- /dev/null +++ b/cypress/support/pages/admin_console/manage/authentication/FlowDetail.ts @@ -0,0 +1,39 @@ +type RequirementType = "Required" | "Alternative" | "Disabled" | "Conditional"; + +export default class FlowDetails { + executionExists(name: string) { + this.getExecution(name).should("exist"); + return this; + } + + private getExecution(name: string) { + return cy.getId(name); + } + + moveRowTo(from: string, to: string) { + cy.getId(from).trigger("dragstart").trigger("dragleave"); + + cy.getId(to) + .trigger("dragenter") + .trigger("dragover") + .trigger("drop") + .trigger("dragend"); + + return this; + } + + changeRequirement(execution: string, requirement: RequirementType) { + this.getExecution(execution) + .parentsUntil(".keycloak__authentication__flow-row") + .find(".keycloak__authentication__requirement-dropdown") + .click() + .contains(requirement) + .click(); + return this; + } + + goToDiagram() { + cy.get("#diagramView").click(); + return this; + } +} diff --git a/package-lock.json b/package-lock.json index 768d6db91a..5805b89081 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "keycloak-admin-ui", "version": "0.0.1", "license": "Apache", "dependencies": { @@ -12,6 +13,7 @@ "@patternfly/react-core": "4.147.0", "@patternfly/react-icons": "4.11.8", "@patternfly/react-table": "4.29.37", + "dagre": "^0.8.5", "file-saver": "^2.0.5", "i18next": "^20.3.5", "keycloak-admin": "^1.14.20", @@ -20,6 +22,7 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-error-boundary": "^3.1.3", + "react-flow-renderer": "^9.6.4", "react-hook-form": "^6.15.8", "react-i18next": "^11.11.4", "react-router-dom": "^5.2.0", @@ -33,6 +36,7 @@ "@snowpack/plugin-typescript": "^1.2.1", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.0.0", + "@types/dagre": "^0.7.45", "@types/file-saver": "^2.0.3", "@types/jest": "^26.0.24", "@types/lodash": "^4.14.172", @@ -1365,8 +1369,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.14.6", - "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", + "version": "7.14.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz", + "integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==", "dependencies": { "regenerator-runtime": "^0.13.4" }, @@ -3256,6 +3261,234 @@ "@types/responselike": "*" } }, + "node_modules/@types/d3": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.0.0.tgz", + "integrity": "sha512-7rMMuS5unvbvFCJXAkQXIxWTo2OUlmVXN5q7sfQFesuVICY55PSP6hhbUhWjTTNpfTTB3iLALsIYDFe7KUNABw==", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.1.tgz", + "integrity": "sha512-D/G7oG0czeszALrkdUiV68CDiHDxXf+M2mLVqAyKktGd12VKQQljj1sHJGBKjcK4jRH1biBd6ZPQPHpJ0mNa0w==" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.1.tgz", + "integrity": "sha512-zji/iIbdd49g9WN0aIsGcwcTBUkgLsCSwB+uH+LPVDAiKWENMtI3cJEWt+7/YYwelMoZmbBfzA3qCdrZ2XFNnw==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.1.tgz", + "integrity": "sha512-B532DozsiTuQMHu2YChdZU0qsFJSio3Q6jmBYGYNp3gMDzBmuFFgPt9qKA4VYuLZMp4qc6eX7IUFUEsvHiXZAw==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-eQfcxIHrg7V++W8Qxn6QkqBNBokyhdWSAS73AbkbMzvLQmVVBviknoz2SRS/ZJdIOmhcmmdCRE/NFOm28Z1AMw==" + }, + "node_modules/@types/d3-color": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.0.2.tgz", + "integrity": "sha512-WVx6zBiz4sWlboCy7TCgjeyHpNjMsoF36yaagny1uXfbadc9f+5BeBf7U+lRmQqY3EHbGQpP8UdW8AC+cywSwQ==" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.1.tgz", + "integrity": "sha512-C3zfBrhHZvrpAAK3YXqLWVAGo87A4SvJ83Q/zVJ8rFWJdKejUnDYaWZPkA8K84kb2vDA/g90LTQAz7etXcgoQQ==", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.0.tgz", + "integrity": "sha512-iGm7ZaGLq11RK3e69VeMM6Oqj2SjKUB9Qhcyd1zIcqn2uE8w9GFB445yCY46NOQO3ByaNyktX1DK+Etz7ZaX+w==" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-NhxMn3bAkqhjoxabVJWKryhnZXXYYVQxaBnbANu0O94+O/nX9qSjrA1P1jbAQJxJf+VC72TxDX/YJcKue5bRqw==" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.1.tgz", + "integrity": "sha512-o1Va7bLwwk6h03+nSM8dpaGEYnoIG19P0lKqlic8Un36ymh9NSkNFX1yiXMKNMx8rJ0Kfnn2eovuFaL6Jvj0zA==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.0.tgz", + "integrity": "sha512-o0/7RlMl9p5n6FQDptuJVMxDf/7EDEv2SYEO/CwdG2tr1hTfUVi0Iavkk2ax+VpaQ/1jVhpnj5rq1nj8vwhn2A==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz", + "integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-toZJNOwrOIqz7Oh6Q7l2zkaNfXkfR7mFSJvGvlD/Ciq/+SQ39d5gynHJZ/0fjt83ec3WL7+u3ssqIijQtBISsw==", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.3.tgz", + "integrity": "sha512-z8GteGVfkWJMKsx6hwC3SiTSLspL98VNpmvLpEFJQpZPq6xpA1I8HNBDNSpukfK0Vb0l64zGFhzunLgEAcBWSA==" + }, + "node_modules/@types/d3-format": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", + "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==" + }, + "node_modules/@types/d3-geo": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.2.tgz", + "integrity": "sha512-DbqK7MLYA8LpyHQfv6Klz0426bQEf7bRTvhMy44sNGVyZoWn//B0c+Qbeg8Osi2Obdc9BLLXYAKpyWege2/7LQ==", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.0.2.tgz", + "integrity": "sha512-+krnrWOZ+aQB6v+E+jEkmkAx9HvsNAD+1LCD0vlBY3t+HwjKnsBFbpVLx6WWzDzCIuiTWdAxXMEnGnVXpB09qQ==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz", + "integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz", + "integrity": "sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz", + "integrity": "sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==" + }, + "node_modules/@types/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.1.tgz", + "integrity": "sha512-GDuXcRcR6mKcpUVMhPNttpOzHi2dP6YcDqLZYSZHgwTZ+sfCa8e9q0VEBwZomblAPNMYpVqxojnSyIEb4s/Pwg==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.1.tgz", + "integrity": "sha512-aJ1d1SCUtERHH65bB8NNoLpUOI3z8kVcfg2BGm4rMMUwuZF4x6qnIEKjT60Vt0o7gP/a/xkRVs4D9CpDifbyRA==" + }, + "node_modules/@types/d3-shape": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.0.2.tgz", + "integrity": "sha512-5+ButCmIfNX8id5seZ7jKj3igdcxx+S9IDBiT35fQGTLZUfkFgTv+oBH34xgeoWDKpWcMITSzBILWQtBoN5Piw==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz", + "integrity": "sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz", + "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-Sv4qEI9uq3bnZwlOANvYK853zvpdKEm1yz9rcc8ZTsxvRklcs9Fx4YFuGA3gXoQN/c/1T6QkVNjhaRO/cWj94g==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.1.tgz", + "integrity": "sha512-7s5L9TjfqIYQmQQEUcpMAcBOahem7TRoSO/+Gkz02GbMVuULiZzjF2BOdw291dbO2aNon4m2OdFsRGaCq2caLQ==", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/dagre": { + "version": "0.7.46", + "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.46.tgz", + "integrity": "sha512-ku3y+F8sPqmiB5Ugl22NpukI2dLAViJiWwdtueXLeuF4fxZozl+bytPSFVlLu/gDgaKiwobu3LBXXRWktIMiIA==", + "dev": true + }, "node_modules/@types/estree": { "version": "0.0.39", "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", @@ -3266,6 +3499,11 @@ "integrity": "sha512-MBIou8pd/41jkff7s97B47bc9+p0BszqqDJsO51yDm49uUxeKzrfuNl5fSLC6BpLEWKA8zlwyqALVmXrFwoBHQ==", "dev": true }, + "node_modules/@types/geojson": { + "version": "7946.0.8", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz", + "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==" + }, "node_modules/@types/graceful-fs": { "version": "4.1.5", "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", @@ -3279,6 +3517,15 @@ "integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==", "dev": true }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", @@ -3442,14 +3689,12 @@ }, "node_modules/@types/prop-types": { "version": "15.7.3", - "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", - "dev": true + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" }, "node_modules/@types/react": { "version": "17.0.16", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.16.tgz", "integrity": "sha512-3kCUiOOlQTwUUvjNFkbBTWMTxdTGybz/PfjCw9JmaRGcEDBQh+nGMg7/E9P2rklhJuYVd25IYLNcvqgSPCPksg==", - "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3464,6 +3709,17 @@ "@types/react": "*" } }, + "node_modules/@types/react-redux": { + "version": "7.1.18", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.18.tgz", + "integrity": "sha512-9iwAsPyJ9DLTRH+OFeIrm9cAbIj1i2ANL3sKQFATqnPWRbg+jEFXyZOKHiQK/N86pNRXbb4HRxAxo0SIX1XwzQ==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "node_modules/@types/react-router": { "version": "5.1.15", "integrity": "sha512-z3UlMG/x91SFEVmmvykk9FLTliDvfdIUky4k2rCfXWQ0NKbrP8o9BTCaCTPuYsB8gDkUnUmkcA2vYlm2DR+HAA==", @@ -3485,8 +3741,7 @@ }, "node_modules/@types/react/node_modules/csstype": { "version": "3.0.8", - "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==", - "dev": true + "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==" }, "node_modules/@types/resolve": { "version": "1.17.1", @@ -3508,8 +3763,7 @@ }, "node_modules/@types/scheduler": { "version": "0.16.2", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "dev": true + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, "node_modules/@types/sinonjs__fake-timers": { "version": "6.0.3", @@ -5360,6 +5614,16 @@ "node": ">=0.10.0" } }, + "node_modules/classcat": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.3.tgz", + "integrity": "sha512-6dK2ke4VEJZOFx2ZfdDAl5OhEL8lvkl6EHF92IfRePfHxQTqir5NlcNVUv+2idjDqCX2NDc8m8YSAI5NI975ZQ==" + }, + "node_modules/classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, "node_modules/clean-stack": { "version": "2.2.0", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", @@ -5957,6 +6221,111 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/d3-color": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.0.1.tgz", + "integrity": "sha512-6/SlHkDOBLyQSJ1j1Ghs82OIUXpKWlR0hCsw0XrLSQhuUPuCSmLQ1QPH98vpnQxMUQM2/gfAkUEWsupVpd9JGw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, "node_modules/dashdash": { "version": "1.14.1", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", @@ -7579,8 +7948,7 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-diff": { "version": "1.2.0", @@ -8110,6 +8478,14 @@ "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", "dev": true }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "dependencies": { + "lodash": "^4.17.15" + } + }, "node_modules/growly": { "version": "1.3.0", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", @@ -13852,6 +14228,15 @@ "react": "17.0.2" } }, + "node_modules/react-draggable": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.3.tgz", + "integrity": "sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w==", + "dependencies": { + "classnames": "^2.2.5", + "prop-types": "^15.6.0" + } + }, "node_modules/react-dropzone": { "version": "9.0.0", "integrity": "sha512-wZ2o9B2qkdE3RumWhfyZT9swgJYJPeU5qHEcMU8weYpmLex1eeWX0CC32/Y0VutB+BBi2D+iePV/YZIiB4kZGw==", @@ -13882,6 +14267,27 @@ "react": ">=16.13.1" } }, + "node_modules/react-flow-renderer": { + "version": "9.6.6", + "resolved": "https://registry.npmjs.org/react-flow-renderer/-/react-flow-renderer-9.6.6.tgz", + "integrity": "sha512-cRlA7mweva/+ucVVde2zsdiktxm1prML4FJYAg7z9V9PjWCoxRIzEewG99aCIoPD0yF2nuuYLRQf+8CvPLn8og==", + "dependencies": { + "@babel/runtime": "^7.14.8", + "@types/d3": "^7.0.0", + "@types/react-redux": "^7.1.18", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "fast-deep-equal": "^3.1.3", + "react-draggable": "^4.4.3", + "react-redux": "^7.2.4", + "redux": "^4.1.0" + }, + "peerDependencies": { + "react": "16 || 17", + "react-dom": "16 || 17" + } + }, "node_modules/react-hook-form": { "version": "6.15.8", "integrity": "sha512-prq82ofMbnRyj5wqDe8hsTRcdR25jQ+B8KtCS7BLCzjFHAwNuCjRwzPuP4eYLsEBjEIeYd6try+pdLdw0kPkpg==", @@ -13907,6 +14313,35 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, + "node_modules/react-redux": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.4.tgz", + "integrity": "sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/react-redux": "^7.1.16", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.13.1" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/react-refresh": { "version": "0.9.0", "integrity": "sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==", @@ -14131,6 +14566,14 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", + "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/regenerate": { "version": "1.4.2", "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", @@ -17929,8 +18372,9 @@ } }, "@babel/runtime": { - "version": "7.14.6", - "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", + "version": "7.14.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz", + "integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==", "requires": { "regenerator-runtime": "^0.13.4" } @@ -19410,6 +19854,234 @@ "@types/responselike": "*" } }, + "@types/d3": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.0.0.tgz", + "integrity": "sha512-7rMMuS5unvbvFCJXAkQXIxWTo2OUlmVXN5q7sfQFesuVICY55PSP6hhbUhWjTTNpfTTB3iLALsIYDFe7KUNABw==", + "requires": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "@types/d3-array": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.1.tgz", + "integrity": "sha512-D/G7oG0czeszALrkdUiV68CDiHDxXf+M2mLVqAyKktGd12VKQQljj1sHJGBKjcK4jRH1biBd6ZPQPHpJ0mNa0w==" + }, + "@types/d3-axis": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.1.tgz", + "integrity": "sha512-zji/iIbdd49g9WN0aIsGcwcTBUkgLsCSwB+uH+LPVDAiKWENMtI3cJEWt+7/YYwelMoZmbBfzA3qCdrZ2XFNnw==", + "requires": { + "@types/d3-selection": "*" + } + }, + "@types/d3-brush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.1.tgz", + "integrity": "sha512-B532DozsiTuQMHu2YChdZU0qsFJSio3Q6jmBYGYNp3gMDzBmuFFgPt9qKA4VYuLZMp4qc6eX7IUFUEsvHiXZAw==", + "requires": { + "@types/d3-selection": "*" + } + }, + "@types/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-eQfcxIHrg7V++W8Qxn6QkqBNBokyhdWSAS73AbkbMzvLQmVVBviknoz2SRS/ZJdIOmhcmmdCRE/NFOm28Z1AMw==" + }, + "@types/d3-color": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.0.2.tgz", + "integrity": "sha512-WVx6zBiz4sWlboCy7TCgjeyHpNjMsoF36yaagny1uXfbadc9f+5BeBf7U+lRmQqY3EHbGQpP8UdW8AC+cywSwQ==" + }, + "@types/d3-contour": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.1.tgz", + "integrity": "sha512-C3zfBrhHZvrpAAK3YXqLWVAGo87A4SvJ83Q/zVJ8rFWJdKejUnDYaWZPkA8K84kb2vDA/g90LTQAz7etXcgoQQ==", + "requires": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "@types/d3-delaunay": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.0.tgz", + "integrity": "sha512-iGm7ZaGLq11RK3e69VeMM6Oqj2SjKUB9Qhcyd1zIcqn2uE8w9GFB445yCY46NOQO3ByaNyktX1DK+Etz7ZaX+w==" + }, + "@types/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-NhxMn3bAkqhjoxabVJWKryhnZXXYYVQxaBnbANu0O94+O/nX9qSjrA1P1jbAQJxJf+VC72TxDX/YJcKue5bRqw==" + }, + "@types/d3-drag": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.1.tgz", + "integrity": "sha512-o1Va7bLwwk6h03+nSM8dpaGEYnoIG19P0lKqlic8Un36ymh9NSkNFX1yiXMKNMx8rJ0Kfnn2eovuFaL6Jvj0zA==", + "requires": { + "@types/d3-selection": "*" + } + }, + "@types/d3-dsv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.0.tgz", + "integrity": "sha512-o0/7RlMl9p5n6FQDptuJVMxDf/7EDEv2SYEO/CwdG2tr1hTfUVi0Iavkk2ax+VpaQ/1jVhpnj5rq1nj8vwhn2A==" + }, + "@types/d3-ease": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz", + "integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==" + }, + "@types/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-toZJNOwrOIqz7Oh6Q7l2zkaNfXkfR7mFSJvGvlD/Ciq/+SQ39d5gynHJZ/0fjt83ec3WL7+u3ssqIijQtBISsw==", + "requires": { + "@types/d3-dsv": "*" + } + }, + "@types/d3-force": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.3.tgz", + "integrity": "sha512-z8GteGVfkWJMKsx6hwC3SiTSLspL98VNpmvLpEFJQpZPq6xpA1I8HNBDNSpukfK0Vb0l64zGFhzunLgEAcBWSA==" + }, + "@types/d3-format": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", + "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==" + }, + "@types/d3-geo": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.2.tgz", + "integrity": "sha512-DbqK7MLYA8LpyHQfv6Klz0426bQEf7bRTvhMy44sNGVyZoWn//B0c+Qbeg8Osi2Obdc9BLLXYAKpyWege2/7LQ==", + "requires": { + "@types/geojson": "*" + } + }, + "@types/d3-hierarchy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.0.2.tgz", + "integrity": "sha512-+krnrWOZ+aQB6v+E+jEkmkAx9HvsNAD+1LCD0vlBY3t+HwjKnsBFbpVLx6WWzDzCIuiTWdAxXMEnGnVXpB09qQ==" + }, + "@types/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "requires": { + "@types/d3-color": "*" + } + }, + "@types/d3-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz", + "integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==" + }, + "@types/d3-polygon": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz", + "integrity": "sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==" + }, + "@types/d3-quadtree": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz", + "integrity": "sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==" + }, + "@types/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==" + }, + "@types/d3-scale": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.1.tgz", + "integrity": "sha512-GDuXcRcR6mKcpUVMhPNttpOzHi2dP6YcDqLZYSZHgwTZ+sfCa8e9q0VEBwZomblAPNMYpVqxojnSyIEb4s/Pwg==", + "requires": { + "@types/d3-time": "*" + } + }, + "@types/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==" + }, + "@types/d3-selection": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.1.tgz", + "integrity": "sha512-aJ1d1SCUtERHH65bB8NNoLpUOI3z8kVcfg2BGm4rMMUwuZF4x6qnIEKjT60Vt0o7gP/a/xkRVs4D9CpDifbyRA==" + }, + "@types/d3-shape": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.0.2.tgz", + "integrity": "sha512-5+ButCmIfNX8id5seZ7jKj3igdcxx+S9IDBiT35fQGTLZUfkFgTv+oBH34xgeoWDKpWcMITSzBILWQtBoN5Piw==", + "requires": { + "@types/d3-path": "*" + } + }, + "@types/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==" + }, + "@types/d3-time-format": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz", + "integrity": "sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==" + }, + "@types/d3-timer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz", + "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==" + }, + "@types/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-Sv4qEI9uq3bnZwlOANvYK853zvpdKEm1yz9rcc8ZTsxvRklcs9Fx4YFuGA3gXoQN/c/1T6QkVNjhaRO/cWj94g==", + "requires": { + "@types/d3-selection": "*" + } + }, + "@types/d3-zoom": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.1.tgz", + "integrity": "sha512-7s5L9TjfqIYQmQQEUcpMAcBOahem7TRoSO/+Gkz02GbMVuULiZzjF2BOdw291dbO2aNon4m2OdFsRGaCq2caLQ==", + "requires": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "@types/dagre": { + "version": "0.7.46", + "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.46.tgz", + "integrity": "sha512-ku3y+F8sPqmiB5Ugl22NpukI2dLAViJiWwdtueXLeuF4fxZozl+bytPSFVlLu/gDgaKiwobu3LBXXRWktIMiIA==", + "dev": true + }, "@types/estree": { "version": "0.0.39", "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", @@ -19420,6 +20092,11 @@ "integrity": "sha512-MBIou8pd/41jkff7s97B47bc9+p0BszqqDJsO51yDm49uUxeKzrfuNl5fSLC6BpLEWKA8zlwyqALVmXrFwoBHQ==", "dev": true }, + "@types/geojson": { + "version": "7946.0.8", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz", + "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==" + }, "@types/graceful-fs": { "version": "4.1.5", "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", @@ -19433,6 +20110,15 @@ "integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==", "dev": true }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/http-cache-semantics": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", @@ -19574,14 +20260,12 @@ }, "@types/prop-types": { "version": "15.7.3", - "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", - "dev": true + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" }, "@types/react": { "version": "17.0.16", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.16.tgz", "integrity": "sha512-3kCUiOOlQTwUUvjNFkbBTWMTxdTGybz/PfjCw9JmaRGcEDBQh+nGMg7/E9P2rklhJuYVd25IYLNcvqgSPCPksg==", - "dev": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -19590,8 +20274,7 @@ "dependencies": { "csstype": { "version": "3.0.8", - "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==", - "dev": true + "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==" } } }, @@ -19603,6 +20286,17 @@ "@types/react": "*" } }, + "@types/react-redux": { + "version": "7.1.18", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.18.tgz", + "integrity": "sha512-9iwAsPyJ9DLTRH+OFeIrm9cAbIj1i2ANL3sKQFATqnPWRbg+jEFXyZOKHiQK/N86pNRXbb4HRxAxo0SIX1XwzQ==", + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "@types/react-router": { "version": "5.1.15", "integrity": "sha512-z3UlMG/x91SFEVmmvykk9FLTliDvfdIUky4k2rCfXWQ0NKbrP8o9BTCaCTPuYsB8gDkUnUmkcA2vYlm2DR+HAA==", @@ -19642,8 +20336,7 @@ }, "@types/scheduler": { "version": "0.16.2", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "dev": true + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, "@types/sinonjs__fake-timers": { "version": "6.0.3", @@ -21044,6 +21737,16 @@ } } }, + "classcat": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.3.tgz", + "integrity": "sha512-6dK2ke4VEJZOFx2ZfdDAl5OhEL8lvkl6EHF92IfRePfHxQTqir5NlcNVUv+2idjDqCX2NDc8m8YSAI5NI975ZQ==" + }, + "classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, "clean-stack": { "version": "2.2.0", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", @@ -21494,6 +22197,81 @@ } } }, + "d3-color": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.0.1.tgz", + "integrity": "sha512-6/SlHkDOBLyQSJ1j1Ghs82OIUXpKWlR0hCsw0XrLSQhuUPuCSmLQ1QPH98vpnQxMUQM2/gfAkUEWsupVpd9JGw==" + }, + "d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==" + }, + "d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + } + }, + "d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" + }, + "d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "requires": { + "d3-color": "1 - 3" + } + }, + "d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" + }, + "d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" + }, + "d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "requires": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + } + }, + "dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "requires": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, "dashdash": { "version": "1.14.1", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", @@ -22708,8 +23486,7 @@ }, "fast-deep-equal": { "version": "3.1.3", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-diff": { "version": "1.2.0", @@ -23117,6 +23894,14 @@ "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", "dev": true }, + "graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "requires": { + "lodash": "^4.17.15" + } + }, "growly": { "version": "1.3.0", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", @@ -27338,6 +28123,15 @@ "scheduler": "^0.20.2" } }, + "react-draggable": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.3.tgz", + "integrity": "sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w==", + "requires": { + "classnames": "^2.2.5", + "prop-types": "^15.6.0" + } + }, "react-dropzone": { "version": "9.0.0", "integrity": "sha512-wZ2o9B2qkdE3RumWhfyZT9swgJYJPeU5qHEcMU8weYpmLex1eeWX0CC32/Y0VutB+BBi2D+iePV/YZIiB4kZGw==", @@ -27355,6 +28149,23 @@ "@babel/runtime": "^7.12.5" } }, + "react-flow-renderer": { + "version": "9.6.6", + "resolved": "https://registry.npmjs.org/react-flow-renderer/-/react-flow-renderer-9.6.6.tgz", + "integrity": "sha512-cRlA7mweva/+ucVVde2zsdiktxm1prML4FJYAg7z9V9PjWCoxRIzEewG99aCIoPD0yF2nuuYLRQf+8CvPLn8og==", + "requires": { + "@babel/runtime": "^7.14.8", + "@types/d3": "^7.0.0", + "@types/react-redux": "^7.1.18", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "fast-deep-equal": "^3.1.3", + "react-draggable": "^4.4.3", + "react-redux": "^7.2.4", + "redux": "^4.1.0" + } + }, "react-hook-form": { "version": "6.15.8", "integrity": "sha512-prq82ofMbnRyj5wqDe8hsTRcdR25jQ+B8KtCS7BLCzjFHAwNuCjRwzPuP4eYLsEBjEIeYd6try+pdLdw0kPkpg==", @@ -27374,6 +28185,26 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, + "react-redux": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.4.tgz", + "integrity": "sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==", + "requires": { + "@babel/runtime": "^7.12.1", + "@types/react-redux": "^7.1.16", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.13.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, "react-refresh": { "version": "0.9.0", "integrity": "sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==", @@ -27561,6 +28392,14 @@ "strip-indent": "^3.0.0" } }, + "redux": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", + "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, "regenerate": { "version": "1.4.2", "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", diff --git a/package.json b/package.json index 145ba7fa19..7afcb71ce3 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@patternfly/react-core": "4.147.0", "@patternfly/react-icons": "4.11.8", "@patternfly/react-table": "4.29.37", + "dagre": "^0.8.5", "file-saver": "^2.0.5", "i18next": "^20.3.5", "keycloak-admin": "^1.14.20", @@ -36,6 +37,7 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-error-boundary": "^3.1.3", + "react-flow-renderer": "^9.6.4", "react-hook-form": "^6.15.8", "react-i18next": "^11.11.4", "react-router-dom": "^5.2.0", @@ -49,6 +51,7 @@ "@snowpack/plugin-typescript": "^1.2.1", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.0.0", + "@types/dagre": "^0.7.45", "@types/file-saver": "^2.0.3", "@types/jest": "^26.0.24", "@types/lodash": "^4.14.172", diff --git a/src/authentication/AuthenticationSection.tsx b/src/authentication/AuthenticationSection.tsx index 29b50c9b69..463096cb5f 100644 --- a/src/authentication/AuthenticationSection.tsx +++ b/src/authentication/AuthenticationSection.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { Link, useRouteMatch } from "react-router-dom"; +import { Link } from "react-router-dom"; import { Trans, useTranslation } from "react-i18next"; import { AlertVariant, @@ -10,6 +10,7 @@ import { Popover, Tab, TabTitleText, + ToolbarItem, } from "@patternfly/react-core"; import { CheckCircleIcon } from "@patternfly/react-icons"; @@ -24,10 +25,12 @@ import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useAlerts } from "../components/alert/Alerts"; import { toUpperCase } from "../util"; import { DuplicateFlowModal } from "./DuplicateFlowModal"; +import { toCreateFlow } from "./routes/CreateFlow"; +import { toFlow } from "./routes/Flow"; import "./authentication-section.css"; -type UsedBy = "client" | "default" | "idp"; +type UsedBy = "specificClients" | "default" | "specificProviders"; type AuthenticationType = AuthenticationFlowRepresentation & { usedBy: { type?: UsedBy; values: string[] }; @@ -49,7 +52,6 @@ export const AuthenticationSection = () => { const [key, setKey] = useState(0); const refresh = () => setKey(new Date().getTime()); const { addAlert, addError } = useAlerts(); - const { url } = useRouteMatch(); const [selectedFlow, setSelectedFlow] = useState(); const [open, setOpen] = useState(false); @@ -74,7 +76,7 @@ export const AuthenticationSection = () => { client.authenticationFlowBindingOverrides["browser"] === flow.id) ); if (client) { - flow.usedBy.type = "client"; + flow.usedBy.type = "specificClients"; flow.usedBy.values.push(client.clientId!); } @@ -84,7 +86,7 @@ export const AuthenticationSection = () => { idp.postBrokerLoginFlowAlias === flow.alias ); if (idp) { - flow.usedBy.type = "idp"; + flow.usedBy.type = "specificProviders"; flow.usedBy.values.push(idp.alias!); } @@ -121,13 +123,16 @@ export const AuthenticationSection = () => { const UsedBy = ({ id, usedBy: { type, values } }: AuthenticationType) => ( <> - {(type === "idp" || type === "client") && ( + {(type === "specificProviders" || type === "specificClients") && ( - {t("appliedBy" + (type === "client" ? "Clients" : "Providers"))}{" "} + {t( + "appliedBy" + + (type === "specificClients" ? "Clients" : "Providers") + )}{" "} {values.map((used, index) => ( <> {used} @@ -142,7 +147,7 @@ export const AuthenticationSection = () => { className="keycloak_authentication-section__usedby" key={`icon-${id}`} />{" "} - {t("specific" + (type === "client" ? "Clients" : "Providers"))} + {t(type)} )} @@ -163,9 +168,22 @@ export const AuthenticationSection = () => { ); - const AliasRenderer = ({ id, alias, builtIn }: AuthenticationType) => ( + const AliasRenderer = ({ + id, + alias, + usedBy, + builtIn, + }: AuthenticationType) => ( <> - + {toUpperCase(alias!)} {" "} {builtIn && } @@ -198,6 +216,17 @@ export const AuthenticationSection = () => { loader={loader} ariaLabelKey="authentication:title" searchPlaceholderKey="authentication:searchForEvent" + toolbarItem={ + + + + } actionResolver={({ data }) => { const defaultActions = [ { diff --git a/src/authentication/DuplicateFlowModal.tsx b/src/authentication/DuplicateFlowModal.tsx index 79dd4b90e4..f59bcc5c24 100644 --- a/src/authentication/DuplicateFlowModal.tsx +++ b/src/authentication/DuplicateFlowModal.tsx @@ -1,20 +1,18 @@ import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { useForm } from "react-hook-form"; +import { FormProvider, useForm } from "react-hook-form"; import { AlertVariant, Button, ButtonVariant, Form, - FormGroup, Modal, ModalVariant, - TextInput, - ValidatedOptions, } from "@patternfly/react-core"; import { useAdminClient } from "../context/auth/AdminClient"; import { useAlerts } from "../components/alert/Alerts"; +import { NameDescription } from "./form/NameDescription"; type DuplicateFlowModalProps = { name: string; @@ -30,9 +28,10 @@ export const DuplicateFlowModal = ({ onComplete, }: DuplicateFlowModalProps) => { const { t } = useTranslation("authentication"); - const { register, errors, setValue, trigger, getValues } = useForm({ + const form = useForm({ shouldUnregister: false, }); + const { setValue, trigger, getValues } = form; const adminClient = useAdminClient(); const { addAlert, addError } = useAlerts(); @@ -74,10 +73,16 @@ export const DuplicateFlowModal = ({ onClose={toggleDialog} variant={ModalVariant.small} actions={[ - , + + + + ))} + + + ); +}; diff --git a/src/authentication/FlowDetails.tsx b/src/authentication/FlowDetails.tsx new file mode 100644 index 0000000000..6d3488363d --- /dev/null +++ b/src/authentication/FlowDetails.tsx @@ -0,0 +1,242 @@ +import React, { useState } from "react"; +import { useParams } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { + DataList, + Label, + PageSection, + Toolbar, + ToolbarContent, + ToggleGroup, + ToggleGroupItem, + AlertVariant, +} from "@patternfly/react-core"; +import { CheckCircleIcon, TableIcon } from "@patternfly/react-icons"; + +import type AuthenticationExecutionInfoRepresentation from "keycloak-admin/lib/defs/authenticationExecutionInfoRepresentation"; +import type AuthenticationFlowRepresentation from "keycloak-admin/lib/defs/authenticationFlowRepresentation"; +import type { FlowParams } from "./routes/Flow"; +import { ViewHeader } from "../components/view-header/ViewHeader"; +import { useAdminClient, useFetch } from "../context/auth/AdminClient"; +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 { 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(); + const { addAlert } = useAlerts(); + const { id, usedBy, builtIn } = useParams(); + const [key, setKey] = useState(0); + const refresh = () => setKey(new Date().getTime()); + + const [tableView, setTableView] = useState(true); + const [flow, setFlow] = useState(); + const [executionList, setExecutionList] = useState(); + const [dragged, setDragged] = + useState(); + const [liveText, setLiveText] = useState(""); + + useFetch( + async () => { + const flows = await adminClient.authenticationManagement.getFlows(); + const flow = flows.find((f) => f.id === id); + const executions = + await adminClient.authenticationManagement.getExecutions({ + flow: flow?.alias!, + }); + return { flow, executions }; + }, + ({ flow, executions }) => { + setFlow(flow); + setExecutionList(new ExecutionList(executions)); + }, + [key] + ); + + const executeChange = async ( + ex: AuthenticationFlowRepresentation, + change: LevelChange | IndexChange + ) => { + try { + let id = ex.id!; + if ("parent" in change) { + await adminClient.authenticationManagement.delExecution({ id }); + const result = + await adminClient.authenticationManagement.addExecutionToFlow({ + flow: change.parent?.displayName! || flow?.alias!, + provider: ex.providerId!, + }); + id = result.id!; + } + const times = change.newIndex - change.oldIndex; + const requests = []; + for (let index = 0; index < Math.abs(times); index++) { + if (times > 0) { + requests.push( + adminClient.authenticationManagement.lowerPriorityExecution({ + id, + }) + ); + } else { + requests.push( + adminClient.authenticationManagement.raisePriorityExecution({ + id, + }) + ); + } + } + await Promise.all(requests); + refresh(); + addAlert(t("updateFlowSuccess"), AlertVariant.success); + } catch (error) { + addAlert( + t("updateFlowError", { + error: error.response?.data?.errorMessage || error, + }), + AlertVariant.danger + ); + } + }; + + const update = async ( + execution: AuthenticationExecutionInfoRepresentation + ) => { + try { + await adminClient.authenticationManagement.updateExecution( + { flow: flow?.alias! }, + execution + ); + refresh(); + addAlert(t("updateFlowSuccess"), AlertVariant.success); + } catch (error) { + addAlert( + t("updateFlowError", { + error: error.response?.data?.errorMessage || error, + }), + AlertVariant.danger + ); + } + }; + + return ( + <> + {t(usedBy)} }, + builtIn + ? { + text: ( + + ), + id: "builtIn", + } + : {}, + ]} + /> + + {executionList?.expandableList?.length && ( + + + + } + aria-label={t("tableView")} + buttonId="tableView" + isSelected={tableView} + onChange={() => setTableView(true)} + /> + } + aria-label={t("diagramView")} + buttonId="diagramView" + isSelected={!tableView} + onChange={() => setTableView(false)} + /> + + + + )} + {tableView && executionList?.expandableList?.length && ( + <> + { + const withoutHeaderId = order.slice(1); + setLiveText( + t("common:onDragFinish", { list: dragged?.displayName }) + ); + const change = executionList.getChange( + dragged!, + withoutHeaderId + ); + executeChange(dragged!, change); + }} + onDragStart={(id) => { + const item = executionList.findExecution(id)!; + setLiveText( + t("common:onDragStart", { item: item.displayName }) + ); + setDragged(item); + if (item.executionList && !item.isCollapsed) { + item.isCollapsed = true; + setExecutionList(executionList.clone()); + } + }} + onDragMove={() => + setLiveText( + t("common:onDragMove", { item: dragged?.displayName }) + ) + } + onDragCancel={() => setLiveText(t("common:onDragCancel"))} + itemOrder={[ + "header", + ...executionList.order().map((ex) => ex.id!), + ]} + > + + <> + {executionList.expandableList.map((execution) => ( + { + execution.isCollapsed = !execution.isCollapsed; + setExecutionList(executionList.clone()); + }} + onRowChange={update} + /> + ))} + + +
+ {liveText} +
+ + )} + {!tableView && executionList?.expandableList && ( + + )} + {!executionList?.expandableList || + (executionList.expandableList.length === 0 && ( + + ))} +
+ + ); +}; diff --git a/src/authentication/__tests__/ExecutionList.test.tsx b/src/authentication/__tests__/ExecutionList.test.tsx new file mode 100644 index 0000000000..345054ec00 --- /dev/null +++ b/src/authentication/__tests__/ExecutionList.test.tsx @@ -0,0 +1,150 @@ +import { ExecutionList, IndexChange, LevelChange } from "../execution-model"; + +describe("ExecutionList", () => { + const list2 = new ExecutionList([ + { id: "1", index: 0, level: 0 }, + { id: "2", index: 1, level: 0 }, + { id: "3", index: 0, level: 1 }, + { id: "4", index: 1, level: 1 }, + { id: "5", index: 0, level: 2 }, + { id: "6", index: 1, level: 2 }, + { id: "7", index: 2, level: 0 }, + ]); + + it("Move 1 down to the end", () => { + const diff = list2.getChange({ id: "1" }, [ + "2", + "3", + "4", + "5", + "1", + "6", + "7", + ]); + + expect(diff).toBeInstanceOf(LevelChange); + expect((diff as LevelChange).parent?.id).toBe("4"); + }); + + it("Index change", () => { + const diff = list2.getChange({ id: "5" }, [ + "1", + "2", + "3", + "4", + "6", + "5", + "7", + ]); + + expect(diff).toBeInstanceOf(IndexChange); + expect((diff as IndexChange).newIndex).toBe(1); + expect((diff as IndexChange).oldIndex).toBe(0); + }); + + it("Move 7 down to the top", () => { + const diff = list2.getChange({ id: "7" }, [ + "7", + "1", + "2", + "3", + "4", + "5", + "6", + ]); + + expect(diff).toBeInstanceOf(IndexChange); + expect((diff as IndexChange).newIndex).toBe(0); + expect((diff as IndexChange).oldIndex).toBe(2); + }); + + it("Move 5 to the top level", () => { + const diff = list2.getChange({ id: "5" }, [ + "1", + "5", + "2", + "3", + "4", + "6", + "7", + ]); + + expect(diff).toBeInstanceOf(LevelChange); + expect((diff as LevelChange).parent).toBeUndefined(); + }); + + it("Move 5 to the top level, begin of the list", () => { + const diff = list2.getChange({ id: "5" }, [ + "5", + "1", + "2", + "3", + "4", + "6", + "7", + ]); + + expect(diff).toBeInstanceOf(LevelChange); + expect((diff as LevelChange).parent).toBeUndefined(); + }); + + it("Move 6 one level up", () => { + const diff = list2.getChange({ id: "6" }, [ + "1", + "2", + "6", + "3", + "4", + "5", + "7", + ]); + + expect(diff).toBeInstanceOf(LevelChange); + expect((diff as LevelChange).parent?.id).toBe("2"); + }); + + it("Move a parent to the top", () => { + const diff = list2.getChange({ id: "4" }, [ + "4", + "5", + "6", + "1", + "2", + "3", + "7", + ]); + + expect(diff).toBeInstanceOf(LevelChange); + expect((diff as LevelChange).parent?.id).toBeUndefined(); + }); + + it("Move a parent same level", () => { + const diff = list2.getChange({ id: "4" }, [ + "1", + "2", + "4", + "5", + "6", + "3", + "7", + ]); + + expect(diff).toBeInstanceOf(IndexChange); + expect((diff as IndexChange).newIndex).toBe(0); + }); + + it("Move 5 to the bottom", () => { + const diff = list2.getChange({ id: "5" }, [ + "1", + "2", + "3", + "4", + "6", + "7", + "5", + ]); + + expect(diff).toBeInstanceOf(LevelChange); + expect((diff as LevelChange).parent).toBeUndefined(); + }); +}); diff --git a/src/authentication/authentication-section.css b/src/authentication/authentication-section.css index 3ce53bbba2..ad358a71c2 100644 --- a/src/authentication/authentication-section.css +++ b/src/authentication/authentication-section.css @@ -1,3 +1,7 @@ .keycloak_authentication-section__usedby { color: var(--pf-global--success-color--100); +} + +.keycloak_authentication-section__usedby_label .pf-c-label__icon { + color: var(--pf-global--success-color--100); } \ No newline at end of file diff --git a/src/authentication/components/FlowDiagram.tsx b/src/authentication/components/FlowDiagram.tsx new file mode 100644 index 0000000000..4efc2efcfc --- /dev/null +++ b/src/authentication/components/FlowDiagram.tsx @@ -0,0 +1,264 @@ +import React, { useState, MouseEvent } from "react"; +import { + Drawer, + DrawerActions, + DrawerCloseButton, + DrawerContent, + DrawerContentBody, + DrawerHead, + DrawerPanelContent, +} from "@patternfly/react-core"; +import ReactFlow, { + Node, + Edge, + Elements, + Position, + removeElements, + MiniMap, + Controls, + Background, + isNode, +} from "react-flow-renderer"; + +import type AuthenticationExecutionInfoRepresentation from "keycloak-admin/lib/defs/authenticationExecutionInfoRepresentation"; +import type { ExecutionList, ExpandableExecution } from "../execution-model"; +import { EndSubFlowNode, StartSubFlowNode } from "./diagram/SubFlowNode"; +import { ConditionalNode } from "./diagram/ConditionalNode"; +import { ButtonEdge } from "./diagram/ButtonEdge"; +import { getLayoutedElements } from "./diagram/auto-layout"; + +import "./flow-diagram.css"; + +type FlowDiagramProps = { + executionList: ExecutionList; +}; + +const createEdge = (fromNode: string, toNode: string) => ({ + id: `edge-${fromNode}-to-${toNode}`, + type: "buttonEdge", + source: fromNode, + target: toNode, + data: { + onEdgeClick: ( + evt: React.MouseEvent, + id: string + ) => { + evt.stopPropagation(); + alert(`hello ${id}`); + }, + }, +}); + +const createNode = (ex: ExpandableExecution) => { + let nodeType: string | undefined = undefined; + if (ex.executionList) { + nodeType = "startSubFlow"; + } + if (ex.displayName?.startsWith("Condition")) { + nodeType = "conditional"; + } + return { + id: ex.id!, + type: nodeType, + sourcePosition: Position.Right, + targetPosition: Position.Left, + data: { label: ex.displayName! }, + position: { x: 0, y: 0 }, + }; +}; + +const renderParallelNodes = ( + start: AuthenticationExecutionInfoRepresentation, + execution: ExpandableExecution, + end: AuthenticationExecutionInfoRepresentation +) => { + const elements: Elements = []; + elements.push(createNode(execution)); + elements.push(createEdge(start.id!, execution.id!)); + elements.push(createEdge(execution.id!, end.id!)); + return elements; +}; + +const renderSequentialNodes = ( + start: AuthenticationExecutionInfoRepresentation, + execution: ExpandableExecution, + end: AuthenticationExecutionInfoRepresentation, + prefExecution: ExpandableExecution, + isFirst: boolean, + isLast: boolean +) => { + const elements: Elements = []; + elements.push(createNode(execution)); + if (isFirst) { + elements.push(createEdge(start.id!, execution.id!)); + } else { + elements.push(createEdge(prefExecution.id!, execution.id!)); + } + + if (isLast) { + elements.push(createEdge(execution.id!, end.id!)); + } + + return elements; +}; + +const renderSubFlow = ( + execution: ExpandableExecution, + start: AuthenticationExecutionInfoRepresentation, + end: AuthenticationExecutionInfoRepresentation, + prefExecution?: ExpandableExecution +) => { + const elements: Elements = []; + + elements.push({ + id: execution.id!, + type: "startSubFlow", + sourcePosition: Position.Right, + targetPosition: Position.Left, + data: { label: execution.displayName! }, + position: { x: 0, y: 0 }, + }); + const endSubFlowId = `flow-end-${execution.id}`; + elements.push({ + id: endSubFlowId, + type: "endSubFlow", + sourcePosition: Position.Right, + targetPosition: Position.Left, + data: { label: execution.displayName! }, + position: { x: 0, y: 0 }, + }); + elements.push( + createEdge( + prefExecution && prefExecution.requirement !== "ALTERNATIVE" + ? prefExecution.id! + : start.id!, + execution.id! + ) + ); + elements.push(createEdge(endSubFlowId, end.id!)); + + return elements.concat( + renderFlow(execution, execution.executionList, { + ...execution, + id: endSubFlowId, + }) + ); +}; + +const renderFlow = ( + start: AuthenticationExecutionInfoRepresentation, + executionList: ExpandableExecution[], + end: AuthenticationExecutionInfoRepresentation +) => { + let elements: Elements = []; + + for (let index = 0; index < executionList.length; index++) { + const execution = executionList[index]; + if (execution.executionList) { + elements = elements.concat( + renderSubFlow(execution, start, end, executionList[index - 1]) + ); + } else { + if ( + execution.requirement === "ALTERNATIVE" || + execution.requirement === "DISABLED" + ) { + elements = elements.concat(renderParallelNodes(start, execution, end)); + } else { + elements = elements.concat( + renderSequentialNodes( + start, + execution, + end, + executionList[index - 1], + index === 0, + index === executionList.length - 1 + ) + ); + } + } + } + + return elements; +}; + +export const FlowDiagram = ({ + executionList: { expandableList }, +}: FlowDiagramProps) => { + let elements: Elements = [ + { + id: "start", + sourcePosition: Position.Right, + type: "input", + data: { label: "Start" }, + position: { x: 0, y: 0 }, + className: "keycloak__authentication__input_node", + }, + { + id: "end", + targetPosition: Position.Left, + type: "output", + data: { label: "End" }, + position: { x: 0, y: 0 }, + className: "keycloak__authentication__output_node", + }, + ]; + + elements = elements.concat( + renderFlow({ id: "start" }, expandableList, { id: "end" }) + ); + + const onLoad = (reactFlowInstance: { fitView: () => void }) => + reactFlowInstance.fitView(); + + const [layoutedElements, setElements] = useState( + getLayoutedElements(elements) + ); + const [expandDrawer, setExpandDrawer] = useState(false); + + const onElementClick = (_event: MouseEvent, element: Node | Edge) => { + if (isNode(element)) setExpandDrawer(!expandDrawer); + }; + + const onElementsRemove = (elementsToRemove: Elements) => + setElements((els) => removeElements(elementsToRemove, els)); + + return ( + setExpandDrawer(true)}> + + + drawer-panel + + setExpandDrawer(false)} /> + + + + } + > + + + + + + + + + + ); +}; diff --git a/src/authentication/components/FlowHeader.tsx b/src/authentication/components/FlowHeader.tsx new file mode 100644 index 0000000000..553f3e42cb --- /dev/null +++ b/src/authentication/components/FlowHeader.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { + DataListItem, + DataListItemRow, + DataListDragButton, + DataListItemCells, + DataListCell, +} from "@patternfly/react-core"; + +import "./flow-header.css"; + +export const FlowHeader = () => { + const { t } = useTranslation("authentication"); + return ( + + + + + <>{t("steps")} + , + + <>{t("requirement")} + , + ]} + /> + + + ); +}; diff --git a/src/authentication/components/FlowRequirementDropdown.tsx b/src/authentication/components/FlowRequirementDropdown.tsx new file mode 100644 index 0000000000..27e615c5c7 --- /dev/null +++ b/src/authentication/components/FlowRequirementDropdown.tsx @@ -0,0 +1,48 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Select, SelectOption, SelectVariant } from "@patternfly/react-core"; + +import type AuthenticationExecutionInfoRepresentation from "keycloak-admin/lib/defs/authenticationExecutionInfoRepresentation"; + +type FlowRequirementDropdownProps = { + flow: AuthenticationExecutionInfoRepresentation; + onChange: (flow: AuthenticationExecutionInfoRepresentation) => void; +}; + +export const FlowRequirementDropdown = ({ + flow, + onChange, +}: FlowRequirementDropdownProps) => { + const { t } = useTranslation("authentication"); + const [open, setOpen] = useState(false); + + const options = flow.requirementChoices!.map((option, index) => ( + + {t(`requirements.${option}`)} + + )); + + return ( + <> + {flow.requirementChoices && flow.requirementChoices.length > 1 && ( + + )} + {(!flow.requirementChoices || flow.requirementChoices.length <= 1) && ( + <>{t(`requirements.${flow.requirement}`)} + )} + + ); +}; diff --git a/src/authentication/components/FlowRow.tsx b/src/authentication/components/FlowRow.tsx new file mode 100644 index 0000000000..d5fbd64716 --- /dev/null +++ b/src/authentication/components/FlowRow.tsx @@ -0,0 +1,102 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { + DataListItemRow, + DataListControl, + DataListDragButton, + DataListItemCells, + DataListCell, + DataListItem, + DataListToggle, + Text, + TextVariants, +} from "@patternfly/react-core"; + +import type AuthenticationExecutionInfoRepresentation from "keycloak-admin/lib/defs/authenticationExecutionInfoRepresentation"; +import type { ExpandableExecution } from "../FlowDetails"; +import { FlowTitle } from "./FlowTitle"; +import { FlowRequirementDropdown } from "./FlowRequirementDropdown"; + +import "./flow-row.css"; + +type FlowRowProps = { + execution: ExpandableExecution; + onRowClick: (execution: ExpandableExecution) => void; + onRowChange: (execution: AuthenticationExecutionInfoRepresentation) => void; +}; + +export const FlowRow = ({ + execution, + onRowClick, + onRowChange, +}: FlowRowProps) => { + const { t } = useTranslation("authentication"); + const hasSubList = !!execution.executionList?.length; + return ( + <> + + + + + + {hasSubList && ( + onRowClick(execution)} + isExpanded={!execution.isCollapsed} + id={`toggle1-${execution.id}`} + aria-controls={`expand-${execution.id}`} + /> + )} + + {!hasSubList && ( + + )} + {hasSubList && ( + <> + {execution.displayName}
{" "} + + {execution.description} + + + )} + , + + + , + ]} + /> +
+
+ {!execution.isCollapsed && hasSubList && ( + <> + {execution.executionList.map((execution) => ( + + ))} + + )} + + ); +}; diff --git a/src/authentication/components/FlowTitle.tsx b/src/authentication/components/FlowTitle.tsx new file mode 100644 index 0000000000..9fd607b07d --- /dev/null +++ b/src/authentication/components/FlowTitle.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { Card, CardBody } from "@patternfly/react-core"; + +import "./flow-title.css"; + +type FlowTitleProps = { + title: string; +}; + +export const FlowTitle = ({ title }: FlowTitleProps) => { + return ( + + {title} + + ); +}; diff --git a/src/authentication/components/diagram/ButtonEdge.tsx b/src/authentication/components/diagram/ButtonEdge.tsx new file mode 100644 index 0000000000..3023a6c4b2 --- /dev/null +++ b/src/authentication/components/diagram/ButtonEdge.tsx @@ -0,0 +1,91 @@ +import React, { CSSProperties } from "react"; +import { PlusIcon } from "@patternfly/react-icons"; +import { + ArrowHeadType, + getBezierPath, + getEdgeCenter, + getMarkerEnd, + Position, +} from "react-flow-renderer"; + +type ButtonEdgeProps = { + id: string; + sourceX: number; + sourceY: number; + sourcePosition?: Position; + targetX: number; + targetY: number; + targetPosition?: Position; + style: CSSProperties; + arrowHeadType?: ArrowHeadType; + markerEndId: string; + selected: boolean; + data: { + onEdgeClick: ( + evt: React.MouseEvent, + id: string + ) => void; + }; +}; + +const foreignObjectSize = 33; + +export const ButtonEdge = ({ + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + style = {}, + arrowHeadType, + markerEndId, + selected, + data: { onEdgeClick }, +}: ButtonEdgeProps) => { + const edgePath = getBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + const markerEnd = getMarkerEnd(arrowHeadType, markerEndId); + const [edgeCenterX, edgeCenterY] = getEdgeCenter({ + sourceX, + sourceY, + targetX, + targetY, + }); + + return ( + <> + + {selected && ( + + + + )} + + ); +}; diff --git a/src/authentication/components/diagram/ConditionalNode.tsx b/src/authentication/components/diagram/ConditionalNode.tsx new file mode 100644 index 0000000000..d96a12af59 --- /dev/null +++ b/src/authentication/components/diagram/ConditionalNode.tsx @@ -0,0 +1,26 @@ +import React, { memo } from "react"; +import { Handle, Position } from "react-flow-renderer"; + +type ConditionalNodeProps = { + data: { label: string }; + selected: boolean; +}; + +export const ConditionalNode = memo(function TheNode({ + data, + selected, +}: ConditionalNodeProps) { + return ( + <> + +
+
{data.label}
+
+ + + ); +}); diff --git a/src/authentication/components/diagram/SubFlowNode.tsx b/src/authentication/components/diagram/SubFlowNode.tsx new file mode 100644 index 0000000000..361456fc74 --- /dev/null +++ b/src/authentication/components/diagram/SubFlowNode.tsx @@ -0,0 +1,40 @@ +import React, { memo } from "react"; +import { Handle, Position } from "react-flow-renderer"; + +type NodeProps = { + data: { label: string }; + selected: boolean; +}; + +type SubFlowNodeProps = NodeProps & { + prefix: string; +}; + +export const SubFlowNode = memo(function TheNode({ + data: { label }, + prefix, + selected, +}: SubFlowNodeProps) { + return ( + <> + +
+
+ {prefix} {label} +
+
+ + + ); +}); + +export const StartSubFlowNode = ({ ...props }: NodeProps) => ( + +); +export const EndSubFlowNode = ({ ...props }: NodeProps) => ( + +); diff --git a/src/authentication/components/diagram/auto-layout.ts b/src/authentication/components/diagram/auto-layout.ts new file mode 100644 index 0000000000..4d40920a26 --- /dev/null +++ b/src/authentication/components/diagram/auto-layout.ts @@ -0,0 +1,41 @@ +import { Elements, Position, isNode } from "react-flow-renderer"; +import dagre from "dagre"; + +const dagreGraph = new dagre.graphlib.Graph(); +dagreGraph.setDefaultEdgeLabel(() => ({})); + +const nodeWidth = 130; +const nodeHeight = 28; + +export const getLayoutedElements = (elements: Elements, direction = "LR") => { + const isHorizontal = direction === "LR"; + dagreGraph.setGraph({ rankdir: direction }); + + elements.forEach((element) => { + if (isNode(element)) { + dagreGraph.setNode(element.id, { + width: nodeWidth, + height: nodeHeight, + }); + } else { + dagreGraph.setEdge(element.source, element.target); + } + }); + + dagre.layout(dagreGraph); + + return elements.map((element) => { + if (isNode(element)) { + const nodeWithPosition = dagreGraph.node(element.id); + element.targetPosition = isHorizontal ? Position.Left : Position.Top; + element.sourcePosition = isHorizontal ? Position.Right : Position.Bottom; + + element.position = { + x: nodeWithPosition.x - nodeWidth / 2 + Math.random() / 1000, + y: nodeWithPosition.y - nodeHeight / 2, + }; + } + + return element; + }); +}; diff --git a/src/authentication/components/flow-diagram.css b/src/authentication/components/flow-diagram.css new file mode 100644 index 0000000000..bb691c38a8 --- /dev/null +++ b/src/authentication/components/flow-diagram.css @@ -0,0 +1,52 @@ +.keycloak__authentication__input_node, +.keycloak__authentication__output_node { + width: 50px; + height: 50px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--pf-global--BackgroundColor--200); + border: 0; +} + +.keycloak__authentication__conditional_node { + border: 1px solid #777; + width: 80px; + height: 80px; + transform: rotate(45deg); +} + +.keycloak__authentication__conditional_node div { + transform: rotate(-45deg); +} + +.keycloak__authentication__subflow_node { + width: 80px; + height: 80px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--pf-global--BackgroundColor--200); +} + +.keycloak__authentication__input_node.selected, +.keycloak__authentication__input_node:hover, +.keycloak__authentication__output_node.selected, +.keycloak__authentication__output_node:hover, +.keycloak__authentication__subflow_node.selected, +.keycloak__authentication__subflow_node:hover, +.keycloak__authentication__conditional_node.selected, +.keycloak__authentication__conditional_node:hover { + box-shadow: 0 0 0 0.5px #1a192b; +} + +.edgebutton { + background-color: var(--pf-global--BackgroundColor--200); + border: 1px solid var(--pf-global--BackgroundColor--100); + border-radius: 50%; + cursor: pointer; + height: 33px; + width: 33px; +} diff --git a/src/authentication/components/flow-header.css b/src/authentication/components/flow-header.css new file mode 100644 index 0000000000..4ae3f81d7b --- /dev/null +++ b/src/authentication/components/flow-header.css @@ -0,0 +1,7 @@ +.keycloak__authentication__header-drag-button svg { + fill: var(--pf-global--BackgroundColor--100); +} + +.keycloak__authentication__header .pf-c-data-list__cell { + font-weight: 700; +} diff --git a/src/authentication/components/flow-row.css b/src/authentication/components/flow-row.css new file mode 100644 index 0000000000..d4f51034bc --- /dev/null +++ b/src/authentication/components/flow-row.css @@ -0,0 +1,55 @@ +.keycloak__authentication__flow-item:before { + width: 0; +} + +.keycloak__authentication__flow-row[aria-level="1"] { + padding-left: calc( + var(--pf-global--spacer--lg) * 2 + ); +} + +.keycloak__authentication__flow-row[aria-level="2"] { + padding-left: calc( + var(--pf-global--spacer--lg) * 3 + ); +} +.keycloak__authentication__flow-row[aria-level="3"] { + padding-left: calc( + var(--pf-global--spacer--lg) * 4 + ); +} +.keycloak__authentication__flow-row[aria-level="4"] { + padding-left: calc( + var(--pf-global--spacer--lg) * 5 + ); +} +.keycloak__authentication__flow-row[aria-level="5"] { + padding-left: calc( + var(--pf-global--spacer--lg) * 6 + ); +} +.keycloak__authentication__flow-row[aria-level="6"] { + padding-left: calc( + var(--pf-global--spacer--lg) * 7 + ); +} +.keycloak__authentication__flow-row[aria-level="7"] { + padding-left: calc( + var(--pf-global--spacer--lg) * 8 + ); +} +.keycloak__authentication__flow-row[aria-level="8"] { + padding-left: calc( + var(--pf-global--spacer--lg) * 9 + ); +} +.keycloak__authentication__flow-row[aria-level="9"] { + padding-left: calc( + var(--pf-global--spacer--lg) * 10 + ); +} +.keycloak__authentication__flow-row[aria-level="10"] { + padding-left: calc( + var(--pf-global--spacer--lg) * 11 + ); +} diff --git a/src/authentication/components/flow-title.css b/src/authentication/components/flow-title.css new file mode 100644 index 0000000000..6add421cde --- /dev/null +++ b/src/authentication/components/flow-title.css @@ -0,0 +1,9 @@ +.keycloak__authentication__title { + width: fit-content; + width: -moz-fit-content; +} + +.keycloak__authentication__title .pf-c-card__body { + padding-bottom: var(--pf-global--spacer--sm); + padding-top: var(--pf-global--spacer--sm); +} diff --git a/src/authentication/empty-execution-state.css b/src/authentication/empty-execution-state.css new file mode 100644 index 0000000000..bb09a64a99 --- /dev/null +++ b/src/authentication/empty-execution-state.css @@ -0,0 +1,11 @@ +.keycloak__empty-execution-state__block { + padding-top: var(--pf-global--spacer--sm); +} +.keycloak__empty-execution-state__help { + max-width: 36rem; + margin: 0 auto var(--pf-global--spacer--2xl); +} + +.keycloak__empty-execution-state__help p { + color: var(--pf-global--Color--200); +} diff --git a/src/authentication/execution-model.ts b/src/authentication/execution-model.ts new file mode 100644 index 0000000000..8e1a8bf231 --- /dev/null +++ b/src/authentication/execution-model.ts @@ -0,0 +1,143 @@ +import type AuthenticationExecutionInfoRepresentation from "keycloak-admin/lib/defs/authenticationExecutionInfoRepresentation"; + +export type ExpandableExecution = AuthenticationExecutionInfoRepresentation & { + executionList: ExpandableExecution[]; + isCollapsed: boolean; +}; + +export class IndexChange { + oldIndex: number; + newIndex: number; + + constructor(oldIndex: number, newIndex: number) { + this.oldIndex = oldIndex; + this.newIndex = newIndex; + } +} + +export class LevelChange extends IndexChange { + parent?: ExpandableExecution; + + constructor( + oldIndex: number, + newIndex: number, + parent?: ExpandableExecution + ) { + super(oldIndex, newIndex); + this.parent = parent; + } +} + +export class ExecutionList { + private list: ExpandableExecution[]; + expandableList: ExpandableExecution[]; + + constructor(list: AuthenticationExecutionInfoRepresentation[]) { + this.list = list as ExpandableExecution[]; + this.expandableList = this.transformToExpandableList(0, 0, { + executionList: [], + isCollapsed: false, + }).executionList; + } + + private transformToExpandableList( + level: number, + currIndex: number, + execution: ExpandableExecution + ) { + for (let index = currIndex; index < this.list.length; index++) { + const ex = this.list[index]; + const nextRowLevel = this.list[index + 1]?.level || 0; + + if (ex.level === level && nextRowLevel <= level) { + execution.executionList.push(ex); + } else if (ex.level === level && nextRowLevel > level) { + const expandable = this.transformToExpandableList( + nextRowLevel, + index + 1, + { + ...ex, + executionList: [], + isCollapsed: false, + } + ); + execution.executionList.push(expandable); + } + } + return execution; + } + + order(list?: ExpandableExecution[]) { + let result: ExpandableExecution[] = []; + for (const row of list || this.expandableList) { + result.push(row); + if (row.executionList && !row.isCollapsed) { + result = result.concat(this.order(row.executionList)); + } + } + return result; + } + + findExecution( + id: string, + list?: ExpandableExecution[] + ): ExpandableExecution | undefined { + let found = (list || this.expandableList).find((ex) => ex.id === id); + if (!found) { + for (const ex of list || this.expandableList) { + if (ex.executionList) { + found = this.findExecution(id, ex.executionList); + if (found) { + return found; + } + } + } + } + return found; + } + + private getParentNodes(level?: number) { + for (let index = 0; index < this.list.length; index++) { + const ex = this.list[index]; + if ( + index + 1 < this.list.length && + this.list[index + 1].level! > ex.level! && + ex.level! + 1 === level + ) { + return ex; + } + } + } + + getChange( + changed: AuthenticationExecutionInfoRepresentation, + order: string[] + ) { + const currentOrder = this.order(); + const newLocIndex = order.findIndex((id) => id === changed.id); + const oldLocation = + currentOrder[currentOrder.findIndex((ex) => ex.id === changed.id)]; + const newLocation = currentOrder[newLocIndex]; + + if (newLocation.level !== oldLocation.level) { + if (newLocation.level! > 0) { + const parent = this.getParentNodes(newLocation.level); + return new LevelChange( + parent?.executionList?.length || 0, + newLocation.index!, + parent + ); + } + return new LevelChange(this.expandableList.length, newLocation.index!); + } + + return new IndexChange(oldLocation.index!, newLocation.index!); + } + + clone() { + const newList = new ExecutionList([]); + newList.list = this.list; + newList.expandableList = this.expandableList; + return newList; + } +} diff --git a/src/authentication/form/CreateFlow.tsx b/src/authentication/form/CreateFlow.tsx new file mode 100644 index 0000000000..11cb78bf72 --- /dev/null +++ b/src/authentication/form/CreateFlow.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import { useHistory } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { FormProvider, useForm } from "react-hook-form"; +import { + ActionGroup, + AlertVariant, + Button, + PageSection, +} from "@patternfly/react-core"; + +import type AuthenticationFlowRepresentation from "keycloak-admin/lib/defs/authenticationFlowRepresentation"; +import { ViewHeader } from "../../components/view-header/ViewHeader"; +import { FormAccess } from "../../components/form-access/FormAccess"; +import { useRealm } from "../../context/realm-context/RealmContext"; +import { useAdminClient } from "../../context/auth/AdminClient"; +import { useAlerts } from "../../components/alert/Alerts"; +import { NameDescription } from "./NameDescription"; +import { FlowType } from "./FlowType"; + +export const CreateFlow = () => { + const { t } = useTranslation("authentication"); + const history = useHistory(); + const { realm } = useRealm(); + const form = useForm({ + defaultValues: { builtIn: false, topLevel: true }, + }); + const { handleSubmit, register } = form; + + const adminClient = useAdminClient(); + const { addAlert } = useAlerts(); + + const save = async (flow: AuthenticationFlowRepresentation) => { + try { + await adminClient.authenticationManagement.createFlow(flow); + addAlert(t("flowCreatedSuccess"), AlertVariant.success); + } catch (error) { + addAlert( + t("flowCreateError", { + error: error.response?.data?.errorMessage || error, + }), + AlertVariant.danger + ); + } + }; + + return ( + <> + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/authentication/form/FlowType.tsx b/src/authentication/form/FlowType.tsx new file mode 100644 index 0000000000..366e463776 --- /dev/null +++ b/src/authentication/form/FlowType.tsx @@ -0,0 +1,62 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Controller, useFormContext } from "react-hook-form"; +import { + FormGroup, + Select, + SelectOption, + SelectVariant, +} from "@patternfly/react-core"; + +import { HelpItem } from "../../components/help-enabler/HelpItem"; + +const types = ["basic-flow", "client-flow"]; + +export const FlowType = () => { + const { t } = useTranslation("authentication"); + const { control } = useFormContext(); + + const [open, setOpen] = useState(false); + + return ( + + } + fieldId="flowType" + > + ( + + )} + /> + + ); +}; diff --git a/src/authentication/form/NameDescription.tsx b/src/authentication/form/NameDescription.tsx new file mode 100644 index 0000000000..9dbde6f624 --- /dev/null +++ b/src/authentication/form/NameDescription.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { useFormContext } from "react-hook-form"; +import { + FormGroup, + TextArea, + TextInput, + ValidatedOptions, +} from "@patternfly/react-core"; + +import { HelpItem } from "../../components/help-enabler/HelpItem"; + +export const NameDescription = () => { + const { t } = useTranslation("authentication"); + const { register, errors } = useFormContext(); + + return ( + <> + + } + > + + + + } + validated={ + errors.description ? ValidatedOptions.error : ValidatedOptions.default + } + helperTextInvalid={errors.description?.message} + > +