feat(FN-3083): add GraphTaskNode wrapper and integrate into dependency grap

The merge introduces a new `GraphTaskNode` wrapper component for the dependency graph plugin (FN-3083) with visual parity tests, adds an AI security scan gate to the plugin install/unpack flows (FN-3077), updates the dashboard PluginManager UI and adds plugin API routes, and cleans up remaining main

Fusion-Task-Id: FN-3083
This commit is contained in:
Fusion
2026-05-07 02:30:51 -07:00
committed by gsxdsm
parent fce3668a83
commit caf4062b76
9 changed files with 335 additions and 37 deletions

View File

@@ -1,7 +1,7 @@
import { Maximize, ZoomIn, ZoomOut } from "lucide-react";
import { useEffect, useMemo, useRef } from "react";
import type { Task } from "@fusion/core";
import { TaskCard } from "@fusion/dashboard/app/components/TaskCard";
import { GraphTaskNode } from "./GraphTaskNode";
import { GraphEdges } from "./edges";
import { filterGraphTasks } from "./filters";
import { computeAutoLayout } from "./layout";
@@ -15,10 +15,42 @@ const NODE_HEIGHT = 100;
export interface DependencyGraphProps {
tasks: Task[];
projectId?: string;
onOpenTaskDetail: (taskId: string) => void;
onOpenTaskDetail?: (taskId: string) => void;
onOpenDetail?: (task: Task) => void;
addToast?: (message: string, type?: "success" | "error" | "info" | "warning") => void;
globalPaused?: boolean;
onUpdateTask?: (id: string, updates: { title?: string; description?: string; dependencies?: string[] }) => Promise<Task>;
onArchiveTask?: (id: string) => Promise<Task>;
onUnarchiveTask?: (id: string) => Promise<Task>;
onDeleteTask?: (id: string, options?: { removeDependencyReferences?: boolean }) => Promise<Task>;
onRetryTask?: (id: string) => Promise<Task>;
onOpenDetailWithTab?: (task: Task, initialTab: "changes") => void;
taskStuckTimeoutMs?: number;
onOpenMission?: (missionId: string) => void;
onMoveTask?: (id: string, column: Task["column"], optionsOrPosition?: { preserveProgress?: boolean } | number) => Promise<Task>;
lastFetchTimeMs?: number;
workflowStepNameLookup?: ReadonlyMap<string, string>;
}
export function DependencyGraph({ tasks, projectId, onOpenTaskDetail }: DependencyGraphProps) {
export function DependencyGraph({
tasks,
projectId,
onOpenTaskDetail,
onOpenDetail,
addToast,
globalPaused,
onUpdateTask,
onArchiveTask,
onUnarchiveTask,
onDeleteTask,
onRetryTask,
onOpenDetailWithTab,
taskStuckTimeoutMs,
onOpenMission,
onMoveTask,
lastFetchTimeMs,
workflowStepNameLookup,
}: DependencyGraphProps) {
const viewportRef = useRef<HTMLDivElement | null>(null);
const filteredTasks = useMemo(() => filterGraphTasks(tasks), [tasks]);
const graphData = useGraphData(filteredTasks);
@@ -78,26 +110,35 @@ export function DependencyGraph({ tasks, projectId, onOpenTaskDetail }: Dependen
) : (
<div className="dependency-graph__canvas" style={{ transform, width: `${bounds.width}px`, height: `${bounds.height}px` }}>
<GraphEdges edges={graphData.edges} positions={positions} nodeWidth={NODE_WIDTH} nodeHeight={NODE_HEIGHT} />
{graphData.nodes.map((node) => {
const position = positions.get(node.task.id);
if (!position) return null;
<div className="dependency-graph__nodes-layer">
{graphData.nodes.map((node) => {
const position = positions.get(node.task.id);
if (!position) return null;
return (
<div
key={node.task.id}
className="dependency-graph__node"
style={{ width: `${NODE_WIDTH}px`, minHeight: `${NODE_HEIGHT}px`, left: `${position.x}px`, top: `${position.y}px` }}
>
<TaskCard
return (
<GraphTaskNode
key={node.task.id}
task={node.task}
projectId={projectId}
onOpenDetail={() => onOpenTaskDetail(node.task.id)}
addToast={() => {}}
disableDrag={true}
style={{ minHeight: `${NODE_HEIGHT}px`, left: `${position.x}px`, top: `${position.y}px` }}
onOpenDetail={onOpenDetail ?? ((task) => onOpenTaskDetail?.(task.id))}
addToast={addToast ?? (() => {})}
globalPaused={globalPaused}
onUpdateTask={onUpdateTask}
onArchiveTask={onArchiveTask}
onUnarchiveTask={onUnarchiveTask}
onDeleteTask={onDeleteTask}
onRetryTask={onRetryTask}
onOpenDetailWithTab={onOpenDetailWithTab}
taskStuckTimeoutMs={taskStuckTimeoutMs}
onOpenMission={onOpenMission}
onMoveTask={onMoveTask}
lastFetchTimeMs={lastFetchTimeMs}
workflowStepNameLookup={workflowStepNameLookup}
/>
</div>
);
})}
);
})}
</div>
</div>
)}
</div>