added subflow and create flow to authentication (#1198)

* added subflow and create flow to authentication

* fixed types
This commit is contained in:
Erik Jan de Wit 2021-09-23 17:06:58 +02:00 committed by GitHub
parent 8a9f96e53a
commit b7ebd3260b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 451 additions and 75 deletions

View file

@ -14,7 +14,7 @@ describe("Authentication test", () => {
const detailPage = new FlowDetails();
beforeEach(function () {
beforeEach(() => {
keycloakBefore();
loginPage.logIn();
sidebarPage.goToAuthentication();
@ -78,4 +78,32 @@ describe("Authentication test", () => {
masthead.checkNotificationMessage("Flow successfully updated");
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);
});
});

View file

@ -6,6 +6,11 @@ export default class FlowDetails {
return this;
}
flowExists(name: string) {
cy.findAllByText(name).should("exist");
return this;
}
private getExecution(name: string) {
return cy.findByTestId(name);
}
@ -63,4 +68,38 @@ export default class FlowDetails {
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;
}
}

View file

@ -1,4 +1,4 @@
import React from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import {
Button,
@ -8,23 +8,62 @@ import {
TitleSizes,
} 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 { AddStepModal } from "./components/modals/AddStepModal";
import { AddSubFlowModal, Flow } from "./components/modals/AddSubFlowModal";
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 [show, setShow] = useState<SectionType>();
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
message={t("emptyExecution")}
instructions={t("emptyExecutionInstructions")}
/>
<div className="keycloak__empty-execution-state__block">
{sections.map((section) => (
{SECTIONS.map((section) => (
<Flex key={section} className="keycloak__empty-execution-state__help">
<FlexItem flex={{ default: "flex_1" }}>
<Title headingLevel="h3" size={TitleSizes.md}>
@ -34,7 +73,13 @@ export const EmptyExecutionState = () => {
</FlexItem>
<Flex alignSelf={{ default: "alignSelfCenter" }}>
<FlexItem>
<Button variant="tertiary">{t(section)}</Button>
<Button
data-testId={section}
variant="tertiary"
onClick={() => setShow(section)}
>
{t(section)}
</Button>
</FlexItem>
</Flex>
</Flex>

View file

@ -1,6 +1,6 @@
import React, { useState } from "react";
import { useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import {
DataList,
Label,
@ -10,8 +10,11 @@ import {
ToggleGroup,
ToggleGroupItem,
AlertVariant,
ActionGroup,
Button,
ButtonVariant,
} 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 { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
@ -31,6 +34,9 @@ import {
} from "./execution-model";
import { FlowDiagram } from "./components/FlowDiagram";
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 = (
value: AuthenticationProviderRepresentation
@ -51,6 +57,12 @@ export const FlowDetails = () => {
useState<AuthenticationExecutionInfoRepresentation>();
const [liveText, setLiveText] = useState("");
const [showAddExecutionDialog, setShowAddExecutionDialog] =
useState<boolean>();
const [showAddSubFlowDialog, setShowSubFlowDialog] = useState<boolean>();
const [selectedExecution, setSelectedExecution] =
useState<ExpandableExecution>();
useFetch(
async () => {
const flows = await adminClient.authenticationManagement.getFlows();
@ -84,23 +96,17 @@ export const FlowDetails = () => {
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,
})
);
await adminClient.authenticationManagement.lowerPriorityExecution({
id,
});
} else {
requests.push(
adminClient.authenticationManagement.raisePriorityExecution({
id,
})
);
await adminClient.authenticationManagement.raisePriorityExecution({
id,
});
}
}
await Promise.all(requests);
refresh();
addAlert(t("updateFlowSuccess"), AlertVariant.success);
} catch (error: any) {
@ -124,12 +130,12 @@ export const FlowDetails = () => {
};
const addExecution = async (
execution: ExpandableExecution,
name: string,
type: AuthenticationProviderRepresentation
) => {
try {
await adminClient.authenticationManagement.addExecutionToFlow({
flow: execution.displayName!,
flow: name,
provider: type.id!,
});
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 (
<>
<ViewHeader
@ -161,7 +211,7 @@ export const FlowDetails = () => {
]}
/>
<PageSection variant="light">
{executionList?.expandableList?.length && (
{hasExecutions && (
<Toolbar id="toolbar">
<ToolbarContent>
<ToggleGroup>
@ -183,8 +233,9 @@ export const FlowDetails = () => {
</ToolbarContent>
</Toolbar>
)}
{tableView && executionList?.expandableList?.length && (
{tableView && executionList && hasExecutions && (
<>
<DeleteConfirm />
<DataList
aria-label="flows"
onDragFinish={(order) => {
@ -204,7 +255,7 @@ export const FlowDetails = () => {
t("common:onDragStart", { item: item.displayName })
);
setDragged(item);
if (item.executionList && !item.isCollapsed) {
if (!item.isCollapsed) {
item.isCollapsed = true;
setExecutionList(executionList.clone());
}
@ -231,11 +282,62 @@ export const FlowDetails = () => {
setExecutionList(executionList.clone());
}}
onRowChange={update}
onAddExecution={addExecution}
onAddExecution={(execution, type) =>
addExecution(execution.displayName!, type)
}
onAddFlow={(flow) => addFlow(execution.displayName!, flow)}
onDelete={() => {
setSelectedExecution(execution);
toggleDeleteDialog();
}}
/>
))}
</>
</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">
{liveText}
</div>
@ -245,8 +347,12 @@ export const FlowDetails = () => {
<FlowDiagram executionList={executionList} />
)}
{!executionList?.expandableList ||
(executionList.expandableList.length === 0 && (
<EmptyExecutionState />
(flow && !hasExecutions && (
<EmptyExecutionState
flow={flow}
onAddExecution={(type) => addExecution(flow.alias!, type)}
onAddFlow={(newFlow) => addFlow(flow.alias!, newFlow)}
/>
))}
</PageSection>
</>

View file

@ -7,7 +7,7 @@ import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-ad
import type { ExpandableExecution } from "../execution-model";
import { AddStepModal, FlowType } from "./modals/AddStepModal";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { AddSubFlowModal } from "./modals/AddSubFlowModal";
import { AddSubFlowModal, Flow } from "./modals/AddSubFlowModal";
type EditFlowDropdownProps = {
execution: ExpandableExecution;
@ -15,11 +15,13 @@ type EditFlowDropdownProps = {
execution: ExpandableExecution,
type: AuthenticationProviderRepresentation
) => void;
onAddFlow: (flow: Flow) => void;
};
export const EditFlowDropdown = ({
execution,
onAddExecution,
onAddFlow,
}: EditFlowDropdownProps) => {
const { t } = useTranslation("authentication");
const adminClient = useAdminClient();
@ -83,7 +85,10 @@ export const EditFlowDropdown = ({
<AddSubFlowModal
name={execution.displayName!}
onCancel={() => setType(undefined)}
onConfirm={() => setType(undefined)}
onConfirm={(flow) => {
onAddFlow(flow);
setType(undefined);
}}
/>
)}
</>

View file

@ -139,7 +139,7 @@ const renderSubFlow = (
elements.push(createEdge(endSubFlowId, end.id!));
return elements.concat(
renderFlow(execution, execution.executionList, {
renderFlow(execution, execution.executionList || [], {
...execution,
id: endSubFlowId,
})

View file

@ -10,11 +10,14 @@ import {
DataListToggle,
Text,
TextVariants,
Button,
} from "@patternfly/react-core";
import { TrashIcon } from "@patternfly/react-icons";
import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation";
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
import type { ExpandableExecution } from "../execution-model";
import type { Flow } from "./modals/AddSubFlowModal";
import { FlowTitle } from "./FlowTitle";
import { FlowRequirementDropdown } from "./FlowRequirementDropdown";
import { ExecutionConfigModal } from "./ExecutionConfigModal";
@ -30,6 +33,8 @@ type FlowRowProps = {
execution: ExpandableExecution,
type: AuthenticationProviderRepresentation
) => void;
onAddFlow: (flow: Flow) => void;
onDelete: () => void;
};
export const FlowRow = ({
@ -37,6 +42,8 @@ export const FlowRow = ({
onRowClick,
onRowChange,
onAddExecution,
onAddFlow,
onDelete,
}: FlowRowProps) => {
const { t } = useTranslation("authentication");
const hasSubList = !!execution.executionList?.length;
@ -69,13 +76,13 @@ export const FlowRow = ({
<DataListItemCells
dataListCells={[
<DataListCell key={`${execution.id}-name`}>
{!hasSubList && (
{!execution.authenticationFlow && (
<FlowTitle
key={execution.id}
title={execution.displayName!}
/>
)}
{hasSubList && (
{execution.authenticationFlow && (
<>
{execution.displayName} <br />{" "}
<Text component={TextVariants.small}>
@ -98,26 +105,34 @@ export const FlowRow = ({
<EditFlowDropdown
execution={execution}
onAddExecution={onAddExecution}
onAddFlow={onAddFlow}
/>
)}
<Button
variant="plain"
aria-label={t("common:delete")}
onClick={onDelete}
>
<TrashIcon />
</Button>
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
{!execution.isCollapsed && hasSubList && (
<>
{execution.executionList.map((execution) => (
<FlowRow
key={execution.id}
execution={execution}
onRowClick={onRowClick}
onRowChange={onRowChange}
onAddExecution={onAddExecution}
/>
))}
</>
)}
{!execution.isCollapsed &&
hasSubList &&
execution.executionList?.map((execution) => (
<FlowRow
key={execution.id}
execution={execution}
onRowClick={onRowClick}
onRowChange={onRowChange}
onAddExecution={onAddExecution}
onAddFlow={onAddFlow}
onDelete={onDelete}
/>
))}
</>
);
};

View file

@ -1,6 +1,6 @@
import React from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
import { Controller, useForm } from "react-hook-form";
import {
Button,
ButtonVariant,
@ -8,25 +8,53 @@ import {
FormGroup,
Modal,
ModalVariant,
Select,
SelectOption,
SelectVariant,
TextInput,
ValidatedOptions,
} from "@patternfly/react-core";
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
import { HelpItem } from "../../../components/help-enabler/HelpItem";
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
type AddSubFlowProps = {
name: string;
onConfirm: () => void;
onConfirm: (flow: Flow) => 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 = ({
name,
onConfirm,
onCancel,
}: AddSubFlowProps) => {
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 (
<Modal
@ -39,7 +67,8 @@ export const AddSubFlowModal = ({
id="modal-add"
data-testid="modal-add"
key="add"
onClick={() => onConfirm()}
type="submit"
form="sub-flow-form"
>
{t("common:add")}
</Button>,
@ -56,11 +85,7 @@ export const AddSubFlowModal = ({
</Button>,
]}
>
<Form
id="execution-config-form"
isHorizontal
onSubmit={handleSubmit(onConfirm)}
>
<Form id="sub-flow-form" isHorizontal onSubmit={handleSubmit(onConfirm)}>
<FormGroup
label={t("common:name")}
fieldId="name"
@ -107,6 +132,100 @@ export const AddSubFlowModal = ({
ref={register}
/>
</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>
</Modal>
);

View file

@ -1,7 +1,7 @@
import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation";
export type ExpandableExecution = AuthenticationExecutionInfoRepresentation & {
executionList: ExpandableExecution[];
executionList?: ExpandableExecution[];
isCollapsed: boolean;
};
@ -34,10 +34,11 @@ export class ExecutionList {
constructor(list: AuthenticationExecutionInfoRepresentation[]) {
this.list = list as ExpandableExecution[];
this.expandableList = this.transformToExpandableList(0, 0, {
executionList: [],
isCollapsed: false,
}).executionList;
this.expandableList =
this.transformToExpandableList(0, 0, {
executionList: [],
isCollapsed: false,
}).executionList || [];
}
private transformToExpandableList(
@ -50,7 +51,7 @@ export class ExecutionList {
const nextRowLevel = this.list[index + 1]?.level || 0;
if (ex.level === level && nextRowLevel <= level) {
execution.executionList.push(ex);
execution.executionList?.push(ex);
} else if (ex.level === level && nextRowLevel > level) {
const expandable = this.transformToExpandableList(
nextRowLevel,
@ -61,7 +62,7 @@ export class ExecutionList {
isCollapsed: false,
}
);
execution.executionList.push(expandable);
execution.executionList?.push(expandable);
}
}
return execution;

View file

@ -17,6 +17,7 @@ import { useAdminClient } from "../../context/auth/AdminClient";
import { useAlerts } from "../../components/alert/Alerts";
import { NameDescription } from "./NameDescription";
import { FlowType } from "./FlowType";
import { toFlow } from "../routes/Flow";
export const CreateFlow = () => {
const { t } = useTranslation("authentication");
@ -32,8 +33,17 @@ export const CreateFlow = () => {
const save = async (flow: AuthenticationFlowRepresentation) => {
try {
await adminClient.authenticationManagement.createFlow(flow);
const { id } = (await adminClient.authenticationManagement.createFlow(
flow
)) as unknown as AuthenticationFlowRepresentation;
addAlert(t("flowCreatedSuccess"), AlertVariant.success);
history.push(
toFlow({
realm,
id: id!,
usedBy: "notInUse",
})
);
} catch (error: any) {
addAlert(
t("flowCreateError", {

View file

@ -10,7 +10,7 @@ import {
import { HelpItem } from "../../components/help-enabler/HelpItem";
const types = ["basic-flow", "client-flow"];
const TYPES = ["basic-flow", "client-flow"] as const;
export const FlowType = () => {
const { t } = useTranslation("authentication");
@ -23,7 +23,7 @@ export const FlowType = () => {
label={t("flowType")}
labelIcon={
<HelpItem
helpText="authentication-help:flowType"
helpText="authentication-help:topLevelFlowType"
forLabel={t("flowType")}
forID="flowType"
/>
@ -32,7 +32,7 @@ export const FlowType = () => {
>
<Controller
name="providerId"
defaultValue={types[0]}
defaultValue={TYPES[0]}
control={control}
render={({ onChange, value }) => (
<Select
@ -42,17 +42,15 @@ export const FlowType = () => {
onChange(value as string);
setOpen(false);
}}
selections={t(`flow-type.${value}`)}
selections={t(`top-level-flow-type.${value}`)}
variant={SelectVariant.single}
aria-label={t("flowType")}
isOpen={open}
>
{types.map((type) => (
<SelectOption
selected={type === value}
key={type}
value={t(`flow-type.${type}`)}
/>
{TYPES.map((type) => (
<SelectOption selected={type === value} key={type} value={type}>
{t(`top-level-flow-type.${type}`)}
</SelectOption>
))}
</Select>
)}

View file

@ -3,7 +3,8 @@ export default {
name: "Help text for the name 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",
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",
addExecution:
"Execution can have a wide range of actions, from sending a reset email to validating an OTP",

View file

@ -18,6 +18,11 @@ export default {
deleteFlowSuccess: "Flow successfully deleted",
deleteFlowError: "Could not delete flow: {{error}}",
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",
updateFlowError: "Could not update flow: {{error}}",
copyOf: "Copy of {{name}}",
@ -26,6 +31,10 @@ export default {
createFlow: "Create flow",
flowType: "Flow type",
"flow-type": {
"basic-flow": "Generic",
"form-flow": "Form",
},
"top-level-flow-type": {
"basic-flow": "Basic flow",
"client-flow": "Client flow",
},