refactor: Update Component Port and Return Value Viewers for improved styling and accessibility

- Enhanced styling for ComponentPortViewer and ComponentReturnValueViewer components, including updated typography and layout.
- Replaced ErrorMessage with inline messages for better user experience when no ports or return values are present.
- Improved the visual hierarchy and spacing in the ComponentSettingsModal, adding sections for Identity, Documentation, Configuration, Connections, and Output.
- Refactored ComponentsModal to streamline component selection and improve search functionality with better UI elements.
- Updated Workflow component styles for a more modern look, including adjustments to edge styles and background settings.
This commit is contained in:
Nawaz Dhandala
2026-03-26 07:47:44 +00:00
parent 7fac485049
commit 9f8891de88
7 changed files with 1202 additions and 506 deletions

View File

@@ -13,7 +13,6 @@ import ComponentMetadata, {
NodeType,
} from "Common/Types/Workflow/Component";
import Button, { ButtonStyleType } from "Common/UI/Components/Button/Button";
import Card from "Common/UI/Components/Card/Card";
import ComponentLoader from "Common/UI/Components/ComponentLoader/ComponentLoader";
import ConfirmModal from "Common/UI/Components/Modal/ConfirmModal";
import { loadComponentsAndCategories } from "Common/UI/Components/Workflow/Utils";
@@ -231,11 +230,11 @@ const Delete: FunctionComponent<PageComponentProps> = (): ReactElement => {
},
});
setSaveStatus("Changes Saved.");
setSaveStatus("Saved");
} catch (err) {
setError(API.getFriendlyMessage(err));
setSaveStatus("Save Error.");
setSaveStatus("Error saving");
}
if (saveTimeout) {
@@ -250,100 +249,146 @@ const Delete: FunctionComponent<PageComponentProps> = (): ReactElement => {
await loadGraph();
}, []);
type GetSaveStatusColorFunction = () => string;
const getSaveStatusColor: GetSaveStatusColorFunction = (): string => {
if (saveStatus === "Saved") {
return "#10b981";
}
if (saveStatus === "Error saving") {
return "#ef4444";
}
return "#94a3b8";
};
return (
<Fragment>
<>
<Card
title={"Workflow Builder"}
description={"Workflow builder for OneUptime"}
rightElement={
<div className="flex">
<p className="text-sm text-gray-400 mr-3 mt-2">{saveStatus}</p>
<div className="hidden md:block">
<Button
title="Watch Demo"
icon={IconProp.Play}
buttonStyle={ButtonStyleType.OUTLINE}
onClick={() => {
Navigation.navigate(
URL.fromString("https://youtu.be/k1-reCQTZnM"),
{
openInNewTab: true,
},
);
}}
/>
</div>
<div>
<Button
title="Add Component"
icon={IconProp.Add}
onClick={() => {
setShowComponentPickerModal(true);
}}
/>
</div>
<div>
<Button
title="Run Workflow Manually"
icon={IconProp.Play}
onClick={() => {
setShowRunModal(true);
}}
/>
</div>
</div>
}
{/* Toolbar */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "0.75rem 1rem",
backgroundColor: "#ffffff",
borderRadius: "10px",
border: "1px solid #e2e8f0",
marginBottom: "0.75rem",
boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.03)",
}}
>
{isLoading ? <ComponentLoader /> : <></>}
{!isLoading ? (
<Workflow
workflowId={modelId}
showComponentsPickerModal={showComponentPickerModal}
onComponentPickerModalUpdate={(value: boolean) => {
setShowComponentPickerModal(value);
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "0.375rem",
}}
initialNodes={nodes}
onRunModalUpdate={(value: boolean) => {
setShowRunModal(value);
}}
showRunModal={showRunModal}
initialEdges={edges}
onWorkflowUpdated={async (
nodes: Array<Node>,
edges: Array<Edge>,
) => {
setNodes(nodes);
setEdges(edges);
await saveGraph(nodes, edges);
}}
onRun={async (component: NodeDataProp) => {
try {
const result: HTTPErrorResponse | HTTPResponse<JSONObject> =
await API.post({
url: URL.fromString(WORKFLOW_URL.toString()).addRoute(
"/manual/run/" + modelId.toString(),
),
data: {
data: component.arguments,
},
});
>
<div
style={{
width: "7px",
height: "7px",
borderRadius: "50%",
backgroundColor: getSaveStatusColor(),
transition: "background-color 0.3s ease",
}}
/>
<span
style={{
fontSize: "0.75rem",
color: getSaveStatusColor(),
fontWeight: 500,
transition: "color 0.3s ease",
}}
>
{saveStatus || "Ready"}
</span>
</div>
</div>
if (result instanceof HTTPErrorResponse) {
throw result;
}
setShowRunSuccessConfirmation(true);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
<Button
title="Add Component"
icon={IconProp.Add}
buttonStyle={ButtonStyleType.OUTLINE}
onClick={() => {
setShowComponentPickerModal(true);
}}
/>
) : (
<></>
)}
</Card>
<Button
title="Run Workflow"
icon={IconProp.Play}
buttonStyle={ButtonStyleType.SUCCESS_OUTLINE}
onClick={() => {
setShowRunModal(true);
}}
/>
</div>
</div>
{/* Canvas */}
{isLoading ? (
<div
style={{
height: "calc(100vh - 280px)",
minHeight: "500px",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#ffffff",
borderRadius: "10px",
border: "1px solid #e2e8f0",
}}
>
<ComponentLoader />
</div>
) : (
<Workflow
workflowId={modelId}
showComponentsPickerModal={showComponentPickerModal}
onComponentPickerModalUpdate={(value: boolean) => {
setShowComponentPickerModal(value);
}}
initialNodes={nodes}
onRunModalUpdate={(value: boolean) => {
setShowRunModal(value);
}}
showRunModal={showRunModal}
initialEdges={edges}
onWorkflowUpdated={async (
nodes: Array<Node>,
edges: Array<Edge>,
) => {
setNodes(nodes);
setEdges(edges);
await saveGraph(nodes, edges);
}}
onRun={async (component: NodeDataProp) => {
try {
const result: HTTPErrorResponse | HTTPResponse<JSONObject> =
await API.post({
url: URL.fromString(WORKFLOW_URL.toString()).addRoute(
"/manual/run/" + modelId.toString(),
),
data: {
data: component.arguments,
},
});
if (result instanceof HTTPErrorResponse) {
throw result;
}
setShowRunSuccessConfirmation(true);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
}}
/>
)}
{error && (
<ConfirmModal
title={`Error`}
@@ -358,9 +403,9 @@ const Delete: FunctionComponent<PageComponentProps> = (): ReactElement => {
{showRunSuccessConfirmation && (
<ConfirmModal
title={`Workflow scheduled to execute`}
description={`This workflow is scheduled to execute soon. You can see the status of the run in the Runs and Logs section.`}
submitButtonText={"Close"}
title={`Workflow Triggered`}
description={`Your workflow has been scheduled to execute. Check the Logs tab to monitor the run.`}
submitButtonText={"Got it"}
onSubmit={() => {
setShowRunSuccessConfirmation(false);
}}

View File

@@ -1,7 +1,5 @@
import Icon, { ThickProp } from "../Icon/Icon";
import Pill from "../Pill/Pill";
import Tooltip from "../Tooltip/Tooltip";
import { Green } from "../../../Types/BrandColors";
import IconProp from "../../../Types/Icon/IconProp";
import {
ComponentType,
@@ -17,106 +15,269 @@ export interface ComponentProps {
selected: boolean;
}
type CategoryColorScheme = {
bg: string;
border: string;
headerBg: string;
headerText: string;
iconColor: string;
selectedBorder: string;
selectedShadow: string;
handleBg: string;
handleBorder: string;
};
const getCategoryColors = (
category: string,
componentType: ComponentType,
): CategoryColorScheme => {
if (componentType === ComponentType.Trigger) {
return {
bg: "#fefce8",
border: "#fde68a",
headerBg: "linear-gradient(135deg, #f59e0b, #d97706)",
headerText: "#ffffff",
iconColor: "#ffffff",
selectedBorder: "#f59e0b",
selectedShadow: "0 0 0 3px rgba(245, 158, 11, 0.2)",
handleBg: "#f59e0b",
handleBorder: "#d97706",
};
}
const lowerCategory: string = category.toLowerCase();
if (lowerCategory.includes("condition") || lowerCategory.includes("logic")) {
return {
bg: "#faf5ff",
border: "#e9d5ff",
headerBg: "linear-gradient(135deg, #a855f7, #7c3aed)",
headerText: "#ffffff",
iconColor: "#ffffff",
selectedBorder: "#a855f7",
selectedShadow: "0 0 0 3px rgba(168, 85, 247, 0.2)",
handleBg: "#a855f7",
handleBorder: "#7c3aed",
};
}
if (lowerCategory.includes("api") || lowerCategory.includes("webhook")) {
return {
bg: "#eff6ff",
border: "#bfdbfe",
headerBg: "linear-gradient(135deg, #3b82f6, #2563eb)",
headerText: "#ffffff",
iconColor: "#ffffff",
selectedBorder: "#3b82f6",
selectedShadow: "0 0 0 3px rgba(59, 130, 246, 0.2)",
handleBg: "#3b82f6",
handleBorder: "#2563eb",
};
}
if (
lowerCategory.includes("slack") ||
lowerCategory.includes("discord") ||
lowerCategory.includes("teams") ||
lowerCategory.includes("telegram") ||
lowerCategory.includes("email") ||
lowerCategory.includes("notification")
) {
return {
bg: "#ecfdf5",
border: "#a7f3d0",
headerBg: "linear-gradient(135deg, #10b981, #059669)",
headerText: "#ffffff",
iconColor: "#ffffff",
selectedBorder: "#10b981",
selectedShadow: "0 0 0 3px rgba(16, 185, 129, 0.2)",
handleBg: "#10b981",
handleBorder: "#059669",
};
}
if (
lowerCategory.includes("code") ||
lowerCategory.includes("javascript") ||
lowerCategory.includes("custom")
) {
return {
bg: "#fef2f2",
border: "#fecaca",
headerBg: "linear-gradient(135deg, #ef4444, #dc2626)",
headerText: "#ffffff",
iconColor: "#ffffff",
selectedBorder: "#ef4444",
selectedShadow: "0 0 0 3px rgba(239, 68, 68, 0.2)",
handleBg: "#ef4444",
handleBorder: "#dc2626",
};
}
if (lowerCategory.includes("json") || lowerCategory.includes("util")) {
return {
bg: "#f0fdf4",
border: "#bbf7d0",
headerBg: "linear-gradient(135deg, #22c55e, #16a34a)",
headerText: "#ffffff",
iconColor: "#ffffff",
selectedBorder: "#22c55e",
selectedShadow: "0 0 0 3px rgba(34, 197, 94, 0.2)",
handleBg: "#22c55e",
handleBorder: "#16a34a",
};
}
if (
lowerCategory.includes("schedule") ||
lowerCategory.includes("cron") ||
lowerCategory.includes("timer")
) {
return {
bg: "#fff7ed",
border: "#fed7aa",
headerBg: "linear-gradient(135deg, #f97316, #ea580c)",
headerText: "#ffffff",
iconColor: "#ffffff",
selectedBorder: "#f97316",
selectedShadow: "0 0 0 3px rgba(249, 115, 22, 0.2)",
handleBg: "#f97316",
handleBorder: "#ea580c",
};
}
// Default / database models
return {
bg: "#f8fafc",
border: "#e2e8f0",
headerBg: "linear-gradient(135deg, #6366f1, #4f46e5)",
headerText: "#ffffff",
iconColor: "#ffffff",
selectedBorder: "#6366f1",
selectedShadow: "0 0 0 3px rgba(99, 102, 241, 0.2)",
handleBg: "#6366f1",
handleBorder: "#4f46e5",
};
};
type GetPortPositionFunction = (
portCount: number,
totalPorts: number,
isLabel: boolean,
) => React.CSSProperties;
const getPortPosition: GetPortPositionFunction = (
portCount: number,
totalPorts: number,
isLabel: boolean,
): React.CSSProperties => {
if (portCount === 1 && totalPorts === 1) {
return isLabel ? { left: 120 } : {};
}
if (portCount === 1 && totalPorts === 2) {
return { left: isLabel ? 70 : 80 };
}
if (portCount === 2 && totalPorts === 2) {
return { left: isLabel ? 170 : 180 };
}
if (portCount === 1 && totalPorts === 3) {
return { left: isLabel ? 40 : 50 };
}
if (portCount === 2 && totalPorts === 3) {
return isLabel ? { left: 120 } : {};
}
if (portCount === 3 && totalPorts === 3) {
return { left: isLabel ? 200 : 210 };
}
return {};
};
const Node: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
const [isHovering, setIsHovering] = useState<boolean>(false);
let textColor: string = "#6b7280";
let descriptionColor: string = "#6b7280";
if (isHovering) {
textColor = "#111827";
descriptionColor = "#111827";
}
let componentStyle: React.CSSProperties = {
width: "15rem",
height: "10rem",
padding: "1rem",
borderColor: props.selected ? "#6366f1" : textColor,
alignItems: "center",
borderRadius: "0.25rem",
borderWidth: "2px",
backgroundColor: "white",
display: "inline-block",
verticalAlign: "middle",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
};
let handleStyle: React.CSSProperties = {
background: "#6b7280",
height: "0.75rem",
width: "0.75rem",
};
type GetPortPositionFunction = (
portCount: number,
totalPorts: number,
isLabel: boolean,
) => React.CSSProperties;
const getPortPosition: GetPortPositionFunction = (
portCount: number,
totalPorts: number,
isLabel: boolean,
): React.CSSProperties => {
if (portCount === 1 && totalPorts === 1) {
return isLabel ? { left: 100 } : {};
}
if (portCount === 1 && totalPorts === 2) {
return { left: isLabel ? 70 : 80 };
}
if (portCount === 2 && totalPorts === 2) {
return { left: isLabel ? 150 : 160 };
}
if (portCount === 1 && totalPorts === 3) {
return { left: isLabel ? 70 : 80 };
}
if (portCount === 2 && totalPorts === 3) {
return isLabel ? { left: 100 } : {};
}
if (portCount === 3 && totalPorts === 3) {
return { left: isLabel ? 150 : 160 };
}
// default
return {};
};
const colors: CategoryColorScheme = getCategoryColors(
props.data.metadata.category || "",
props.data.metadata.componentType,
);
// Placeholder node
if (props.data.nodeType === NodeType.PlaceholderNode) {
handleStyle = {
background: "#cbd5e1",
height: "0.75rem",
width: "0.75rem",
};
componentStyle = {
borderStyle: "dashed",
width: "15rem",
height: "8rem",
padding: "1rem",
display: "inline-block",
alignItems: "center",
verticalAlign: "middle",
borderColor: isHovering ? "#94a3b8" : "#cbd5e1",
borderRadius: "0.25rem",
borderWidth: "2px",
backgroundColor: "white",
};
textColor = "#cbd5e1";
descriptionColor = "#cbd5e1";
if (isHovering) {
textColor = "#94a3b8";
descriptionColor = "#94a3b8";
}
return (
<div
className="cursor-pointer"
onMouseOver={() => {
setIsHovering(true);
}}
onMouseOut={() => {
setIsHovering(false);
}}
style={{
width: "16rem",
borderRadius: "12px",
border: `2px dashed ${isHovering ? "#94a3b8" : "#cbd5e1"}`,
backgroundColor: isHovering ? "#f8fafc" : "#ffffff",
padding: "1.5rem",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: "0.75rem",
transition: "all 0.2s ease",
minHeight: "7rem",
}}
onClick={() => {
if (props.data.onClick) {
props.data.onClick(props.data);
}
}}
>
<div
style={{
width: "2.5rem",
height: "2.5rem",
borderRadius: "50%",
backgroundColor: isHovering ? "#e2e8f0" : "#f1f5f9",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "all 0.2s ease",
}}
>
<Icon
icon={IconProp.Add}
style={{
color: isHovering ? "#64748b" : "#94a3b8",
width: "1.25rem",
height: "1.25rem",
transition: "all 0.2s ease",
}}
/>
</div>
<p
style={{
color: isHovering ? "#64748b" : "#94a3b8",
fontSize: "0.8125rem",
fontWeight: 500,
textAlign: "center",
margin: 0,
transition: "all 0.2s ease",
}}
>
{props.data.metadata.description || "Click to add trigger"}
</p>
</div>
);
}
// Regular node
const hasError: boolean = Boolean(props.data.error);
return (
<div
className="cursor-pointer"
@@ -127,8 +288,27 @@ const Node: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
setIsHovering(false);
}}
style={{
...componentStyle,
height: props.data.id ? "12rem" : "10rem",
width: "16rem",
borderRadius: "12px",
border: `2px solid ${
hasError
? "#fca5a5"
: props.selected
? colors.selectedBorder
: isHovering
? colors.selectedBorder
: colors.border
}`,
backgroundColor: "#ffffff",
overflow: "hidden",
boxShadow: props.selected
? colors.selectedShadow
: isHovering
? `0 8px 25px -5px rgba(0, 0, 0, 0.1), 0 4px 10px -6px rgba(0, 0, 0, 0.05)`
: `0 1px 3px 0 rgba(0, 0, 0, 0.07), 0 1px 2px -1px rgba(0, 0, 0, 0.05)`,
transition: "all 0.2s ease",
transform: isHovering ? "translateY(-1px)" : "none",
position: "relative",
}}
onClick={() => {
if (props.data.onClick) {
@@ -136,42 +316,7 @@ const Node: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
}
}}
>
<div className="flex justify-center">
{props.data.metadata.componentType === ComponentType.Trigger &&
props.data.nodeType !== NodeType.PlaceholderNode &&
!props.data.isPreview && <Pill text="Trigger" color={Green} />}
</div>
{!props.data.isPreview &&
props.data.error &&
props.data.nodeType !== NodeType.PlaceholderNode && (
<div
style={{
width: "20px",
height: "20px",
borderRadius: "100px",
color: "#ef4444",
position: "absolute",
top: "0px",
left: "220px",
cursor: "pointer",
}}
onClick={() => {}}
>
<Icon
icon={IconProp.Alert}
style={{
color: "#ef4444",
width: "1rem",
height: "1rem",
textAlign: "center",
margin: "auto",
marginTop: "2px",
}}
thick={ThickProp.Thick}
/>
</div>
)}
{/* In Ports (top handles) */}
{!props.data.isPreview &&
props.data.metadata.componentType !== ComponentType.Trigger && (
<div>
@@ -187,7 +332,12 @@ const Node: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
isConnectable={true}
position={Position.Top}
style={{
...handleStyle,
background: colors.handleBg,
height: "10px",
width: "10px",
border: `2px solid ${colors.handleBorder}`,
top: "-5px",
transition: "all 0.15s ease",
...getPortPosition(
i + 1,
props.data.metadata.inPorts.length,
@@ -200,117 +350,198 @@ const Node: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
</div>
)}
<div
style={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{/* Error indicator */}
{!props.data.isPreview && hasError && (
<div
style={{
margin: "auto",
marginTop: props.data.metadata.iconProp ? "0.5rem" : "1rem",
position: "absolute",
top: "8px",
right: "8px",
zIndex: 10,
width: "22px",
height: "22px",
borderRadius: "50%",
backgroundColor: "#fef2f2",
display: "flex",
alignItems: "center",
justifyContent: "center",
border: "1px solid #fecaca",
}}
>
{props.data.metadata.iconProp && (
<Icon
icon={IconProp.Alert}
style={{
color: "#ef4444",
width: "0.75rem",
height: "0.75rem",
}}
thick={ThickProp.Thick}
/>
</div>
)}
{/* Header bar with gradient */}
<div
style={{
background: colors.headerBg,
padding: "0.625rem 0.875rem",
display: "flex",
alignItems: "center",
gap: "0.5rem",
}}
>
{props.data.metadata.iconProp && (
<div
style={{
width: "1.75rem",
height: "1.75rem",
borderRadius: "6px",
backgroundColor: "rgba(255, 255, 255, 0.2)",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
<Icon
icon={props.data.metadata.iconProp}
style={{
color: textColor,
width: "1.5rem",
height: "1.5rem",
textAlign: "center",
margin: "auto",
color: colors.iconColor,
width: "0.875rem",
height: "0.875rem",
}}
/>
)}
<p
style={{
color: textColor,
fontSize: "0.875rem",
lineHeight: "1.25rem",
textAlign: "center",
marginTop: "6px",
}}
>
{props.data.metadata.title}
</p>
{!props.data.isPreview && props.data.id && (
<p
style={{
color: descriptionColor,
fontSize: "0.875rem",
textAlign: "center",
}}
>
({props.data.id.trim()})
</p>
)}
<p
style={{
color: descriptionColor,
fontSize: "0.775rem",
lineHeight: "1.0rem",
textAlign: "center",
marginTop: "6px",
}}
>
{props.data.metadata.description}
</p>
</div>
</div>
)}
<span
style={{
color: colors.headerText,
fontSize: "0.8125rem",
fontWeight: 600,
letterSpacing: "0.01em",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{props.data.metadata.title}
</span>
</div>
{/* Body */}
<div
style={{
padding: "0.75rem 0.875rem",
backgroundColor: colors.bg,
minHeight: "3rem",
}}
>
{/* Component ID badge */}
{!props.data.isPreview && props.data.id && (
<div
style={{
display: "inline-flex",
alignItems: "center",
backgroundColor: "#ffffff",
border: "1px solid #e2e8f0",
borderRadius: "6px",
padding: "0.125rem 0.5rem",
marginBottom: "0.5rem",
}}
>
<span
style={{
color: "#64748b",
fontSize: "0.6875rem",
fontWeight: 500,
fontFamily:
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace',
}}
>
{props.data.id.trim()}
</span>
</div>
)}
{/* Description */}
<p
style={{
color: "#64748b",
fontSize: "0.75rem",
lineHeight: "1.125rem",
margin: 0,
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}
>
{props.data.metadata.description}
</p>
</div>
{/* Out ports section */}
{!props.data.isPreview &&
props.data.nodeType !== NodeType.PlaceholderNode && (
props.data.metadata.outPorts &&
props.data.metadata.outPorts.length > 0 && (
<>
<div>
{props.data.metadata.outPorts &&
props.data.metadata.outPorts.length > 0 &&
props.data.metadata.outPorts.map((port: Port, i: number) => {
return (
<Handle
key={i}
type="source"
id={port.id}
onConnect={(_params: Connection) => {}}
isConnectable={true}
position={Position.Bottom}
{/* Port labels */}
<div
style={{
borderTop: `1px solid ${colors.border}`,
padding: "0.375rem 0.875rem",
display: "flex",
justifyContent:
props.data.metadata.outPorts.length === 1
? "center"
: "space-between",
backgroundColor: "#ffffff",
}}
>
{props.data.metadata.outPorts.map((port: Port, i: number) => {
return (
<Tooltip key={i} text={port.description || ""}>
<span
style={{
...handleStyle,
...getPortPosition(
i + 1,
props.data.metadata.outPorts.length,
false,
),
color: "#94a3b8",
fontSize: "0.6875rem",
fontWeight: 500,
}}
/>
);
})}
>
{port.title}
</span>
</Tooltip>
);
})}
</div>
{/* Bottom handles */}
<div>
{props.data.metadata.outPorts &&
props.data.metadata.outPorts.length > 0 &&
props.data.metadata.outPorts.map((port: Port, i: number) => {
return (
<Tooltip key={i} text={port.description || ""}>
<div
key={i}
className="text-sm text-gray-400 absolute"
style={{
bottom: "10px",
...getPortPosition(
i + 1,
props.data.metadata.outPorts.length,
true,
),
}}
>
{port.title}
</div>
</Tooltip>
);
})}
{props.data.metadata.outPorts.map((port: Port, i: number) => {
return (
<Handle
key={i}
type="source"
id={port.id}
onConnect={(_params: Connection) => {}}
isConnectable={true}
position={Position.Bottom}
style={{
background: colors.handleBg,
height: "10px",
width: "10px",
border: `2px solid ${colors.handleBorder}`,
bottom: "-5px",
transition: "all 0.15s ease",
...getPortPosition(
i + 1,
props.data.metadata.outPorts.length,
false,
),
}}
/>
);
})}
</div>
</>
)}

View File

@@ -1,4 +1,3 @@
import ErrorMessage from "../ErrorMessage/ErrorMessage";
import { Port } from "../../../Types/Workflow/Component";
import React, { FunctionComponent, ReactElement } from "react";
@@ -12,37 +11,75 @@ const ComponentPortViewer: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
return (
<div className="mt-5 mb-5">
<h2 className="text-base font-medium text-gray-500">{props.name}</h2>
<p className="text-sm font-medium text-gray-400">{props.description}</p>
<div className="mt-3 mb-3">
<h2 className="text-sm font-semibold text-gray-600">{props.name}</h2>
<p className="text-xs text-gray-400 mb-2">{props.description}</p>
{props.ports && props.ports.length === 0 && (
<ErrorMessage message={"This component does not have any ports."} />
<p className="text-xs text-gray-400 italic">No ports configured.</p>
)}
<div className="mt-3">
<div>
{props.ports &&
props.ports.length > 0 &&
props.ports.map((port: Port, i: number) => {
return (
<div
key={i}
className="mt-2 mb-2 relative flex items-center space-x-3 rounded-lg border border-gray-300 bg-white px-6 py-5 shadow-sm focus-within:ring-2 focus-within:ring-pink-500 focus-within:ring-offset-2"
style={{
display: "flex",
alignItems: "center",
gap: "0.625rem",
padding: "0.5rem 0.75rem",
borderRadius: "8px",
backgroundColor: "#f8fafc",
border: "1px solid #f1f5f9",
marginBottom: "0.375rem",
}}
>
<div className="min-w-0 flex-1">
<div className="focus:outline-none">
<div
style={{
width: "8px",
height: "8px",
borderRadius: "50%",
backgroundColor: "#94a3b8",
flexShrink: 0,
}}
/>
<div style={{ minWidth: 0, flex: 1 }}>
<p
style={{
fontSize: "0.8125rem",
fontWeight: 500,
color: "#334155",
margin: 0,
lineHeight: "1.25rem",
}}
>
{port.title}
<span
className="absolute inset-0"
aria-hidden="true"
></span>
<p className="text-sm font-medium text-gray-900">
{port.title}{" "}
<span className="text-gray-500 font-normal">
(ID: {port.id})
</span>
</p>
<p className="truncate text-sm text-gray-500">
style={{
color: "#94a3b8",
fontWeight: 400,
fontSize: "0.6875rem",
marginLeft: "0.375rem",
fontFamily:
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace',
}}
>
{port.id}
</span>
</p>
{port.description && (
<p
style={{
fontSize: "0.75rem",
color: "#94a3b8",
margin: 0,
lineHeight: "1rem",
}}
>
{port.description}
</p>
</div>
)}
</div>
</div>
);

View File

@@ -1,6 +1,3 @@
import ErrorMessage from "../ErrorMessage/ErrorMessage";
import Pill from "../Pill/Pill";
import { Black } from "../../../Types/BrandColors";
import { ReturnValue } from "../../../Types/Workflow/Component";
import React, { FunctionComponent, ReactElement } from "react";
@@ -14,41 +11,84 @@ const ComponentReturnValueViewer: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
return (
<div className="mt-5 mb-5">
<h2 className="text-base font-medium text-gray-500">{props.name}</h2>
<p className="text-sm font-medium text-gray-400">{props.description}</p>
<div className="mt-3 mb-3">
<h2 className="text-sm font-semibold text-gray-600">{props.name}</h2>
<p className="text-xs text-gray-400 mb-2">{props.description}</p>
{props.returnValues && props.returnValues.length === 0 && (
<ErrorMessage message={"This component does not return any value."} />
<p className="text-xs text-gray-400 italic">
This component does not return any values.
</p>
)}
<div className="mt-3">
<div>
{props.returnValues &&
props.returnValues.length > 0 &&
props.returnValues.map((returnValue: ReturnValue, i: number) => {
return (
<div
key={i}
className="mt-2 mb-2 relative flex items-center space-x-3 rounded-lg border border-gray-300 bg-white px-6 py-5 shadow-sm focus-within:ring-2 focus-within:ring-pink-500 focus-within:ring-offset-2"
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "0.625rem",
padding: "0.5rem 0.75rem",
borderRadius: "8px",
backgroundColor: "#f8fafc",
border: "1px solid #f1f5f9",
marginBottom: "0.375rem",
}}
>
<div className="min-w-0 flex-1 flex justify-between">
<div className="focus:outline-none">
<div style={{ minWidth: 0, flex: 1 }}>
<p
style={{
fontSize: "0.8125rem",
fontWeight: 500,
color: "#334155",
margin: 0,
lineHeight: "1.25rem",
}}
>
{returnValue.name}
<span
className="absolute inset-0"
aria-hidden="true"
></span>
<p className="text-sm font-medium text-gray-900">
{returnValue.name}{" "}
<span className="text-gray-500 font-normal">
(ID: {returnValue.id})
</span>
</p>
<p className="truncate text-sm text-gray-500">
style={{
color: "#94a3b8",
fontWeight: 400,
fontSize: "0.6875rem",
marginLeft: "0.375rem",
fontFamily:
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace',
}}
>
{returnValue.id}
</span>
</p>
{returnValue.description && (
<p
style={{
fontSize: "0.75rem",
color: "#94a3b8",
margin: 0,
lineHeight: "1rem",
}}
>
{returnValue.description}
</p>
</div>
<div>
<Pill color={Black} text={returnValue.type} />
</div>
)}
</div>
<span
style={{
fontSize: "0.6875rem",
fontWeight: 500,
color: "#6366f1",
backgroundColor: "#eef2ff",
padding: "0.125rem 0.5rem",
borderRadius: "100px",
whiteSpace: "nowrap",
border: "1px solid #e0e7ff",
}}
>
{returnValue.type}
</span>
</div>
);
})}

View File

@@ -1,5 +1,4 @@
import Button, { ButtonStyleType } from "../Button/Button";
import Divider from "../Divider/Divider";
import BasicForm from "../Forms/BasicForm";
import FormFieldSchemaType from "../Forms/Types/FormFieldSchemaType";
import FormValues from "../Forms/Types/FormValues";
@@ -15,6 +14,7 @@ import { JSONObject } from "../../../Types/JSON";
import ObjectID from "../../../Types/ObjectID";
import { NodeDataProp } from "../../../Types/Workflow/Component";
import React, { FunctionComponent, ReactElement, useState } from "react";
import Icon from "../Icon/Icon";
export interface ComponentProps {
title: string;
@@ -47,7 +47,7 @@ const ComponentSettingsModal: FunctionComponent<ComponentProps> = (
}}
leftFooterElement={
<Button
title={`Delete ${component.metadata.componentType}`}
title={`Delete`}
icon={IconProp.Trash}
buttonStyle={ButtonStyleType.DANGER_OUTLINE}
onClick={() => {
@@ -73,7 +73,40 @@ const ComponentSettingsModal: FunctionComponent<ComponentProps> = (
submitButtonType={ButtonStyleType.DANGER}
/>
)}
<div className="mb-3 mt-3">
{/* Component ID Section */}
<div
style={{
backgroundColor: "#f8fafc",
borderRadius: "10px",
border: "1px solid #e2e8f0",
padding: "1rem",
marginTop: "0.75rem",
marginBottom: "1rem",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "0.5rem",
marginBottom: "0.5rem",
}}
>
<Icon
icon={IconProp.Label}
style={{ color: "#64748b", width: "0.875rem", height: "0.875rem" }}
/>
<span
style={{
fontSize: "0.8125rem",
fontWeight: 600,
color: "#334155",
}}
>
Identity
</span>
</div>
<BasicForm
hideSubmitButton={true}
initialValues={{
@@ -91,23 +124,54 @@ const ComponentSettingsModal: FunctionComponent<ComponentProps> = (
fields={[
{
title: `${component.metadata.componentType} ID`,
description: `${component.metadata.componentType} ID will make it easier for you to connect to other components.`,
description: `Unique identifier used to reference this ${component.metadata.componentType.toLowerCase()} from other components.`,
field: {
id: true,
},
required: true,
fieldType: FormFieldSchemaType.Text,
},
]}
/>
</div>
{/* Documentation Section */}
{component.metadata.documentationLink && (
<div>
<Divider />
<div
style={{
backgroundColor: "#eff6ff",
borderRadius: "10px",
border: "1px solid #bfdbfe",
padding: "1rem",
marginBottom: "1rem",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "0.5rem",
marginBottom: "0.5rem",
}}
>
<Icon
icon={IconProp.Book}
style={{
color: "#3b82f6",
width: "0.875rem",
height: "0.875rem",
}}
/>
<span
style={{
fontSize: "0.8125rem",
fontWeight: 600,
color: "#1e40af",
}}
>
Documentation
</span>
</div>
<DocumentationViewer
documentationLink={component.metadata.documentationLink}
workflowId={props.workflowId}
@@ -115,48 +179,133 @@ const ComponentSettingsModal: FunctionComponent<ComponentProps> = (
</div>
)}
<Divider />
<ArgumentsForm
graphComponents={props.graphComponents}
workflowId={props.workflowId}
component={component}
onFormChange={(component: NodeDataProp) => {
setComponent({ ...component });
{/* Arguments Section */}
<div
style={{
backgroundColor: "#ffffff",
borderRadius: "10px",
border: "1px solid #e2e8f0",
padding: "1rem",
marginBottom: "1rem",
}}
onHasFormValidationErrors={(value: Dictionary<boolean>) => {
setHasFormValidationErrors({
...hasFormValidationErrors,
...value,
});
}}
/>
<Divider />
<div className="mb-3 mt-3">
<ComponentPortViewer
name="In Ports"
description="Here is a list of inports for this component"
ports={component.metadata.inPorts}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "0.5rem",
marginBottom: "0.75rem",
}}
>
<Icon
icon={IconProp.Settings}
style={{ color: "#64748b", width: "0.875rem", height: "0.875rem" }}
/>
<span
style={{
fontSize: "0.8125rem",
fontWeight: 600,
color: "#334155",
}}
>
Configuration
</span>
</div>
<ArgumentsForm
graphComponents={props.graphComponents}
workflowId={props.workflowId}
component={component}
onFormChange={(component: NodeDataProp) => {
setComponent({ ...component });
}}
onHasFormValidationErrors={(value: Dictionary<boolean>) => {
setHasFormValidationErrors({
...hasFormValidationErrors,
...value,
});
}}
/>
</div>
<Divider />
<div className="mb-3 mt-3">
{/* Ports Section */}
<div
style={{
backgroundColor: "#ffffff",
borderRadius: "10px",
border: "1px solid #e2e8f0",
padding: "1rem",
marginBottom: "1rem",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "0.5rem",
marginBottom: "0.25rem",
}}
>
<Icon
icon={IconProp.Link}
style={{ color: "#64748b", width: "0.875rem", height: "0.875rem" }}
/>
<span
style={{
fontSize: "0.8125rem",
fontWeight: 600,
color: "#334155",
}}
>
Connections
</span>
</div>
<ComponentPortViewer
name="In Ports"
description="Input connections for this component"
ports={component.metadata.inPorts}
/>
<ComponentPortViewer
name="Out Ports"
description="Here is a list of outports for this component"
description="Output connections from this component"
ports={component.metadata.outPorts}
/>
</div>
<Divider />
<div className="mb-3 mt-3">
{/* Return Values Section */}
<div
style={{
backgroundColor: "#ffffff",
borderRadius: "10px",
border: "1px solid #e2e8f0",
padding: "1rem",
marginBottom: "1rem",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "0.5rem",
marginBottom: "0.25rem",
}}
>
<Icon
icon={IconProp.ArrowCircleRight}
style={{ color: "#64748b", width: "0.875rem", height: "0.875rem" }}
/>
<span
style={{
fontSize: "0.8125rem",
fontWeight: 600,
color: "#334155",
}}
>
Output
</span>
</div>
<ComponentReturnValueViewer
name="Return Values"
description="Here is a list of values that this component returns"
description="Values this component produces for downstream use"
returnValues={component.metadata.returnValues}
/>
</div>

View File

@@ -3,11 +3,10 @@ import ErrorMessage from "../ErrorMessage/ErrorMessage";
import Icon from "../Icon/Icon";
import Input from "../Input/Input";
import SideOver from "../SideOver/SideOver";
import ComponentElement from "./Component";
import IconProp from "../../../Types/Icon/IconProp";
import ComponentMetadata, {
ComponentCategory,
ComponentType,
NodeType,
} from "../../../Types/Workflow/Component";
import React, {
FunctionComponent,
@@ -38,11 +37,12 @@ const ComponentsModal: FunctionComponent<ComponentProps> = (
const [isSearching, setIsSearching] = useState<boolean>(false);
const [selectedComponentMetadata, setSelectedComponentMetadata] =
useState<ComponentMetadata | null>(null);
useEffect(() => {
setComponents(props.components);
setComponentsToShow([...props.components]);
setCategories(props.categories);
}, []);
@@ -76,14 +76,11 @@ const ComponentsModal: FunctionComponent<ComponentProps> = (
]);
}, [search]);
const [selectedComponentMetadata, setSelectedComponentMetadata] =
useState<ComponentMetadata | null>(null);
return (
<SideOver
submitButtonText="Create"
title={`Select a ${props.componentsType}`}
description={`Please select a component to add to your workflow.`}
submitButtonText="Add to Workflow"
title={`Add ${props.componentsType}`}
description={`Choose a ${props.componentsType.toLowerCase()} to add to your workflow.`}
onClose={props.onCloseModal}
submitButtonDisabled={!selectedComponentMetadata}
onSubmit={() => {
@@ -95,107 +92,208 @@ const ComponentsModal: FunctionComponent<ComponentProps> = (
>
<>
<div className="flex flex-col h-full">
{/** Search box here */}
<div className="mt-5">
<Input
placeholder="Search..."
onChange={(text: string) => {
setIsSearching(true);
setSearch(text);
}}
/>
{/* Search box */}
<div className="mt-4 mb-4">
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Icon
icon={IconProp.Search}
className="h-4 w-4 text-gray-400"
/>
</div>
<div className="pl-9">
<Input
placeholder={`Search ${props.componentsType.toLowerCase()}s...`}
onChange={(text: string) => {
setIsSearching(true);
setSearch(text);
}}
/>
</div>
</div>
</div>
<div className="overflow-y-auto overflow-x-hidden my-5">
<div className="overflow-y-auto overflow-x-hidden flex-1">
{!componentsToShow ||
(componentsToShow.length === 0 && (
<div className="w-full flex justify-center mt-20">
<ErrorMessage message="No components that match your search. If you are looking for an integration that does not exist currently - you can use Custom Code or API component to build anything you like. If you are an enterprise customer, feel free to talk to us and we will build it for you." />
<div className="w-full flex justify-center mt-20 px-4">
<ErrorMessage message="No components that match your search. If you are looking for an integration that does not exist currently - you can use Custom Code or API component to build anything you like." />
</div>
))}
{categories &&
categories.length > 0 &&
categories.map((category: ComponentCategory, i: number) => {
if (
componentsToShow &&
componentsToShow.length > 0 &&
const categoryComponents: Array<ComponentMetadata> =
componentsToShow.filter(
(componentMetadata: ComponentMetadata) => {
return componentMetadata.category === category.name;
},
).length > 0
) {
return (
<div key={i}>
<h4 className="text-gray-500 text-base mt-5 flex">
{" "}
);
if (categoryComponents.length === 0) {
return <div key={i}></div>;
}
return (
<div key={i} className="mb-6">
{/* Category header */}
<div className="flex items-center gap-2 mb-3 px-1">
<div
className="flex items-center justify-center rounded-md"
style={{
width: "28px",
height: "28px",
backgroundColor: "#f1f5f9",
}}
>
<Icon
icon={category.icon}
className="h-5 w-5 text-gray-500"
/>{" "}
<span className="ml-2">{category.name}</span>
</h4>
<p className="text-gray-400 text-sm mb-5">
{category.description}
</p>
<div className="flex flex-wrap ml-2">
{components &&
components.length > 0 &&
components
.filter((componentMetadata: ComponentMetadata) => {
return (
componentMetadata.category === category.name
);
})
.map(
(
componentMetadata: ComponentMetadata,
i: number,
) => {
return (
<div
key={i}
onClick={() => {
setSelectedComponentMetadata(
componentMetadata,
);
}}
className={`m-5 ml-0 mt-0 ${
selectedComponentMetadata &&
selectedComponentMetadata.id ===
componentMetadata.id
? "rounded ring-offset-2 ring ring-indigo-500"
: ""
}`}
>
<ComponentElement
key={i}
selected={false}
data={{
metadata: componentMetadata,
metadataId: componentMetadata.id,
internalId: "",
nodeType: NodeType.Node,
componentType:
componentMetadata.componentType,
returnValues: {},
isPreview: true,
id: "",
error: "",
arguments: {},
}}
/>
</div>
);
},
)}
className="h-4 w-4 text-gray-500"
/>
</div>
<div>
<h4 className="text-sm font-semibold text-gray-700 leading-tight">
{category.name}
</h4>
<p className="text-xs text-gray-400 leading-tight">
{category.description}
</p>
</div>
</div>
);
}
return <div key={i}></div>;
{/* Component cards grid */}
<div className="grid grid-cols-1 gap-2">
{categoryComponents.map(
(
componentMetadata: ComponentMetadata,
j: number,
) => {
const isSelected: boolean =
selectedComponentMetadata !== null &&
selectedComponentMetadata.id ===
componentMetadata.id;
return (
<div
key={j}
onClick={() => {
setSelectedComponentMetadata(
componentMetadata,
);
}}
className="cursor-pointer transition-all duration-150"
style={{
padding: "0.75rem",
borderRadius: "10px",
border: isSelected
? "2px solid #6366f1"
: "1px solid #e2e8f0",
backgroundColor: isSelected
? "#eef2ff"
: "#ffffff",
display: "flex",
alignItems: "flex-start",
gap: "0.75rem",
boxShadow: isSelected
? "0 0 0 3px rgba(99, 102, 241, 0.1)"
: "0 1px 2px 0 rgba(0, 0, 0, 0.03)",
}}
>
{/* Icon */}
<div
style={{
width: "36px",
height: "36px",
borderRadius: "8px",
backgroundColor: isSelected
? "#6366f1"
: "#f1f5f9",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
transition: "all 0.15s ease",
}}
>
<Icon
icon={componentMetadata.iconProp}
style={{
color: isSelected
? "#ffffff"
: "#64748b",
width: "1rem",
height: "1rem",
}}
/>
</div>
{/* Text */}
<div style={{ minWidth: 0, flex: 1 }}>
<p
style={{
fontSize: "0.8125rem",
fontWeight: 600,
color: isSelected
? "#4338ca"
: "#1e293b",
margin: 0,
lineHeight: "1.25rem",
}}
>
{componentMetadata.title}
</p>
<p
style={{
fontSize: "0.75rem",
color: isSelected
? "#6366f1"
: "#94a3b8",
margin: 0,
marginTop: "2px",
lineHeight: "1rem",
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}
>
{componentMetadata.description}
</p>
</div>
{/* Selection indicator */}
{isSelected && (
<div
style={{
width: "20px",
height: "20px",
borderRadius: "50%",
backgroundColor: "#6366f1",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
marginTop: "2px",
}}
>
<Icon
icon={IconProp.Check}
style={{
color: "#ffffff",
width: "0.625rem",
height: "0.625rem",
}}
/>
</div>
)}
</div>
);
},
)}
</div>
</div>
);
})}
</div>
</div>

View File

@@ -22,6 +22,7 @@ import React, {
} from "react";
import ReactFlow, {
Background,
BackgroundVariant,
Connection,
Controls,
Edge,
@@ -75,9 +76,9 @@ const edgeStyle: React.CSSProperties = {
};
const selectedEdgeStyle: React.CSSProperties = {
strokeWidth: "2px",
stroke: "#818cf8",
color: "#818cf8",
strokeWidth: "2.5px",
stroke: "#6366f1",
color: "#6366f1",
};
type GetEdgeDefaultPropsFunction = (selected: boolean) => JSONObject;
@@ -87,9 +88,14 @@ export const getEdgeDefaultProps: GetEdgeDefaultPropsFunction = (
): JSONObject => {
return {
type: "smoothstep",
animated: selected,
markerEnd: {
type: MarkerType.Arrow,
color: edgeStyle.color?.toString() || "",
type: MarkerType.ArrowClosed,
color: selected
? selectedEdgeStyle.color?.toString() || ""
: edgeStyle.color?.toString() || "",
width: 20,
height: 20,
},
style: selected ? { ...selectedEdgeStyle } : { ...edgeStyle },
};
@@ -271,7 +277,7 @@ const Workflow: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
{
...oldEdge,
markerEnd: {
type: MarkerType.Arrow,
type: MarkerType.ArrowClosed,
color: edgeStyle.color?.toString() || "",
},
style: edgeStyle,
@@ -398,7 +404,65 @@ const Workflow: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
};
return (
<div className="h-[48rem]">
<div
style={{
height: "calc(100vh - 220px)",
minHeight: "600px",
borderRadius: "8px",
overflow: "hidden",
border: "1px solid #e2e8f0",
}}
>
<style>
{`
.react-flow__minimap {
border-radius: 8px !important;
border: 1px solid #e2e8f0 !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.07) !important;
overflow: hidden !important;
background: #ffffff !important;
}
.react-flow__controls {
border-radius: 8px !important;
border: 1px solid #e2e8f0 !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.07) !important;
overflow: hidden !important;
}
.react-flow__controls-button {
border-bottom: 1px solid #f1f5f9 !important;
width: 32px !important;
height: 32px !important;
}
.react-flow__controls-button:hover {
background: #f8fafc !important;
}
.react-flow__controls-button svg {
max-width: 14px !important;
max-height: 14px !important;
}
.react-flow__edge:hover .react-flow__edge-path {
stroke: #6366f1 !important;
stroke-width: 2.5px !important;
}
.react-flow__handle:hover {
transform: scale(1.3) !important;
}
.react-flow__connection-line {
stroke: #6366f1 !important;
stroke-width: 2px !important;
stroke-dasharray: 5 5 !important;
}
@keyframes flow-dash {
to {
stroke-dashoffset: -10;
}
}
.react-flow__edge.animated .react-flow__edge-path {
animation: flow-dash 0.5s linear infinite !important;
stroke-dasharray: 5 5 !important;
}
`}
</style>
<ReactFlow
nodes={nodes}
edges={edges}
@@ -418,10 +482,42 @@ const Workflow: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
nodeTypes={nodeTypes}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
snapToGrid={true}
snapGrid={[16, 16]}
connectionLineStyle={{
stroke: "#6366f1",
strokeWidth: 2,
strokeDasharray: "5 5",
}}
defaultEdgeOptions={{
type: "smoothstep",
style: { ...edgeStyle },
}}
>
<MiniMap />
<MiniMap
nodeStrokeWidth={3}
nodeColor={(node: Node) => {
if (
node.data &&
node.data.metadata &&
node.data.metadata.componentType === ComponentType.Trigger
) {
return "#f59e0b";
}
return "#6366f1";
}}
maskColor="rgba(241, 245, 249, 0.7)"
style={{
backgroundColor: "#ffffff",
}}
/>
<Controls />
<Background color="#111827" />
<Background
variant={BackgroundVariant.Dots}
gap={20}
size={1}
color="#cbd5e1"
/>
</ReactFlow>
{showComponentsModal && (