dataflow-rs 2.1.5

A lightweight rules engine for building IFTTT-style automation and data processing pipelines in Rust. Define rules with JSONLogic conditions, execute actions, and chain workflows.
Documentation
import { useState, useRef } from 'react';
import type { Workflow, Task, JsonLogicValue, Message, MappingItem, ValidationRule } from '../../types';
import type { DebugConfig } from '../../types/debugConfig';
import { ThemeProvider, useTheme, useDebuggerOptional, DebuggerProvider } from './context';
import { TreeView, DetailsPanel, ResultPanel } from './views';
import { IntegratedDebugToolbar } from './debug';
import { ErrorBoundary } from '../common';
import { useResizable } from './hooks';
import { LAYOUT } from './constants';
import type { Theme } from './context';

import './styles/index.css';

// Extended selection types for tree view
export type TreeSelectionType =
  | { type: 'none' }
  | { type: 'folder'; workflows: Workflow[]; name: string }
  | { type: 'workflow'; workflow: Workflow }
  | { type: 'workflow-condition'; workflow: Workflow; condition: JsonLogicValue }
  | { type: 'task'; task: Task; workflow: Workflow }
  | { type: 'task-condition'; task: Task; workflow: Workflow; condition: JsonLogicValue }
  | { type: 'mapping'; task: Task; workflow: Workflow; mapping: MappingItem; mappingIndex: number }
  | { type: 'validation-rule'; task: Task; workflow: Workflow; rule: ValidationRule; ruleIndex: number };


export interface WorkflowVisualizerProps {
  /** Array of workflow definitions to display */
  workflows: Workflow[];
  /** Callback when a workflow is selected */
  onWorkflowSelect?: (workflow: Workflow) => void;
  /** Callback when a task is selected */
  onTaskSelect?: (task: Task, workflow: Workflow) => void;
  /** Initial theme setting */
  theme?: Theme;
  /** Class name for the root element */
  className?: string;
  /** Execution result to display in the result panel */
  executionResult?: Message | null;
  /**
   * Debug configuration for integrated debug mode.
   * When enabled, automatically wraps with DebuggerProvider and shows controls.
   */
  debugConfig?: DebugConfig;
  /**
   * Payload for debugging. Used with debugConfig.
   * Takes precedence over debugConfig.initialPayload.
   */
  debugPayload?: Record<string, unknown>;
}

// Get display info for the current selection
function getSelectionInfo(selection: TreeSelectionType): { title: string; subtitle: string } | null {
  switch (selection.type) {
    case 'folder':
      return {
        title: selection.name,
        subtitle: `${selection.workflows.length} Workflows`,
      };
    case 'workflow':
      return {
        title: selection.workflow.name,
        subtitle: 'Workflow Flow',
      };
    case 'workflow-condition':
      return {
        title: 'Workflow Condition',
        subtitle: selection.workflow.name,
      };
    case 'task':
      return {
        title: selection.task.name,
        subtitle: `${selection.workflow.name} / ${selection.task.function.name}`,
      };
    case 'task-condition':
      return {
        title: 'Task Condition',
        subtitle: `${selection.workflow.name} / ${selection.task.name}`,
      };
    case 'mapping':
      return {
        title: selection.mapping.path,
        subtitle: `${selection.workflow.name} / ${selection.task.name}`,
      };
    case 'validation-rule':
      return {
        title: selection.rule.message,
        subtitle: `${selection.workflow.name} / ${selection.task.name}`,
      };
    default:
      return null;
  }
}

interface VisualizerInnerProps {
  workflows: Workflow[];
  onTaskSelect?: (task: Task, workflow: Workflow) => void;
  onWorkflowSelect?: (workflow: Workflow) => void;
  executionResult?: Message | null;
  /** Debug config for toolbar and debug mode */
  debugConfig?: DebugConfig;
  /** Payload for debugging */
  debugPayload?: Record<string, unknown>;
}

function VisualizerInner({
  workflows,
  onTaskSelect,
  onWorkflowSelect,
  executionResult,
  debugConfig,
  debugPayload,
}: VisualizerInnerProps) {
  const { resolvedTheme } = useTheme();
  // Derive debug mode from debugConfig
  const debugMode = debugConfig?.enabled ?? false;
  // Use optional hook that returns null if no provider exists
  const debuggerContext = useDebuggerOptional();
  const effectiveDebugContext = debugMode ? debuggerContext : null;
  const [selection, setSelection] = useState<TreeSelectionType>({ type: 'none' });
  const containerRef = useRef<HTMLDivElement>(null);
  const leftPanelRef = useRef<HTMLDivElement>(null);

  const {
    size: leftPanelWidth,
    isDragging,
    onMouseDown: handleMouseDown,
  } = useResizable({
    containerRef,
    direction: 'horizontal',
    min: LAYOUT.LEFT_PANEL.MIN,
    max: LAYOUT.LEFT_PANEL.MAX,
    initial: LAYOUT.LEFT_PANEL.DEFAULT,
  });

  const {
    size: treePanelHeight,
    isDragging: isVerticalDragging,
    onMouseDown: handleVerticalMouseDown,
  } = useResizable({
    containerRef: leftPanelRef,
    direction: 'vertical',
    min: LAYOUT.TREE_HEIGHT_PCT.MIN,
    max: LAYOUT.TREE_HEIGHT_PCT.MAX,
    initial: LAYOUT.TREE_HEIGHT_PCT.DEFAULT,
  });

  const handleSelection = (newSelection: TreeSelectionType) => {
    setSelection(newSelection);

    // Fire callbacks for external consumers
    if (newSelection.type === 'workflow') {
      onWorkflowSelect?.(newSelection.workflow);
    } else if (newSelection.type === 'workflow-condition') {
      onWorkflowSelect?.(newSelection.workflow);
    } else if (
      newSelection.type === 'task' ||
      newSelection.type === 'task-condition' ||
      newSelection.type === 'mapping' ||
      newSelection.type === 'validation-rule'
    ) {
      onTaskSelect?.(newSelection.task, newSelection.workflow);
    }
  };

  const selectionInfo = getSelectionInfo(selection);

  // Determine which message to display in result panel
  const hasDebugTrace = debugMode && effectiveDebugContext?.hasTrace;
  const displayMessage = hasDebugTrace
    ? effectiveDebugContext?.currentMessage
    : executionResult;
  const currentChanges = hasDebugTrace ? effectiveDebugContext?.currentChanges : [];
  const currentStepIndex = hasDebugTrace ? effectiveDebugContext?.state.currentStepIndex : -1;

  const isDraggingAny = isDragging || isVerticalDragging;

  return (
    <ErrorBoundary>
      <div className={`df-visualizer-container df-theme-${resolvedTheme}`}>
        {/* Title bar with debug controls */}
        <div className="df-visualizer-title-bar">
          <span className="df-visualizer-title">Workflows</span>
          {debugMode && debugConfig && (
            <IntegratedDebugToolbar
              workflows={workflows}
              payload={debugPayload ?? debugConfig.initialPayload}
              autoExecute={debugConfig.autoExecute}
              onExecutionComplete={debugConfig.onExecutionComplete}
              onExecutionError={debugConfig.onExecutionError}
            />
          )}
        </div>

        {/* Main content area */}
        <div
          ref={containerRef}
          className={`df-visualizer-horizontal ${isDraggingAny ? 'df-dragging' : ''}`}
        >
          {/* Left section: Tree view + Result panel */}
          <div
            ref={leftPanelRef}
            className="df-visualizer-left"
            style={{ width: leftPanelWidth }}
          >
            {/* Tree view section */}
            <div
              className={`df-visualizer-left-tree ${displayMessage ? 'df-with-result' : ''}`}
              style={displayMessage ? { height: `${treePanelHeight}%` } : undefined}
            >
              <div className="df-visualizer-left-header">
                <span className="df-visualizer-left-title">Explorer</span>
              </div>
            <div className="df-visualizer-left-content">
              {workflows.length === 0 ? (
                <div className="df-empty-state">
                  <p>No workflows to display</p>
                </div>
              ) : (
                <TreeView
                  workflows={workflows}
                  selection={selection}
                  onSelect={handleSelection}
                  debugMode={debugMode}
                />
              )}
            </div>
          </div>

          {/* Horizontal resizer between tree and result */}
          {displayMessage && (
            <div
              className={`df-visualizer-divider-horizontal ${isVerticalDragging ? 'df-divider-active' : ''}`}
              onMouseDown={handleVerticalMouseDown}
            />
          )}

          {/* Result panel (bottom half when result exists) */}
          {displayMessage && (
            <div
              className="df-visualizer-result-panel"
              style={{ height: `${100 - treePanelHeight}%` }}
            >
              <ResultPanel
                displayMessage={displayMessage}
                currentStepIndex={currentStepIndex ?? -1}
                currentChanges={currentChanges ?? []}
                theme={resolvedTheme === 'dark' ? 'dark' : 'light'}
                hasDebugTrace={!!hasDebugTrace}
              />
            </div>
          )}
        </div>

        {/* Resizable divider */}
        <div
          className={`df-visualizer-divider ${isDragging ? 'df-divider-active' : ''}`}
          onMouseDown={handleMouseDown}
        />

        {/* Right section: Details panel */}
        <div className="df-visualizer-right">
          {selectionInfo && (
            <div className="df-details-header">
              <div className="df-details-header-info">
                <span className="df-details-header-title">{selectionInfo.title}</span>
                <span className="df-details-header-subtitle">{selectionInfo.subtitle}</span>
              </div>
            </div>
          )}
          <DetailsPanel selection={selection} onSelect={handleSelection} />
        </div>
        </div>
      </div>
    </ErrorBoundary>
  );
}

export function WorkflowVisualizer({
  workflows,
  onWorkflowSelect,
  onTaskSelect,
  theme = 'system',
  className = '',
  executionResult,
  debugConfig,
  debugPayload,
}: WorkflowVisualizerProps) {
  // When debugConfig.enabled is true, wrap internally with DebuggerProvider
  if (debugConfig?.enabled) {
    return (
      <ThemeProvider defaultTheme={theme}>
        <DebuggerProvider
          engineFactory={debugConfig.engineFactory}
          autoActivate={true}
          initialPayload={debugPayload ?? debugConfig.initialPayload}
        >
          <div className={`df-root ${className}`}>
            <VisualizerInner
              workflows={workflows}
              onTaskSelect={onTaskSelect}
              onWorkflowSelect={onWorkflowSelect}
              executionResult={executionResult}
              debugConfig={debugConfig}
              debugPayload={debugPayload}
            />
          </div>
        </DebuggerProvider>
      </ThemeProvider>
    );
  }

  // Non-debug mode: render without DebuggerProvider
  return (
    <ThemeProvider defaultTheme={theme}>
      <div className={`df-root ${className}`}>
        <VisualizerInner
          workflows={workflows}
          onTaskSelect={onTaskSelect}
          onWorkflowSelect={onWorkflowSelect}
          executionResult={executionResult}
        />
      </div>
    </ThemeProvider>
  );
}