mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
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:
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user