Add workflows settings in start node (#1270)

This commit is contained in:
Shuchang Zheng
2024-11-26 10:41:18 -08:00
committed by GitHub
parent 61f94af7f3
commit e0aadac962
13 changed files with 275 additions and 42 deletions

View File

@@ -10,12 +10,13 @@ import {
type Props = { type Props = {
value: ProxyLocation | null; value: ProxyLocation | null;
onChange: (value: ProxyLocation) => void; onChange: (value: ProxyLocation) => void;
className?: string;
}; };
function ProxySelector({ value, onChange }: Props) { function ProxySelector({ value, onChange, className }: Props) {
return ( return (
<Select value={value ?? ""} onValueChange={onChange}> <Select value={value ?? ""} onValueChange={onChange}>
<SelectTrigger className="w-48"> <SelectTrigger className={className}>
<SelectValue placeholder="Proxy Location" /> <SelectValue placeholder="Proxy Location" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@@ -514,6 +514,7 @@ function CreateNewTaskForm({ initialValues }: Props) {
<ProxySelector <ProxySelector
value={field.value} value={field.value}
onChange={field.onChange} onChange={field.onChange}
className="w-48"
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -686,6 +686,7 @@ function SavedTaskForm({ initialValues }: Props) {
<ProxySelector <ProxySelector
value={field.value} value={field.value}
onChange={field.onChange} onChange={field.onChange}
className="w-48"
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -1,4 +1,7 @@
import { getClient } from "@/api/AxiosClient"; import { getClient } from "@/api/AxiosClient";
import { ProxyLocation } from "@/api/types";
import { ProxySelector } from "@/components/ProxySelector";
import { Button } from "@/components/ui/button";
import { import {
Form, Form,
FormControl, FormControl,
@@ -7,28 +10,29 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { Input } from "@/components/ui/input";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useForm } from "react-hook-form";
import { Link, useParams } from "react-router-dom";
import { WorkflowParameterInput } from "./WorkflowParameterInput";
import { Button } from "@/components/ui/button";
import { toast } from "@/components/ui/use-toast"; import { toast } from "@/components/ui/use-toast";
import { useApiCredential } from "@/hooks/useApiCredential";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { copyText } from "@/util/copyText";
import { apiBaseUrl } from "@/util/env";
import { CopyIcon, PlayIcon, ReloadIcon } from "@radix-ui/react-icons"; import { CopyIcon, PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
import { ToastAction } from "@radix-ui/react-toast"; import { ToastAction } from "@radix-ui/react-toast";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import fetchToCurl from "fetch-to-curl"; import fetchToCurl from "fetch-to-curl";
import { apiBaseUrl } from "@/util/env"; import { useForm } from "react-hook-form";
import { useApiCredential } from "@/hooks/useApiCredential"; import { Link, useParams } from "react-router-dom";
import { copyText } from "@/util/copyText";
import { WorkflowParameter } from "./types/workflowTypes";
import { Input } from "@/components/ui/input";
import { z } from "zod"; import { z } from "zod";
import { ProxyLocation } from "@/api/types"; import { WorkflowParameter } from "./types/workflowTypes";
import { ProxySelector } from "@/components/ProxySelector"; import { WorkflowParameterInput } from "./WorkflowParameterInput";
type Props = { type Props = {
workflowParameters: Array<WorkflowParameter>; workflowParameters: Array<WorkflowParameter>;
initialValues: Record<string, unknown>; initialValues: Record<string, unknown>;
initialSettings: {
proxyLocation: ProxyLocation;
webhookCallbackUrl: string;
};
}; };
function parseValuesForWorkflowRun( function parseValuesForWorkflowRun(
@@ -92,19 +96,23 @@ function getRunWorkflowRequestBody(
} }
type RunWorkflowFormType = Record<string, unknown> & { type RunWorkflowFormType = Record<string, unknown> & {
webhookCallbackUrl: string | null; webhookCallbackUrl: string;
proxyLocation: ProxyLocation | null; proxyLocation: ProxyLocation;
}; };
function RunWorkflowForm({ workflowParameters, initialValues }: Props) { function RunWorkflowForm({
workflowParameters,
initialValues,
initialSettings,
}: Props) {
const { workflowPermanentId } = useParams(); const { workflowPermanentId } = useParams();
const credentialGetter = useCredentialGetter(); const credentialGetter = useCredentialGetter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const form = useForm<RunWorkflowFormType>({ const form = useForm<RunWorkflowFormType>({
defaultValues: { defaultValues: {
...initialValues, ...initialValues,
webhookCallbackUrl: null, webhookCallbackUrl: initialSettings.webhookCallbackUrl,
proxyLocation: ProxyLocation.Residential, proxyLocation: initialSettings.proxyLocation,
}, },
}); });
const apiCredential = useApiCredential(); const apiCredential = useApiCredential();
@@ -313,6 +321,7 @@ function RunWorkflowForm({ workflowParameters, initialValues }: Props) {
<ProxySelector <ProxySelector
value={field.value} value={field.value}
onChange={field.onChange} onChange={field.onChange}
className="w-48"
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -5,6 +5,7 @@ import { useLocation, useParams } from "react-router-dom";
import { RunWorkflowForm } from "./RunWorkflowForm"; import { RunWorkflowForm } from "./RunWorkflowForm";
import { WorkflowApiResponse } from "./types/workflowTypes"; import { WorkflowApiResponse } from "./types/workflowTypes";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { ProxyLocation } from "@/api/types";
function WorkflowRunParameters() { function WorkflowRunParameters() {
const credentialGetter = useCredentialGetter(); const credentialGetter = useCredentialGetter();
@@ -93,6 +94,10 @@ function WorkflowRunParameters() {
<RunWorkflowForm <RunWorkflowForm
initialValues={initialValues} initialValues={initialValues}
workflowParameters={workflowParameters} workflowParameters={workflowParameters}
initialSettings={{
proxyLocation: workflow.proxy_location ?? ProxyLocation.Residential,
webhookCallbackUrl: workflow.webhook_callback_url ?? "",
}}
/> />
</div> </div>
); );

View File

@@ -37,6 +37,7 @@ import {
AWSSecretParameter, AWSSecretParameter,
WorkflowApiResponse, WorkflowApiResponse,
WorkflowParameterValueType, WorkflowParameterValueType,
WorkflowSettings,
} from "../types/workflowTypes"; } from "../types/workflowTypes";
import { import {
BitwardenLoginCredentialParameterYAML, BitwardenLoginCredentialParameterYAML,
@@ -50,7 +51,12 @@ import {
import { WorkflowHeader } from "./WorkflowHeader"; import { WorkflowHeader } from "./WorkflowHeader";
import { WorkflowParametersStateContext } from "./WorkflowParametersStateContext"; import { WorkflowParametersStateContext } from "./WorkflowParametersStateContext";
import { edgeTypes } from "./edges"; import { edgeTypes } from "./edges";
import { AppNode, nodeTypes, WorkflowBlockNode } from "./nodes"; import {
AppNode,
isWorkflowBlockNode,
nodeTypes,
WorkflowBlockNode,
} from "./nodes";
import { WorkflowNodeLibraryPanel } from "./panels/WorkflowNodeLibraryPanel"; import { WorkflowNodeLibraryPanel } from "./panels/WorkflowNodeLibraryPanel";
import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel"; import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel";
import "./reactFlowOverrideStyles.css"; import "./reactFlowOverrideStyles.css";
@@ -63,6 +69,7 @@ import {
getOutputParameterKey, getOutputParameterKey,
getWorkflowBlocks, getWorkflowBlocks,
getWorkflowErrors, getWorkflowErrors,
getWorkflowSettings,
layout, layout,
nodeAdderNode, nodeAdderNode,
startNode, startNode,
@@ -200,6 +207,7 @@ function FlowRenderer({
parameters: Array<ParameterYAML>; parameters: Array<ParameterYAML>;
blocks: Array<BlockYAML>; blocks: Array<BlockYAML>;
title: string; title: string;
settings: WorkflowSettings;
}) => { }) => {
if (!workflowPermanentId) { if (!workflowPermanentId) {
return; return;
@@ -208,8 +216,9 @@ function FlowRenderer({
const requestBody: WorkflowCreateYAMLRequest = { const requestBody: WorkflowCreateYAMLRequest = {
title: data.title, title: data.title,
description: workflow.description, description: workflow.description,
proxy_location: workflow.proxy_location, proxy_location: data.settings.proxyLocation,
webhook_callback_url: workflow.webhook_callback_url, webhook_callback_url: data.settings.webhookCallbackUrl,
persist_browser_session: data.settings.persistBrowserSession,
totp_verification_url: workflow.totp_verification_url, totp_verification_url: workflow.totp_verification_url,
workflow_definition: { workflow_definition: {
parameters: data.parameters, parameters: data.parameters,
@@ -267,6 +276,7 @@ function FlowRenderer({
async function handleSave() { async function handleSave() {
const blocks = getWorkflowBlocks(nodes, edges); const blocks = getWorkflowBlocks(nodes, edges);
const settings = getWorkflowSettings(nodes);
const parametersInYAMLConvertibleJSON = convertToParametersYAML(parameters); const parametersInYAMLConvertibleJSON = convertToParametersYAML(parameters);
const filteredParameters = workflow.workflow_definition.parameters.filter( const filteredParameters = workflow.workflow_definition.parameters.filter(
(parameter) => { (parameter) => {
@@ -295,6 +305,7 @@ function FlowRenderer({
], ],
blocks, blocks,
title, title,
settings,
}); });
} }
@@ -308,10 +319,13 @@ function FlowRenderer({
const newNodes: Array<AppNode> = []; const newNodes: Array<AppNode> = [];
const newEdges: Array<Edge> = []; const newEdges: Array<Edge> = [];
const id = nanoid(); const id = nanoid();
const existingLabels = nodes
.filter(isWorkflowBlockNode)
.map((node) => node.data.label);
const node = createNode( const node = createNode(
{ id, parentId: parent }, { id, parentId: parent },
nodeType, nodeType,
generateNodeLabel(nodes.map((node) => node.data.label)), generateNodeLabel(existingLabels),
); );
newNodes.push(node); newNodes.push(node);
if (previous) { if (previous) {
@@ -343,7 +357,9 @@ function FlowRenderer({
// when loop node is first created it needs an adder node so nodes can be added inside the loop // when loop node is first created it needs an adder node so nodes can be added inside the loop
const startNodeId = nanoid(); const startNodeId = nanoid();
const adderNodeId = nanoid(); const adderNodeId = nanoid();
newNodes.push(startNode(startNodeId, id)); newNodes.push(
startNode(startNodeId, { withWorkflowSettings: false }, id),
);
newNodes.push(nodeAdderNode(adderNodeId, id)); newNodes.push(nodeAdderNode(adderNodeId, id));
newEdges.push(defaultEdge(startNodeId, adderNodeId)); newEdges.push(defaultEdge(startNodeId, adderNodeId));
} }
@@ -370,7 +386,7 @@ function FlowRenderer({
function deleteNode(id: string) { function deleteNode(id: string) {
const node = nodes.find((node) => node.id === id); const node = nodes.find((node) => node.id === id);
if (!node) { if (!node || !isWorkflowBlockNode(node)) {
return; return;
} }
const deletedNodeLabel = node.data.label; const deletedNodeLabel = node.data.label;

View File

@@ -7,6 +7,7 @@ import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
import { FlowRenderer } from "./FlowRenderer"; import { FlowRenderer } from "./FlowRenderer";
import { getElements } from "./workflowEditorUtils"; import { getElements } from "./workflowEditorUtils";
import { LogoMinimized } from "@/components/LogoMinimized"; import { LogoMinimized } from "@/components/LogoMinimized";
import { WorkflowSettings } from "../types/workflowTypes";
function WorkflowEditor() { function WorkflowEditor() {
const { workflowPermanentId } = useParams(); const { workflowPermanentId } = useParams();
@@ -39,7 +40,13 @@ function WorkflowEditor() {
return null; return null;
} }
const elements = getElements(workflow.workflow_definition.blocks); const settings: WorkflowSettings = {
persistBrowserSession: workflow.persist_browser_session,
proxyLocation: workflow.proxy_location,
webhookCallbackUrl: workflow.webhook_callback_url,
};
const elements = getElements(workflow.workflow_definition.blocks, settings);
return ( return (
<div className="h-screen w-full"> <div className="h-screen w-full">

View File

@@ -1,6 +1,109 @@
import { Handle, Position } from "@xyflow/react"; import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
import type { StartNode } from "./types";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { useState } from "react";
import { ProxyLocation } from "@/api/types";
import { Label } from "@/components/ui/label";
import { HelpTooltip } from "@/components/HelpTooltip";
import { Input } from "@/components/ui/input";
import { ProxySelector } from "@/components/ProxySelector";
import { Switch } from "@/components/ui/switch";
import { Separator } from "@/components/ui/separator";
function StartNode({ id, data }: NodeProps<StartNode>) {
const { updateNodeData } = useReactFlow();
const [inputs, setInputs] = useState({
webhookCallbackUrl: data.withWorkflowSettings
? data.webhookCallbackUrl
: "",
proxyLocation: data.withWorkflowSettings
? data.proxyLocation
: ProxyLocation.Residential,
persistBrowserSession: data.withWorkflowSettings
? data.persistBrowserSession
: false,
});
function handleChange(key: string, value: unknown) {
setInputs({ ...inputs, [key]: value });
updateNodeData(id, { [key]: value });
}
if (data.withWorkflowSettings) {
return (
<div>
<Handle
type="source"
position={Position.Bottom}
id="a"
className="opacity-0"
/>
<div className="w-[30rem] rounded-lg bg-slate-elevation3 px-6 py-4 text-center">
<div className="space-y-4">
<header>Start</header>
<Separator />
<Accordion type="single" collapsible>
<AccordionItem value="settings" className="border-b-0">
<AccordionTrigger className="py-2">
Workflow Settings
</AccordionTrigger>
<AccordionContent className="pl-6 pr-1 pt-1">
<div className="space-y-4">
<div className="space-y-2">
<div className="flex gap-2">
<Label>Webhook Callback URL</Label>
<HelpTooltip content="The URL of a webhook endpoint to send the workflow results" />
</div>
<Input
value={inputs.webhookCallbackUrl}
placeholder="https://"
onChange={(event) => {
handleChange(
"webhookCallbackUrl",
event.target.value,
);
}}
/>
</div>
<div className="space-y-2">
<div className="flex gap-2">
<Label>Proxy Location</Label>
<HelpTooltip content="Route Skyvern through one of our available proxies." />
</div>
<ProxySelector
value={inputs.proxyLocation}
onChange={(value) => {
handleChange("proxyLocation", value);
}}
/>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label>Persist Browser Session</Label>
<HelpTooltip content="Persist session information across workflow runs" />
<Switch
checked={inputs.persistBrowserSession}
onCheckedChange={(value) => {
handleChange("persistBrowserSession", value);
}}
/>
</div>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
</div>
);
}
function StartNode() {
return ( return (
<div> <div>
<Handle <Handle

View File

@@ -1,5 +1,28 @@
import { ProxyLocation } from "@/api/types";
import type { Node } from "@xyflow/react"; import type { Node } from "@xyflow/react";
import { AppNode } from "..";
export type StartNodeData = Record<string, never>; export type WorkflowStartNodeData = {
withWorkflowSettings: true;
webhookCallbackUrl: string;
proxyLocation: ProxyLocation;
persistBrowserSession: boolean;
};
export type OtherStartNodeData = {
withWorkflowSettings: false;
};
export type StartNodeData = WorkflowStartNodeData | OtherStartNodeData;
export type StartNode = Node<StartNodeData, "start">; export type StartNode = Node<StartNodeData, "start">;
export function isStartNode(node: AppNode): node is StartNode {
return node.type === "start";
}
export function isWorkflowStartNodeData(
data: StartNodeData,
): data is WorkflowStartNodeData {
return data.withWorkflowSettings;
}

View File

@@ -9,6 +9,7 @@ import type {
WorkflowApiResponse, WorkflowApiResponse,
WorkflowBlock, WorkflowBlock,
WorkflowParameterValueType, WorkflowParameterValueType,
WorkflowSettings,
} from "../types/workflowTypes"; } from "../types/workflowTypes";
import { import {
ActionBlockYAML, ActionBlockYAML,
@@ -53,7 +54,12 @@ import {
} from "./nodes/LoopNode/types"; } from "./nodes/LoopNode/types";
import { NodeAdderNode } from "./nodes/NodeAdderNode/types"; import { NodeAdderNode } from "./nodes/NodeAdderNode/types";
import { sendEmailNodeDefaultData } from "./nodes/SendEmailNode/types"; import { sendEmailNodeDefaultData } from "./nodes/SendEmailNode/types";
import { StartNode } from "./nodes/StartNode/types"; import {
isStartNode,
isWorkflowStartNodeData,
StartNode,
StartNodeData,
} from "./nodes/StartNode/types";
import { isTaskNode, taskNodeDefaultData } from "./nodes/TaskNode/types"; import { isTaskNode, taskNodeDefaultData } from "./nodes/TaskNode/types";
import { textPromptNodeDefaultData } from "./nodes/TextPromptNode/types"; import { textPromptNodeDefaultData } from "./nodes/TextPromptNode/types";
import { NodeBaseData } from "./nodes/types"; import { NodeBaseData } from "./nodes/types";
@@ -73,6 +79,7 @@ import {
} from "./nodes/ExtractionNode/types"; } from "./nodes/ExtractionNode/types";
import { loginNodeDefaultData } from "./nodes/LoginNode/types"; import { loginNodeDefaultData } from "./nodes/LoginNode/types";
import { waitNodeDefaultData } from "./nodes/WaitNode/types"; import { waitNodeDefaultData } from "./nodes/WaitNode/types";
import { ProxyLocation } from "@/api/types";
export const NEW_NODE_LABEL_PREFIX = "block_"; export const NEW_NODE_LABEL_PREFIX = "block_";
@@ -458,12 +465,16 @@ export function edgeWithAddButton(source: string, target: string) {
}; };
} }
export function startNode(id: string, parentId?: string): StartNode { export function startNode(
id: string,
data: StartNodeData,
parentId?: string,
): StartNode {
const node: StartNode = { const node: StartNode = {
id, id,
type: "start", type: "start",
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
data: {}, data,
draggable: false, draggable: false,
connectable: false, connectable: false,
}; };
@@ -488,7 +499,10 @@ export function nodeAdderNode(id: string, parentId?: string): NodeAdderNode {
return node; return node;
} }
function getElements(blocks: Array<WorkflowBlock>): { function getElements(
blocks: Array<WorkflowBlock>,
settings: WorkflowSettings,
): {
nodes: Array<AppNode>; nodes: Array<AppNode>;
edges: Array<Edge>; edges: Array<Edge>;
} { } {
@@ -497,7 +511,14 @@ function getElements(blocks: Array<WorkflowBlock>): {
const edges: Array<Edge> = []; const edges: Array<Edge> = [];
const startNodeId = nanoid(); const startNodeId = nanoid();
nodes.push(startNode(startNodeId)); nodes.push(
startNode(startNodeId, {
withWorkflowSettings: true,
persistBrowserSession: settings.persistBrowserSession,
proxyLocation: settings.proxyLocation ?? ProxyLocation.Residential,
webhookCallbackUrl: settings.webhookCallbackUrl ?? "",
}),
);
data.forEach((d, index) => { data.forEach((d, index) => {
const node = convertToNode( const node = convertToNode(
@@ -519,7 +540,15 @@ function getElements(blocks: Array<WorkflowBlock>): {
const loopBlocks = data.filter((d) => d.block.block_type === "for_loop"); const loopBlocks = data.filter((d) => d.block.block_type === "for_loop");
loopBlocks.forEach((block) => { loopBlocks.forEach((block) => {
const startNodeId = nanoid(); const startNodeId = nanoid();
nodes.push(startNode(startNodeId, block.id)); nodes.push(
startNode(
startNodeId,
{
withWorkflowSettings: false,
},
block.id,
),
);
const children = data.filter((b) => b.parentId === block.id); const children = data.filter((b) => b.parentId === block.id);
if (children.length === 0) { if (children.length === 0) {
const adderNodeId = nanoid(); const adderNodeId = nanoid();
@@ -555,7 +584,7 @@ function createNode(
identifiers: { id: string; parentId?: string }, identifiers: { id: string; parentId?: string },
nodeType: NonNullable<WorkflowBlockNode["type"]>, nodeType: NonNullable<WorkflowBlockNode["type"]>,
label: string, label: string,
): AppNode { ): WorkflowBlockNode {
const common = { const common = {
draggable: false, draggable: false,
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
@@ -991,6 +1020,30 @@ function getWorkflowBlocks(
return getWorkflowBlocksUtil(nodes, edges); return getWorkflowBlocksUtil(nodes, edges);
} }
function getWorkflowSettings(nodes: Array<AppNode>): WorkflowSettings {
const defaultSettings = {
persistBrowserSession: false,
proxyLocation: ProxyLocation.Residential,
webhookCallbackUrl: null,
};
const startNodes = nodes.filter(isStartNode);
const startNodeWithWorkflowSettings = startNodes.find(
(node) => node.data.withWorkflowSettings,
);
if (!startNodeWithWorkflowSettings) {
return defaultSettings;
}
const data = startNodeWithWorkflowSettings.data;
if (isWorkflowStartNodeData(data)) {
return {
persistBrowserSession: data.persistBrowserSession,
proxyLocation: data.proxyLocation,
webhookCallbackUrl: data.webhookCallbackUrl,
};
}
return defaultSettings;
}
function generateNodeLabel(existingLabels: Array<string>) { function generateNodeLabel(existingLabels: Array<string>) {
for (let i = 1; i < existingLabels.length + 2; i++) { for (let i = 1; i < existingLabels.length + 2; i++) {
const label = NEW_NODE_LABEL_PREFIX + i; const label = NEW_NODE_LABEL_PREFIX + i;
@@ -1608,6 +1661,7 @@ export {
getBlockNameOfOutputParameterKey, getBlockNameOfOutputParameterKey,
getDefaultValueForParameterType, getDefaultValueForParameterType,
getElements, getElements,
getWorkflowSettings,
getOutputParameterKey, getOutputParameterKey,
getPreviousNodeIds, getPreviousNodeIds,
getUniqueLabelForExistingNode, getUniqueLabelForExistingNode,

View File

@@ -1,5 +1,5 @@
import { useNodes, useReactFlow } from "@xyflow/react"; import { useNodes, useReactFlow } from "@xyflow/react";
import { AppNode } from "../editor/nodes"; import { AppNode, isWorkflowBlockNode } from "../editor/nodes";
import { import {
getUniqueLabelForExistingNode, getUniqueLabelForExistingNode,
getUpdatedNodesAfterLabelUpdateForParameterKeys, getUpdatedNodesAfterLabelUpdateForParameterKeys,
@@ -21,7 +21,9 @@ function useNodeLabelChangeHandler({ id, initialValue }: Props) {
useWorkflowParametersState(); useWorkflowParametersState();
function handleLabelChange(value: string) { function handleLabelChange(value: string) {
const existingLabels = nodes.map((n) => n.data.label); const existingLabels = nodes
.filter(isWorkflowBlockNode)
.map((n) => n.data.label);
const labelWithoutWhitespace = value.replace(/\s+/g, "_"); const labelWithoutWhitespace = value.replace(/\s+/g, "_");
const newLabel = getUniqueLabelForExistingNode( const newLabel = getUniqueLabelForExistingNode(
labelWithoutWhitespace, labelWithoutWhitespace,

View File

@@ -1,3 +1,5 @@
import { ProxyLocation } from "@/api/types";
export type WorkflowParameterBase = { export type WorkflowParameterBase = {
parameter_type: WorkflowParameterType; parameter_type: WorkflowParameterType;
key: string; key: string;
@@ -296,14 +298,22 @@ export type WorkflowApiResponse = {
version: number; version: number;
description: string; description: string;
workflow_definition: WorkflowDefinition; workflow_definition: WorkflowDefinition;
proxy_location: string; proxy_location: ProxyLocation | null;
webhook_callback_url: string; webhook_callback_url: string | null;
totp_verification_url: string; persist_browser_session: boolean;
totp_verification_url: string | null;
totp_identifier: string | null;
created_at: string; created_at: string;
modified_at: string; modified_at: string;
deleted_at: string | null; deleted_at: string | null;
}; };
export type WorkflowSettings = {
proxyLocation: ProxyLocation | null;
webhookCallbackUrl: string | null;
persistBrowserSession: boolean;
};
export function isOutputParameter( export function isOutputParameter(
parameter: Parameter, parameter: Parameter,
): parameter is OutputParameter { ): parameter is OutputParameter {

View File

@@ -3,6 +3,7 @@ export type WorkflowCreateYAMLRequest = {
description?: string | null; description?: string | null;
proxy_location?: string | null; proxy_location?: string | null;
webhook_callback_url?: string | null; webhook_callback_url?: string | null;
persist_browser_session?: boolean;
totp_verification_url?: string | null; totp_verification_url?: string | null;
workflow_definition: WorkflowDefinitionYAML; workflow_definition: WorkflowDefinitionYAML;
is_saved_task?: boolean; is_saved_task?: boolean;