# All files / src/components/dag DagView.tsx

81.81% Statements 18/22
40% Branches 2/5
57.14% Functions 4/7
80.95% Lines 17/21

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124

    3x

    3x

    6x

    6x
    6x

    6x

    6x
    6x
    6x
    6x
    66x

    6x

    6x

    6x

    6x
    3x

    3x

     import { useCallback, useMemo, type JSX, type MouseEvent } from "react";
    import {
      ReactFlow,
      Background,
      Controls,
      MiniMap,
      BackgroundVariant,
      type NodeTypes,
      type Node,
    } from "@xyflow/react";
    import "@xyflow/react/dist/style.css";
    import type { TaskData } from "../../hooks/types.js";
    import { useDagLayout, type TaskNodeData } from "./useDagLayout.js";
    import { TaskNode } from "./TaskNode.js";
    import { taskUrl, newTaskUrl, useAppNavigate } from "../../utils/navigation.js";
    import { STATUS_CSS_VAR_MAP } from "../../utils/taskStatus.js";
    import styles from "./DagView.module.scss";

    /** Props for the DagView component. */
    interface Props {
      workspaceId: string;
      environmentId: string;
      /** All tasks — filtered internally by workspaceId. */
      tasks: TaskData[];
      /** Resolved theme ID, used to recompute CSS variable colors for the MiniMap. */
      resolvedThemeId: string;
    }

    /** CSS variable mapping for MiniMap node coloring by task status. */
    const STATUS_VAR_MAP: Record<string, string> = STATUS_CSS_VAR_MAP;

    /** Custom node type registry for React Flow. */
    const nodeTypes: NodeTypes = {
      task: TaskNode,
    };

    /** Interactive DAG visualization of task hierarchy and dependency relationships. */
    export function DagView({
      workspaceId,
      environmentId,
      tasks,
      resolvedThemeId,
    }: Props): JSX.Element {
      const navigate = useAppNavigate();

      const workspaceTasks = useMemo(
        () => tasks.filter((t) => t.workspaceId === workspaceId),
        [tasks, workspaceId],
      );

      const { nodes, edges } = useDagLayout(workspaceTasks);

      /** Cached color map — recomputed only when the theme changes. */
      const statusColors = useMemo(() => {
        const style = getComputedStyle(document.documentElement);
        const colors: Record<string, string> = {};
        for (const [status, varName] of Object.entries(STATUS_VAR_MAP)) {
          colors[status] = style.getPropertyValue(varName).trim() || "#6b7a8d";
        }
        return colors;
      }, [resolvedThemeId]);

      const onNodeClick = useCallback(
        (_event: MouseEvent, node: Node) => {
          navigate(taskUrl(node.id, undefined, workspaceId, environmentId));
        },
        [navigate, workspaceId, environmentId],
      );

      /** Returns a hex color for the MiniMap based on task status. */
      const minimapNodeColor = useCallback(
        (node: Node): string => {
          const data = node.data as TaskNodeData;
          return statusColors[data.task.status] || statusColors.pending;
        },
        [statusColors],
      );

      if (workspaceTasks.length === 0) {
        return (
          <div className={styles.emptyCta}>
            <button
              className={styles.ctaButton}
              onClick={() => navigate(newTaskUrl(workspaceId, undefined, environmentId))}
            >
              Create Task
            </button>
            <div className={styles.ctaDescription}>Create tasks to see the dependency graph</div>
          </div>
        );
      }

      return (
        <div className={styles.dagContainer}>
          <ReactFlow
            nodes={nodes}
            edges={edges}
            nodeTypes={nodeTypes}
            onNodeClick={onNodeClick}
            // Read-only DAG: dependencies aren't created by dragging (no onConnect),
            // so disable the drag-to-connect affordance on node handles (#1303).
            nodesConnectable={false}
            fitView
            fitViewOptions={{ padding: 0.2 }}
            minZoom={0.3}
            maxZoom={2}
          >
            <Background
              variant={BackgroundVariant.Dots}
              gap={24}
              size={1}
              color="var(--text-disabled)"
            />
            <Controls showInteractive={false} />
            <MiniMap
              nodeColor={minimapNodeColor}
              maskColor="var(--bg-overlay)"
              style={{ background: "var(--bg-inset)" }}
            />
          </ReactFlow>
        </div>
      );
    }