added subflow and create flow to authentication (#1198)
* added subflow and create flow to authentication * fixed types
This commit is contained in:
parent
8a9f96e53a
commit
b7ebd3260b
13 changed files with 451 additions and 75 deletions
|
@ -14,7 +14,7 @@ describe("Authentication test", () => {
|
||||||
|
|
||||||
const detailPage = new FlowDetails();
|
const detailPage = new FlowDetails();
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(() => {
|
||||||
keycloakBefore();
|
keycloakBefore();
|
||||||
loginPage.logIn();
|
loginPage.logIn();
|
||||||
sidebarPage.goToAuthentication();
|
sidebarPage.goToAuthentication();
|
||||||
|
@ -78,4 +78,32 @@ describe("Authentication test", () => {
|
||||||
masthead.checkNotificationMessage("Flow successfully updated");
|
masthead.checkNotificationMessage("Flow successfully updated");
|
||||||
detailPage.executionExists("Username Password Challenge");
|
detailPage.executionExists("Username Password Challenge");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should add a sub-flow", () => {
|
||||||
|
const flowName = "SubFlow";
|
||||||
|
listingPage.goToItemDetails("Copy of browser");
|
||||||
|
detailPage.addSubFlow(
|
||||||
|
"Copy of browser Browser - Conditional OTP",
|
||||||
|
flowName
|
||||||
|
);
|
||||||
|
|
||||||
|
masthead.checkNotificationMessage("Flow successfully updated");
|
||||||
|
detailPage.flowExists(flowName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create flow from scratch", () => {
|
||||||
|
const flowName = "Flow";
|
||||||
|
listingPage.goToCreateItem();
|
||||||
|
detailPage.fillCreateForm(
|
||||||
|
flowName,
|
||||||
|
"Some nice description about what this flow does so that we can use it later",
|
||||||
|
"Client flow"
|
||||||
|
);
|
||||||
|
masthead.checkNotificationMessage("Flow created");
|
||||||
|
detailPage.addSubFlowToEmpty(flowName, "EmptySubFlow");
|
||||||
|
|
||||||
|
masthead.checkNotificationMessage("Flow successfully updated");
|
||||||
|
|
||||||
|
detailPage.flowExists(flowName);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,6 +6,11 @@ export default class FlowDetails {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flowExists(name: string) {
|
||||||
|
cy.findAllByText(name).should("exist");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
private getExecution(name: string) {
|
private getExecution(name: string) {
|
||||||
return cy.findByTestId(name);
|
return cy.findByTestId(name);
|
||||||
}
|
}
|
||||||
|
@ -63,4 +68,38 @@ export default class FlowDetails {
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addSubFlow(subFlowName: string, name: string) {
|
||||||
|
this.clickEditDropdownForFlow(subFlowName, "Add sub-flow");
|
||||||
|
this.fillSubFlowModal(subFlowName, name);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fillSubFlowModal(subFlowName: string, name: string) {
|
||||||
|
cy.get(".pf-c-modal-box__title-text").contains(
|
||||||
|
"Add step to " + subFlowName
|
||||||
|
);
|
||||||
|
cy.findByTestId("name").type(name);
|
||||||
|
cy.findByTestId("modal-add").click();
|
||||||
|
}
|
||||||
|
|
||||||
|
fillCreateForm(
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
type: "Basic flow" | "Client flow"
|
||||||
|
) {
|
||||||
|
cy.findByTestId("alias").type(name);
|
||||||
|
cy.findByTestId("description").type(description);
|
||||||
|
cy.get("#flowType").click().parent().contains(type).click();
|
||||||
|
cy.findByTestId("create").click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addSubFlowToEmpty(subFlowName: string, name: string) {
|
||||||
|
cy.findByTestId("addSubFlow").click();
|
||||||
|
this.fillSubFlowModal(subFlowName, name);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
@ -8,23 +8,62 @@ import {
|
||||||
TitleSizes,
|
TitleSizes,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import type AuthenticationFlowRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationFlowRepresentation";
|
||||||
|
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
|
||||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||||
|
import { AddStepModal } from "./components/modals/AddStepModal";
|
||||||
|
import { AddSubFlowModal, Flow } from "./components/modals/AddSubFlowModal";
|
||||||
|
|
||||||
import "./empty-execution-state.css";
|
import "./empty-execution-state.css";
|
||||||
|
|
||||||
const sections = ["addExecution", "addSubFlow"];
|
const SECTIONS = ["addExecution", "addSubFlow"] as const;
|
||||||
|
type SectionType = typeof SECTIONS[number] | undefined;
|
||||||
|
|
||||||
export const EmptyExecutionState = () => {
|
type EmptyExecutionStateProps = {
|
||||||
|
flow: AuthenticationFlowRepresentation;
|
||||||
|
onAddExecution: (type: AuthenticationProviderRepresentation) => void;
|
||||||
|
onAddFlow: (flow: Flow) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmptyExecutionState = ({
|
||||||
|
flow,
|
||||||
|
onAddExecution,
|
||||||
|
onAddFlow,
|
||||||
|
}: EmptyExecutionStateProps) => {
|
||||||
const { t } = useTranslation("authentication");
|
const { t } = useTranslation("authentication");
|
||||||
|
const [show, setShow] = useState<SectionType>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{show === "addExecution" && (
|
||||||
|
<AddStepModal
|
||||||
|
name={flow.alias!}
|
||||||
|
type={flow.providerId === "client-flow" ? "client" : "basic"}
|
||||||
|
onSelect={(type) => {
|
||||||
|
if (type) {
|
||||||
|
onAddExecution(type);
|
||||||
|
}
|
||||||
|
setShow(undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{show === "addSubFlow" && (
|
||||||
|
<AddSubFlowModal
|
||||||
|
name={flow.alias!}
|
||||||
|
onCancel={() => setShow(undefined)}
|
||||||
|
onConfirm={(newFlow) => {
|
||||||
|
onAddFlow(newFlow);
|
||||||
|
setShow(undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ListEmptyState
|
<ListEmptyState
|
||||||
message={t("emptyExecution")}
|
message={t("emptyExecution")}
|
||||||
instructions={t("emptyExecutionInstructions")}
|
instructions={t("emptyExecutionInstructions")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="keycloak__empty-execution-state__block">
|
<div className="keycloak__empty-execution-state__block">
|
||||||
{sections.map((section) => (
|
{SECTIONS.map((section) => (
|
||||||
<Flex key={section} className="keycloak__empty-execution-state__help">
|
<Flex key={section} className="keycloak__empty-execution-state__help">
|
||||||
<FlexItem flex={{ default: "flex_1" }}>
|
<FlexItem flex={{ default: "flex_1" }}>
|
||||||
<Title headingLevel="h3" size={TitleSizes.md}>
|
<Title headingLevel="h3" size={TitleSizes.md}>
|
||||||
|
@ -34,7 +73,13 @@ export const EmptyExecutionState = () => {
|
||||||
</FlexItem>
|
</FlexItem>
|
||||||
<Flex alignSelf={{ default: "alignSelfCenter" }}>
|
<Flex alignSelf={{ default: "alignSelfCenter" }}>
|
||||||
<FlexItem>
|
<FlexItem>
|
||||||
<Button variant="tertiary">{t(section)}</Button>
|
<Button
|
||||||
|
data-testId={section}
|
||||||
|
variant="tertiary"
|
||||||
|
onClick={() => setShow(section)}
|
||||||
|
>
|
||||||
|
{t(section)}
|
||||||
|
</Button>
|
||||||
</FlexItem>
|
</FlexItem>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
DataList,
|
DataList,
|
||||||
Label,
|
Label,
|
||||||
|
@ -10,8 +10,11 @@ import {
|
||||||
ToggleGroup,
|
ToggleGroup,
|
||||||
ToggleGroupItem,
|
ToggleGroupItem,
|
||||||
AlertVariant,
|
AlertVariant,
|
||||||
|
ActionGroup,
|
||||||
|
Button,
|
||||||
|
ButtonVariant,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import { CheckCircleIcon, TableIcon } from "@patternfly/react-icons";
|
import { CheckCircleIcon, PlusIcon, TableIcon } from "@patternfly/react-icons";
|
||||||
|
|
||||||
import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation";
|
import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation";
|
||||||
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
|
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
|
||||||
|
@ -31,6 +34,9 @@ import {
|
||||||
} from "./execution-model";
|
} from "./execution-model";
|
||||||
import { FlowDiagram } from "./components/FlowDiagram";
|
import { FlowDiagram } from "./components/FlowDiagram";
|
||||||
import { useAlerts } from "../components/alert/Alerts";
|
import { useAlerts } from "../components/alert/Alerts";
|
||||||
|
import { AddStepModal } from "./components/modals/AddStepModal";
|
||||||
|
import { AddSubFlowModal, Flow } from "./components/modals/AddSubFlowModal";
|
||||||
|
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||||
|
|
||||||
export const providerConditionFilter = (
|
export const providerConditionFilter = (
|
||||||
value: AuthenticationProviderRepresentation
|
value: AuthenticationProviderRepresentation
|
||||||
|
@ -51,6 +57,12 @@ export const FlowDetails = () => {
|
||||||
useState<AuthenticationExecutionInfoRepresentation>();
|
useState<AuthenticationExecutionInfoRepresentation>();
|
||||||
const [liveText, setLiveText] = useState("");
|
const [liveText, setLiveText] = useState("");
|
||||||
|
|
||||||
|
const [showAddExecutionDialog, setShowAddExecutionDialog] =
|
||||||
|
useState<boolean>();
|
||||||
|
const [showAddSubFlowDialog, setShowSubFlowDialog] = useState<boolean>();
|
||||||
|
const [selectedExecution, setSelectedExecution] =
|
||||||
|
useState<ExpandableExecution>();
|
||||||
|
|
||||||
useFetch(
|
useFetch(
|
||||||
async () => {
|
async () => {
|
||||||
const flows = await adminClient.authenticationManagement.getFlows();
|
const flows = await adminClient.authenticationManagement.getFlows();
|
||||||
|
@ -84,23 +96,17 @@ export const FlowDetails = () => {
|
||||||
id = result.id!;
|
id = result.id!;
|
||||||
}
|
}
|
||||||
const times = change.newIndex - change.oldIndex;
|
const times = change.newIndex - change.oldIndex;
|
||||||
const requests = [];
|
|
||||||
for (let index = 0; index < Math.abs(times); index++) {
|
for (let index = 0; index < Math.abs(times); index++) {
|
||||||
if (times > 0) {
|
if (times > 0) {
|
||||||
requests.push(
|
await adminClient.authenticationManagement.lowerPriorityExecution({
|
||||||
adminClient.authenticationManagement.lowerPriorityExecution({
|
|
||||||
id,
|
id,
|
||||||
})
|
});
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
requests.push(
|
await adminClient.authenticationManagement.raisePriorityExecution({
|
||||||
adminClient.authenticationManagement.raisePriorityExecution({
|
|
||||||
id,
|
id,
|
||||||
})
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Promise.all(requests);
|
|
||||||
refresh();
|
refresh();
|
||||||
addAlert(t("updateFlowSuccess"), AlertVariant.success);
|
addAlert(t("updateFlowSuccess"), AlertVariant.success);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
@ -124,12 +130,12 @@ export const FlowDetails = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const addExecution = async (
|
const addExecution = async (
|
||||||
execution: ExpandableExecution,
|
name: string,
|
||||||
type: AuthenticationProviderRepresentation
|
type: AuthenticationProviderRepresentation
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await adminClient.authenticationManagement.addExecutionToFlow({
|
await adminClient.authenticationManagement.addExecutionToFlow({
|
||||||
flow: execution.displayName!,
|
flow: name,
|
||||||
provider: type.id!,
|
provider: type.id!,
|
||||||
});
|
});
|
||||||
refresh();
|
refresh();
|
||||||
|
@ -139,6 +145,50 @@ export const FlowDetails = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addFlow = async (
|
||||||
|
flow: string,
|
||||||
|
{ name, description = "", type, provider }: Flow
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await adminClient.authenticationManagement.addFlowToFlow({
|
||||||
|
flow,
|
||||||
|
alias: name,
|
||||||
|
description,
|
||||||
|
provider,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
refresh();
|
||||||
|
addAlert(t("updateFlowSuccess"), AlertVariant.success);
|
||||||
|
} catch (error) {
|
||||||
|
addError("authentication:updateFlowError", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||||
|
titleKey: "authentication:deleteConfirmExecution",
|
||||||
|
children: (
|
||||||
|
<Trans i18nKey="authentication:deleteConfirmExecutionMessage">
|
||||||
|
{" "}
|
||||||
|
<strong>{{ name: selectedExecution?.displayName }}</strong>.
|
||||||
|
</Trans>
|
||||||
|
),
|
||||||
|
continueButtonLabel: "common:delete",
|
||||||
|
continueButtonVariant: ButtonVariant.danger,
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
await adminClient.authenticationManagement.delExecution({
|
||||||
|
id: selectedExecution?.id!,
|
||||||
|
});
|
||||||
|
addAlert(t("deleteExecutionSuccess"), AlertVariant.success);
|
||||||
|
refresh();
|
||||||
|
} catch (error) {
|
||||||
|
addError("authentication:deleteExecutionError", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasExecutions = executionList?.expandableList.length !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewHeader
|
<ViewHeader
|
||||||
|
@ -161,7 +211,7 @@ export const FlowDetails = () => {
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<PageSection variant="light">
|
<PageSection variant="light">
|
||||||
{executionList?.expandableList?.length && (
|
{hasExecutions && (
|
||||||
<Toolbar id="toolbar">
|
<Toolbar id="toolbar">
|
||||||
<ToolbarContent>
|
<ToolbarContent>
|
||||||
<ToggleGroup>
|
<ToggleGroup>
|
||||||
|
@ -183,8 +233,9 @@ export const FlowDetails = () => {
|
||||||
</ToolbarContent>
|
</ToolbarContent>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
)}
|
)}
|
||||||
{tableView && executionList?.expandableList?.length && (
|
{tableView && executionList && hasExecutions && (
|
||||||
<>
|
<>
|
||||||
|
<DeleteConfirm />
|
||||||
<DataList
|
<DataList
|
||||||
aria-label="flows"
|
aria-label="flows"
|
||||||
onDragFinish={(order) => {
|
onDragFinish={(order) => {
|
||||||
|
@ -204,7 +255,7 @@ export const FlowDetails = () => {
|
||||||
t("common:onDragStart", { item: item.displayName })
|
t("common:onDragStart", { item: item.displayName })
|
||||||
);
|
);
|
||||||
setDragged(item);
|
setDragged(item);
|
||||||
if (item.executionList && !item.isCollapsed) {
|
if (!item.isCollapsed) {
|
||||||
item.isCollapsed = true;
|
item.isCollapsed = true;
|
||||||
setExecutionList(executionList.clone());
|
setExecutionList(executionList.clone());
|
||||||
}
|
}
|
||||||
|
@ -231,11 +282,62 @@ export const FlowDetails = () => {
|
||||||
setExecutionList(executionList.clone());
|
setExecutionList(executionList.clone());
|
||||||
}}
|
}}
|
||||||
onRowChange={update}
|
onRowChange={update}
|
||||||
onAddExecution={addExecution}
|
onAddExecution={(execution, type) =>
|
||||||
|
addExecution(execution.displayName!, type)
|
||||||
|
}
|
||||||
|
onAddFlow={(flow) => addFlow(execution.displayName!, flow)}
|
||||||
|
onDelete={() => {
|
||||||
|
setSelectedExecution(execution);
|
||||||
|
toggleDeleteDialog();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
</DataList>
|
</DataList>
|
||||||
|
{flow && (
|
||||||
|
<>
|
||||||
|
{showAddExecutionDialog && (
|
||||||
|
<AddStepModal
|
||||||
|
name={flow.alias!}
|
||||||
|
type={
|
||||||
|
flow.providerId === "client-flow" ? "client" : "basic"
|
||||||
|
}
|
||||||
|
onSelect={(type) => {
|
||||||
|
if (type) {
|
||||||
|
addExecution(flow.alias!, type);
|
||||||
|
}
|
||||||
|
setShowAddExecutionDialog(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showAddSubFlowDialog && (
|
||||||
|
<AddSubFlowModal
|
||||||
|
name={flow.alias!}
|
||||||
|
onCancel={() => setShowSubFlowDialog(false)}
|
||||||
|
onConfirm={(newFlow) => {
|
||||||
|
addFlow(flow.alias!, newFlow);
|
||||||
|
setShowSubFlowDialog(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ActionGroup>
|
||||||
|
<Button
|
||||||
|
data-testid="addStep"
|
||||||
|
variant="link"
|
||||||
|
onClick={() => setShowAddExecutionDialog(true)}
|
||||||
|
>
|
||||||
|
<PlusIcon /> {t("addStep")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
data-testid="addSubFlow"
|
||||||
|
variant="link"
|
||||||
|
onClick={() => setShowSubFlowDialog(true)}
|
||||||
|
>
|
||||||
|
<PlusIcon /> {t("addSubFlow")}
|
||||||
|
</Button>
|
||||||
|
</ActionGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className="pf-screen-reader" aria-live="assertive">
|
<div className="pf-screen-reader" aria-live="assertive">
|
||||||
{liveText}
|
{liveText}
|
||||||
</div>
|
</div>
|
||||||
|
@ -245,8 +347,12 @@ export const FlowDetails = () => {
|
||||||
<FlowDiagram executionList={executionList} />
|
<FlowDiagram executionList={executionList} />
|
||||||
)}
|
)}
|
||||||
{!executionList?.expandableList ||
|
{!executionList?.expandableList ||
|
||||||
(executionList.expandableList.length === 0 && (
|
(flow && !hasExecutions && (
|
||||||
<EmptyExecutionState />
|
<EmptyExecutionState
|
||||||
|
flow={flow}
|
||||||
|
onAddExecution={(type) => addExecution(flow.alias!, type)}
|
||||||
|
onAddFlow={(newFlow) => addFlow(flow.alias!, newFlow)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</PageSection>
|
</PageSection>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-ad
|
||||||
import type { ExpandableExecution } from "../execution-model";
|
import type { ExpandableExecution } from "../execution-model";
|
||||||
import { AddStepModal, FlowType } from "./modals/AddStepModal";
|
import { AddStepModal, FlowType } from "./modals/AddStepModal";
|
||||||
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||||
import { AddSubFlowModal } from "./modals/AddSubFlowModal";
|
import { AddSubFlowModal, Flow } from "./modals/AddSubFlowModal";
|
||||||
|
|
||||||
type EditFlowDropdownProps = {
|
type EditFlowDropdownProps = {
|
||||||
execution: ExpandableExecution;
|
execution: ExpandableExecution;
|
||||||
|
@ -15,11 +15,13 @@ type EditFlowDropdownProps = {
|
||||||
execution: ExpandableExecution,
|
execution: ExpandableExecution,
|
||||||
type: AuthenticationProviderRepresentation
|
type: AuthenticationProviderRepresentation
|
||||||
) => void;
|
) => void;
|
||||||
|
onAddFlow: (flow: Flow) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditFlowDropdown = ({
|
export const EditFlowDropdown = ({
|
||||||
execution,
|
execution,
|
||||||
onAddExecution,
|
onAddExecution,
|
||||||
|
onAddFlow,
|
||||||
}: EditFlowDropdownProps) => {
|
}: EditFlowDropdownProps) => {
|
||||||
const { t } = useTranslation("authentication");
|
const { t } = useTranslation("authentication");
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
|
@ -83,7 +85,10 @@ export const EditFlowDropdown = ({
|
||||||
<AddSubFlowModal
|
<AddSubFlowModal
|
||||||
name={execution.displayName!}
|
name={execution.displayName!}
|
||||||
onCancel={() => setType(undefined)}
|
onCancel={() => setType(undefined)}
|
||||||
onConfirm={() => setType(undefined)}
|
onConfirm={(flow) => {
|
||||||
|
onAddFlow(flow);
|
||||||
|
setType(undefined);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -139,7 +139,7 @@ const renderSubFlow = (
|
||||||
elements.push(createEdge(endSubFlowId, end.id!));
|
elements.push(createEdge(endSubFlowId, end.id!));
|
||||||
|
|
||||||
return elements.concat(
|
return elements.concat(
|
||||||
renderFlow(execution, execution.executionList, {
|
renderFlow(execution, execution.executionList || [], {
|
||||||
...execution,
|
...execution,
|
||||||
id: endSubFlowId,
|
id: endSubFlowId,
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,11 +10,14 @@ import {
|
||||||
DataListToggle,
|
DataListToggle,
|
||||||
Text,
|
Text,
|
||||||
TextVariants,
|
TextVariants,
|
||||||
|
Button,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
|
import { TrashIcon } from "@patternfly/react-icons";
|
||||||
|
|
||||||
import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation";
|
import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation";
|
||||||
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
|
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
|
||||||
import type { ExpandableExecution } from "../execution-model";
|
import type { ExpandableExecution } from "../execution-model";
|
||||||
|
import type { Flow } from "./modals/AddSubFlowModal";
|
||||||
import { FlowTitle } from "./FlowTitle";
|
import { FlowTitle } from "./FlowTitle";
|
||||||
import { FlowRequirementDropdown } from "./FlowRequirementDropdown";
|
import { FlowRequirementDropdown } from "./FlowRequirementDropdown";
|
||||||
import { ExecutionConfigModal } from "./ExecutionConfigModal";
|
import { ExecutionConfigModal } from "./ExecutionConfigModal";
|
||||||
|
@ -30,6 +33,8 @@ type FlowRowProps = {
|
||||||
execution: ExpandableExecution,
|
execution: ExpandableExecution,
|
||||||
type: AuthenticationProviderRepresentation
|
type: AuthenticationProviderRepresentation
|
||||||
) => void;
|
) => void;
|
||||||
|
onAddFlow: (flow: Flow) => void;
|
||||||
|
onDelete: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FlowRow = ({
|
export const FlowRow = ({
|
||||||
|
@ -37,6 +42,8 @@ export const FlowRow = ({
|
||||||
onRowClick,
|
onRowClick,
|
||||||
onRowChange,
|
onRowChange,
|
||||||
onAddExecution,
|
onAddExecution,
|
||||||
|
onAddFlow,
|
||||||
|
onDelete,
|
||||||
}: FlowRowProps) => {
|
}: FlowRowProps) => {
|
||||||
const { t } = useTranslation("authentication");
|
const { t } = useTranslation("authentication");
|
||||||
const hasSubList = !!execution.executionList?.length;
|
const hasSubList = !!execution.executionList?.length;
|
||||||
|
@ -69,13 +76,13 @@ export const FlowRow = ({
|
||||||
<DataListItemCells
|
<DataListItemCells
|
||||||
dataListCells={[
|
dataListCells={[
|
||||||
<DataListCell key={`${execution.id}-name`}>
|
<DataListCell key={`${execution.id}-name`}>
|
||||||
{!hasSubList && (
|
{!execution.authenticationFlow && (
|
||||||
<FlowTitle
|
<FlowTitle
|
||||||
key={execution.id}
|
key={execution.id}
|
||||||
title={execution.displayName!}
|
title={execution.displayName!}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{hasSubList && (
|
{execution.authenticationFlow && (
|
||||||
<>
|
<>
|
||||||
{execution.displayName} <br />{" "}
|
{execution.displayName} <br />{" "}
|
||||||
<Text component={TextVariants.small}>
|
<Text component={TextVariants.small}>
|
||||||
|
@ -98,26 +105,34 @@ export const FlowRow = ({
|
||||||
<EditFlowDropdown
|
<EditFlowDropdown
|
||||||
execution={execution}
|
execution={execution}
|
||||||
onAddExecution={onAddExecution}
|
onAddExecution={onAddExecution}
|
||||||
|
onAddFlow={onAddFlow}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
aria-label={t("common:delete")}
|
||||||
|
onClick={onDelete}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</Button>
|
||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</DataListItemRow>
|
</DataListItemRow>
|
||||||
</DataListItem>
|
</DataListItem>
|
||||||
{!execution.isCollapsed && hasSubList && (
|
{!execution.isCollapsed &&
|
||||||
<>
|
hasSubList &&
|
||||||
{execution.executionList.map((execution) => (
|
execution.executionList?.map((execution) => (
|
||||||
<FlowRow
|
<FlowRow
|
||||||
key={execution.id}
|
key={execution.id}
|
||||||
execution={execution}
|
execution={execution}
|
||||||
onRowClick={onRowClick}
|
onRowClick={onRowClick}
|
||||||
onRowChange={onRowChange}
|
onRowChange={onRowChange}
|
||||||
onAddExecution={onAddExecution}
|
onAddExecution={onAddExecution}
|
||||||
|
onAddFlow={onAddFlow}
|
||||||
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ButtonVariant,
|
ButtonVariant,
|
||||||
|
@ -8,25 +8,53 @@ import {
|
||||||
FormGroup,
|
FormGroup,
|
||||||
Modal,
|
Modal,
|
||||||
ModalVariant,
|
ModalVariant,
|
||||||
|
Select,
|
||||||
|
SelectOption,
|
||||||
|
SelectVariant,
|
||||||
TextInput,
|
TextInput,
|
||||||
ValidatedOptions,
|
ValidatedOptions,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
|
||||||
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||||
|
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
|
||||||
|
|
||||||
type AddSubFlowProps = {
|
type AddSubFlowProps = {
|
||||||
name: string;
|
name: string;
|
||||||
onConfirm: () => void;
|
onConfirm: (flow: Flow) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const types = ["basic-flow", "form-flow"] as const;
|
||||||
|
|
||||||
|
export type Flow = {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
type: typeof types[number];
|
||||||
|
provider: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const AddSubFlowModal = ({
|
export const AddSubFlowModal = ({
|
||||||
name,
|
name,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: AddSubFlowProps) => {
|
}: AddSubFlowProps) => {
|
||||||
const { t } = useTranslation("authentication");
|
const { t } = useTranslation("authentication");
|
||||||
const { register, errors, handleSubmit } = useForm();
|
const { register, control, errors, handleSubmit } = useForm<Flow>();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [openProvider, setOpenProvider] = useState(false);
|
||||||
|
const [formProviders, setFormProviders] =
|
||||||
|
useState<AuthenticationProviderRepresentation[]>();
|
||||||
|
const adminClient = useAdminClient();
|
||||||
|
|
||||||
|
useFetch(
|
||||||
|
() => adminClient.authenticationManagement.getFormProviders(),
|
||||||
|
(providers) =>
|
||||||
|
setFormProviders(
|
||||||
|
providers as unknown as AuthenticationProviderRepresentation[]
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
@ -39,7 +67,8 @@ export const AddSubFlowModal = ({
|
||||||
id="modal-add"
|
id="modal-add"
|
||||||
data-testid="modal-add"
|
data-testid="modal-add"
|
||||||
key="add"
|
key="add"
|
||||||
onClick={() => onConfirm()}
|
type="submit"
|
||||||
|
form="sub-flow-form"
|
||||||
>
|
>
|
||||||
{t("common:add")}
|
{t("common:add")}
|
||||||
</Button>,
|
</Button>,
|
||||||
|
@ -56,11 +85,7 @@ export const AddSubFlowModal = ({
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Form
|
<Form id="sub-flow-form" isHorizontal onSubmit={handleSubmit(onConfirm)}>
|
||||||
id="execution-config-form"
|
|
||||||
isHorizontal
|
|
||||||
onSubmit={handleSubmit(onConfirm)}
|
|
||||||
>
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={t("common:name")}
|
label={t("common:name")}
|
||||||
fieldId="name"
|
fieldId="name"
|
||||||
|
@ -107,6 +132,100 @@ export const AddSubFlowModal = ({
|
||||||
ref={register}
|
ref={register}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={t("flowType")}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="authentication-help:flowType"
|
||||||
|
forLabel={t("flowType")}
|
||||||
|
forID="flowType"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fieldId="flowType"
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="type"
|
||||||
|
defaultValue={types[0]}
|
||||||
|
control={control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Select
|
||||||
|
menuAppendTo="parent"
|
||||||
|
toggleId="flowType"
|
||||||
|
onToggle={() => setOpen(!open)}
|
||||||
|
onSelect={(_, value) => {
|
||||||
|
onChange(value as string);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
selections={t(`flow-type.${value}`)}
|
||||||
|
variant={SelectVariant.single}
|
||||||
|
aria-label={t("flowType")}
|
||||||
|
isOpen={open}
|
||||||
|
>
|
||||||
|
{types.map((type) => (
|
||||||
|
<SelectOption
|
||||||
|
selected={type === value}
|
||||||
|
key={type}
|
||||||
|
value={type}
|
||||||
|
>
|
||||||
|
{t(`flow-type.${type}`)}
|
||||||
|
</SelectOption>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
{formProviders && formProviders.length > 1 && (
|
||||||
|
<FormGroup
|
||||||
|
label={t("flowType")}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="authentication-help:flowType"
|
||||||
|
forLabel={t("flowType")}
|
||||||
|
forID="flowType"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fieldId="flowType"
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="provider"
|
||||||
|
defaultValue=""
|
||||||
|
control={control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Select
|
||||||
|
menuAppendTo="parent"
|
||||||
|
toggleId="provider"
|
||||||
|
onToggle={(toggle) => setOpenProvider(toggle)}
|
||||||
|
onSelect={(_, value) => {
|
||||||
|
onChange(value as string);
|
||||||
|
setOpenProvider(false);
|
||||||
|
}}
|
||||||
|
selections={value.displayName}
|
||||||
|
variant={SelectVariant.single}
|
||||||
|
aria-label={t("flowType")}
|
||||||
|
isOpen={openProvider}
|
||||||
|
>
|
||||||
|
{formProviders.map((provider) => (
|
||||||
|
<SelectOption
|
||||||
|
selected={provider.displayName === value}
|
||||||
|
key={provider.id}
|
||||||
|
value={provider}
|
||||||
|
>
|
||||||
|
{provider.displayName}
|
||||||
|
</SelectOption>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
{formProviders?.length === 1 && (
|
||||||
|
<input
|
||||||
|
name="provider"
|
||||||
|
type="hidden"
|
||||||
|
ref={register}
|
||||||
|
value={formProviders[0].id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation";
|
import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation";
|
||||||
|
|
||||||
export type ExpandableExecution = AuthenticationExecutionInfoRepresentation & {
|
export type ExpandableExecution = AuthenticationExecutionInfoRepresentation & {
|
||||||
executionList: ExpandableExecution[];
|
executionList?: ExpandableExecution[];
|
||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -34,10 +34,11 @@ export class ExecutionList {
|
||||||
|
|
||||||
constructor(list: AuthenticationExecutionInfoRepresentation[]) {
|
constructor(list: AuthenticationExecutionInfoRepresentation[]) {
|
||||||
this.list = list as ExpandableExecution[];
|
this.list = list as ExpandableExecution[];
|
||||||
this.expandableList = this.transformToExpandableList(0, 0, {
|
this.expandableList =
|
||||||
|
this.transformToExpandableList(0, 0, {
|
||||||
executionList: [],
|
executionList: [],
|
||||||
isCollapsed: false,
|
isCollapsed: false,
|
||||||
}).executionList;
|
}).executionList || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private transformToExpandableList(
|
private transformToExpandableList(
|
||||||
|
@ -50,7 +51,7 @@ export class ExecutionList {
|
||||||
const nextRowLevel = this.list[index + 1]?.level || 0;
|
const nextRowLevel = this.list[index + 1]?.level || 0;
|
||||||
|
|
||||||
if (ex.level === level && nextRowLevel <= level) {
|
if (ex.level === level && nextRowLevel <= level) {
|
||||||
execution.executionList.push(ex);
|
execution.executionList?.push(ex);
|
||||||
} else if (ex.level === level && nextRowLevel > level) {
|
} else if (ex.level === level && nextRowLevel > level) {
|
||||||
const expandable = this.transformToExpandableList(
|
const expandable = this.transformToExpandableList(
|
||||||
nextRowLevel,
|
nextRowLevel,
|
||||||
|
@ -61,7 +62,7 @@ export class ExecutionList {
|
||||||
isCollapsed: false,
|
isCollapsed: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
execution.executionList.push(expandable);
|
execution.executionList?.push(expandable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return execution;
|
return execution;
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { useAdminClient } from "../../context/auth/AdminClient";
|
||||||
import { useAlerts } from "../../components/alert/Alerts";
|
import { useAlerts } from "../../components/alert/Alerts";
|
||||||
import { NameDescription } from "./NameDescription";
|
import { NameDescription } from "./NameDescription";
|
||||||
import { FlowType } from "./FlowType";
|
import { FlowType } from "./FlowType";
|
||||||
|
import { toFlow } from "../routes/Flow";
|
||||||
|
|
||||||
export const CreateFlow = () => {
|
export const CreateFlow = () => {
|
||||||
const { t } = useTranslation("authentication");
|
const { t } = useTranslation("authentication");
|
||||||
|
@ -32,8 +33,17 @@ export const CreateFlow = () => {
|
||||||
|
|
||||||
const save = async (flow: AuthenticationFlowRepresentation) => {
|
const save = async (flow: AuthenticationFlowRepresentation) => {
|
||||||
try {
|
try {
|
||||||
await adminClient.authenticationManagement.createFlow(flow);
|
const { id } = (await adminClient.authenticationManagement.createFlow(
|
||||||
|
flow
|
||||||
|
)) as unknown as AuthenticationFlowRepresentation;
|
||||||
addAlert(t("flowCreatedSuccess"), AlertVariant.success);
|
addAlert(t("flowCreatedSuccess"), AlertVariant.success);
|
||||||
|
history.push(
|
||||||
|
toFlow({
|
||||||
|
realm,
|
||||||
|
id: id!,
|
||||||
|
usedBy: "notInUse",
|
||||||
|
})
|
||||||
|
);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
addAlert(
|
addAlert(
|
||||||
t("flowCreateError", {
|
t("flowCreateError", {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
|
|
||||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||||
|
|
||||||
const types = ["basic-flow", "client-flow"];
|
const TYPES = ["basic-flow", "client-flow"] as const;
|
||||||
|
|
||||||
export const FlowType = () => {
|
export const FlowType = () => {
|
||||||
const { t } = useTranslation("authentication");
|
const { t } = useTranslation("authentication");
|
||||||
|
@ -23,7 +23,7 @@ export const FlowType = () => {
|
||||||
label={t("flowType")}
|
label={t("flowType")}
|
||||||
labelIcon={
|
labelIcon={
|
||||||
<HelpItem
|
<HelpItem
|
||||||
helpText="authentication-help:flowType"
|
helpText="authentication-help:topLevelFlowType"
|
||||||
forLabel={t("flowType")}
|
forLabel={t("flowType")}
|
||||||
forID="flowType"
|
forID="flowType"
|
||||||
/>
|
/>
|
||||||
|
@ -32,7 +32,7 @@ export const FlowType = () => {
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="providerId"
|
name="providerId"
|
||||||
defaultValue={types[0]}
|
defaultValue={TYPES[0]}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
<Select
|
<Select
|
||||||
|
@ -42,17 +42,15 @@ export const FlowType = () => {
|
||||||
onChange(value as string);
|
onChange(value as string);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
selections={t(`flow-type.${value}`)}
|
selections={t(`top-level-flow-type.${value}`)}
|
||||||
variant={SelectVariant.single}
|
variant={SelectVariant.single}
|
||||||
aria-label={t("flowType")}
|
aria-label={t("flowType")}
|
||||||
isOpen={open}
|
isOpen={open}
|
||||||
>
|
>
|
||||||
{types.map((type) => (
|
{TYPES.map((type) => (
|
||||||
<SelectOption
|
<SelectOption selected={type === value} key={type} value={type}>
|
||||||
selected={type === value}
|
{t(`top-level-flow-type.${type}`)}
|
||||||
key={type}
|
</SelectOption>
|
||||||
value={t(`flow-type.${type}`)}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -3,7 +3,8 @@ export default {
|
||||||
name: "Help text for the name of the new flow",
|
name: "Help text for the name of the new flow",
|
||||||
description: "Help text for the description of the new flow",
|
description: "Help text for the description of the new flow",
|
||||||
createFlow: "You can create a top level flow within this from",
|
createFlow: "You can create a top level flow within this from",
|
||||||
flowType:
|
flowType: "What kind of form is it",
|
||||||
|
topLevelFlowType:
|
||||||
"What kind of top level flow is it? Type 'client' is used for authentication of clients (applications) when generic is for users and everything else",
|
"What kind of top level flow is it? Type 'client' is used for authentication of clients (applications) when generic is for users and everything else",
|
||||||
addExecution:
|
addExecution:
|
||||||
"Execution can have a wide range of actions, from sending a reset email to validating an OTP",
|
"Execution can have a wide range of actions, from sending a reset email to validating an OTP",
|
||||||
|
|
|
@ -18,6 +18,11 @@ export default {
|
||||||
deleteFlowSuccess: "Flow successfully deleted",
|
deleteFlowSuccess: "Flow successfully deleted",
|
||||||
deleteFlowError: "Could not delete flow: {{error}}",
|
deleteFlowError: "Could not delete flow: {{error}}",
|
||||||
duplicateFlow: "Duplicate flow",
|
duplicateFlow: "Duplicate flow",
|
||||||
|
deleteConfirmExecution: "Delete execution?",
|
||||||
|
deleteConfirmExecutionMessage:
|
||||||
|
'Are you sure you want to permanently delete the execution "<1>{{name}}</1>".',
|
||||||
|
deleteExecutionSuccess: "Execution successfully deleted",
|
||||||
|
deleteExecutionError: "Could not delete execution: {{error}}",
|
||||||
updateFlowSuccess: "Flow successfully updated",
|
updateFlowSuccess: "Flow successfully updated",
|
||||||
updateFlowError: "Could not update flow: {{error}}",
|
updateFlowError: "Could not update flow: {{error}}",
|
||||||
copyOf: "Copy of {{name}}",
|
copyOf: "Copy of {{name}}",
|
||||||
|
@ -26,6 +31,10 @@ export default {
|
||||||
createFlow: "Create flow",
|
createFlow: "Create flow",
|
||||||
flowType: "Flow type",
|
flowType: "Flow type",
|
||||||
"flow-type": {
|
"flow-type": {
|
||||||
|
"basic-flow": "Generic",
|
||||||
|
"form-flow": "Form",
|
||||||
|
},
|
||||||
|
"top-level-flow-type": {
|
||||||
"basic-flow": "Basic flow",
|
"basic-flow": "Basic flow",
|
||||||
"client-flow": "Client flow",
|
"client-flow": "Client flow",
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue