feat(bd-1mh): Add DependencyDag component to web frontend
- Create DependencyDag.tsx with interactive task dependency visualization - Add DAG types to web frontend types - Add /api/dag endpoint to server.ts - Add CSS styles for DAG panel - Add unit tests Co-Authored-By: Claude Worker <noreply@anthropic.com>
This commit is contained in:
parent
68104053a6
commit
ccbe8e7a36
6 changed files with 1908 additions and 3 deletions
|
|
@ -5,7 +5,7 @@
|
|||
{"id":"bd-195","title":"ALT-007: SQLite direct query fallback","description":"For HUMAN bead bd-3sh. Query beads.db directly using sqlite3 or Node.js better-sqlite3. Bypasses br CLI entirely. Requires sqlite3 CLI or npm package. Fastest access but tight coupling to schema.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-03-03T08:39:58.775979286Z","created_by":"coder","updated_at":"2026-03-03T10:33:32.997760049Z","closed_at":"2026-03-03T10:33:31.799597115Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0,"labels":["alternative","br","resilience","worker"],"comments":[{"id":32,"issue_id":"bd-195","author":"Jed Arden","text":"No longer needed - br v0.1.20 fixes the schema bug natively.","created_at":"2026-03-03T10:33:32Z"}]}
|
||||
{"id":"bd-1a2","title":"P2: Add unit tests for parser.ts","description":"Add comprehensive unit tests for src/parser.ts covering: JSON parsing, formatEvent function, edge cases (malformed JSON, missing fields), and colorization options. Follow vitest patterns from tailer.test.ts.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-03-03T07:50:12.670624516Z","created_by":"coder","updated_at":"2026-03-03T10:45:42.655993370Z","closed_at":"2026-03-03T10:45:42.654557737Z","close_reason":"Parser tests already implemented in bd-5eh (36 tests covering parseLogLine, parseLogLines, formatEvent)","source_repo":".","compaction_level":0,"original_size":0,"labels":["parser","testing","unit-test"]}
|
||||
{"id":"bd-1c6","title":"TEST-002: Add store integration tests","description":"Test Coverage: Add integration tests for EventStore - event indexing, LRU eviction, worker tracking, query performance.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-03-03T07:53:40.409846186Z","created_by":"coder","updated_at":"2026-03-03T07:53:40.409846186Z","closed_at":"2026-03-03T07:53:40.409846186Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["store","testing"]}
|
||||
{"id":"bd-1cc","title":"Port FileHeatmap component to web dashboard","description":"Port the TUI FileHeatmap component (src/tui/components/FileHeatmap.ts) to React for the web dashboard. Add corresponding API endpoint if needed.","status":"in_progress","priority":3,"issue_type":"task","assignee":"coder","created_at":"2026-03-03T14:27:43.759301428Z","created_by":"coder","updated_at":"2026-03-03T14:58:30.513770688Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["frontend","phase-4","web"]}
|
||||
{"id":"bd-1cc","title":"Port FileHeatmap component to web dashboard","description":"Port the TUI FileHeatmap component (src/tui/components/FileHeatmap.ts) to React for the web dashboard. Add corresponding API endpoint if needed.","status":"closed","priority":3,"issue_type":"task","assignee":"coder","created_at":"2026-03-03T14:27:43.759301428Z","created_by":"coder","updated_at":"2026-03-03T15:08:13.822190937Z","closed_at":"2026-03-03T15:08:13.792085291Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0,"labels":["frontend","phase-4","web"]}
|
||||
{"id":"bd-1e1","title":"P3-001: Setup Express HTTP server with static file serving","description":"Phase 3 Web Dashboard: Create Express server in src/web/server.ts that serves static files and handles WebSocket upgrade. Foundation for web dashboard.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-03-03T07:52:09.228852666Z","created_by":"coder","updated_at":"2026-03-03T10:05:21.171663977Z","closed_at":"2026-03-03T10:05:21.171457522Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["phase-3","server","web"]}
|
||||
{"id":"bd-1fe","title":"Add RecoveryPanel component to web frontend","description":"Port TUI RecoveryPanel.ts to React web frontend. Create src/web/frontend/src/components/RecoveryPanel.tsx showing recovery suggestions for stuck workers.","status":"open","priority":2,"issue_type":"task","created_at":"2026-03-03T14:26:22.464306236Z","created_by":"coder","updated_at":"2026-03-03T14:26:22.464306236Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["frontend","phase-3","web"]}
|
||||
{"id":"bd-1fk","title":"P1: Add Express HTTP server for web dashboard","description":"Set up Express.js HTTP server with basic routing for web dashboard. Should serve static files and provide API endpoints for worker data. Part of Phase 3 Web Dashboard.","status":"closed","priority":1,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-03-03T07:50:12.280655428Z","created_by":"coder","updated_at":"2026-03-03T09:37:50.271173474Z","closed_at":"2026-03-03T08:51:00.693337164Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["http","phase-3","server","web"],"comments":[{"id":10,"issue_id":"bd-1fk","author":"Jed Arden","text":"Express server implemented in src/web/server.ts","created_at":"2026-03-03T08:52:43Z"}]}
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
{"id":"bd-1k7","title":"ALERT: Worker claude-code-glm-5-bravo has no work available","description":"# Worker Starvation Alert\n\nWorker **claude-code-glm-5-bravo** has exhausted all priorities and found zero work.\n\nThis is considered an error state - there should always be more work.\n\n## Worker State\n\n- **Executor:** claude-code-glm-5\n- **Model:** glm-5\n- **Workspace:** /home/coder/FABRIC\n- **Root Boundary:** /home/coder/FABRIC\n- **Last completion:** \n- **Beads completed:** 0\n- **Claim success rate:** %\n- **Uptime:** 23231s (h)\n- **Consecutive empty iterations:** 5\n\n## Priorities Exhausted\n\n1. ✗ Local workspace (bottoms-up): No beads in /home/coder/FABRIC or subfolders\n2. ✗ Parent exploration: No suitable workspaces found\n3. ✓ Maintenance: Completed (cleaned orphaned claims/locks)\n4. ✗ Gap analysis: false - No gaps found or created\n5. ✗ HUMAN alternatives: true - No HUMAN beads found to unblock\n\n## Discovered Workspaces\n\nTotal: 1\n\n- /home/coder/FABRIC\n\n## Required Actions\n\n1. Review discovery roots: Are all project folders being scanned?\n2. Check if projects need new features/tasks\n3. Review ROADMAP.md files across projects\n4. Enable gap analysis if disabled: `--enable-gap-analysis`\n5. Enable HUMAN alternatives if disabled\n6. Create manual beads to bootstrap work\n\n---\n*This alert was created automatically by Priority 6*","status":"closed","priority":0,"issue_type":"human","assignee":"coder","created_at":"2026-03-03T10:50:43.655296543Z","created_by":"coder","updated_at":"2026-03-03T10:54:08.648495859Z","closed_at":"2026-03-03T10:54:08.646738418Z","close_reason":"FALSE POSITIVE: Worker starvation alert triggered incorrectly. Ready queue has 22 beads available (ready-queue.json). The br ready command shows epic bd-2kf and other work exists. Worker discovery logic should check ready-queue.json directly before creating starvation alerts. This follows the established pattern for false-positive starvation alerts.","source_repo":".","compaction_level":0,"original_size":0}
|
||||
{"id":"bd-1l5","title":"ALERT: Worker claude-code-glm-5-bravo has no work available","description":"# Worker Starvation Alert\n\nWorker **claude-code-glm-5-bravo** has exhausted all priorities and found zero work.\n\nThis is considered an error state - there should always be more work.\n\n## Worker State\n\n- **Executor:** claude-code-glm-5\n- **Model:** glm-5\n- **Workspace:** /home/coder/FABRIC\n- **Root Boundary:** /home/coder/FABRIC\n- **Last completion:** \n- **Beads completed:** 0\n- **Claim success rate:** %\n- **Uptime:** 30662s (h)\n- **Consecutive empty iterations:** 5\n\n## Priorities Exhausted\n\n1. ✗ Local workspace (bottoms-up): No beads in /home/coder/FABRIC or subfolders\n2. ✗ Parent exploration: No suitable workspaces found\n3. ✓ Maintenance: Completed (cleaned orphaned claims/locks)\n4. ✗ Gap analysis: false - No gaps found or created\n5. ✗ HUMAN alternatives: true - No HUMAN beads found to unblock\n\n## Discovered Workspaces\n\nTotal: 1\n\n- /home/coder/FABRIC\n\n## Required Actions\n\n1. Review discovery roots: Are all project folders being scanned?\n2. Check if projects need new features/tasks\n3. Review ROADMAP.md files across projects\n4. Enable gap analysis if disabled: `--enable-gap-analysis`\n5. Enable HUMAN alternatives if disabled\n6. Create manual beads to bootstrap work\n\n---\n*This alert was created automatically by Priority 6*","status":"closed","priority":0,"issue_type":"human","assignee":"coder","created_at":"2026-03-03T12:54:35.115007190Z","created_by":"coder","updated_at":"2026-03-03T12:55:34.048020006Z","closed_at":"2026-03-03T12:55:34.034977802Z","close_reason":"FALSE POSITIVE: Ready queue has 22 available beads. Worker did not check ready-queue.json before escalating starvation alert. See MEMORY.md pattern.","source_repo":".","compaction_level":0,"original_size":0}
|
||||
{"id":"bd-1lc","title":"ALERT: Worker claude-code-glm-5-bravo has no work available","description":"# Worker Starvation Alert\n\nWorker **claude-code-glm-5-bravo** has exhausted all priorities and found zero work.\n\nThis is considered an error state - there should always be more work.\n\n## Worker State\n\n- **Executor:** claude-code-glm-5\n- **Model:** glm-5\n- **Workspace:** /home/coder/FABRIC\n- **Root Boundary:** /home/coder/FABRIC\n- **Last completion:** \n- **Beads completed:** 0\n- **Claim success rate:** %\n- **Uptime:** 19477s (h)\n- **Consecutive empty iterations:** 5\n\n## Priorities Exhausted\n\n1. ✗ Local workspace (bottoms-up): No beads in /home/coder/FABRIC or subfolders\n2. ✗ Parent exploration: No suitable workspaces found\n3. ✓ Maintenance: Completed (cleaned orphaned claims/locks)\n4. ✗ Gap analysis: false - No gaps found or created\n5. ✗ HUMAN alternatives: true - No HUMAN beads found to unblock\n\n## Discovered Workspaces\n\nTotal: 1\n\n- /home/coder/FABRIC\n\n## Required Actions\n\n1. Review discovery roots: Are all project folders being scanned?\n2. Check if projects need new features/tasks\n3. Review ROADMAP.md files across projects\n4. Enable gap analysis if disabled: `--enable-gap-analysis`\n5. Enable HUMAN alternatives if disabled\n6. Create manual beads to bootstrap work\n\n---\n*This alert was created automatically by Priority 6*","status":"closed","priority":0,"issue_type":"human","created_at":"2026-03-03T09:48:10.053171230Z","created_by":"coder","updated_at":"2026-03-03T09:55:17.989495128Z","closed_at":"2026-03-03T09:55:07.746952209Z","source_repo":".","compaction_level":0,"original_size":0,"comments":[{"id":20,"issue_id":"bd-1lc","author":"Jed Arden","text":"FALSE POSITIVE: ready-queue.json contains 22 available beads. Worker discovery failed to check ready queue before escalating. Closed per Worker Starvation Resolution pattern.","created_at":"2026-03-03T09:55:17Z"}]}
|
||||
{"id":"bd-1mh","title":"Add DependencyDag component to web frontend","description":"Port TUI DependencyDag.ts to React web frontend. Create src/web/frontend/src/components/DependencyDag.tsx with interactive task dependency graph visualization.","status":"open","priority":2,"issue_type":"task","created_at":"2026-03-03T14:26:17.941694993Z","created_by":"coder","updated_at":"2026-03-03T14:26:17.941694993Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["frontend","phase-3","web"]}
|
||||
{"id":"bd-1mh","title":"Add DependencyDag component to web frontend","description":"Port TUI DependencyDag.ts to React web frontend. Create src/web/frontend/src/components/DependencyDag.tsx with interactive task dependency graph visualization.","status":"in_progress","priority":2,"issue_type":"task","assignee":"coder","created_at":"2026-03-03T14:26:17.941694993Z","created_by":"coder","updated_at":"2026-03-03T15:00:57.556080545Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["frontend","phase-3","web"]}
|
||||
{"id":"bd-1mq","title":"ALT-003: Use br list JSON instead of br ready","description":"For HUMAN bd-1sw. Create a wrapper script that uses br list --all --format json (which works) to find available work instead of br ready (which has schema bug). Script created at scripts/br-ready-jsonl.sh. Drop-in replacement for br ready command.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-03-03T08:30:23.972858496Z","created_by":"coder","updated_at":"2026-03-03T09:59:56.375702385Z","closed_at":"2026-03-03T09:59:56.375449151Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["alternative","discovery","worker"],"dependencies":[{"issue_id":"bd-1mq","depends_on_id":"bd-1sw","type":"blocks","created_at":"2026-03-03T08:30:48.335086821Z","created_by":"coder","metadata":"{}","thread_id":""}],"comments":[{"id":23,"issue_id":"bd-1mq","author":"Jed Arden","text":"Implementation verified working. Script outputs 17 available beads correctly in both JSON and table formats.","created_at":"2026-03-03T09:59:56Z"}]}
|
||||
{"id":"bd-1ms","title":"ALERT: Worker claude-code-glm-5-bravo has no work available","description":"# Worker Starvation Alert\n\nWorker **claude-code-glm-5-bravo** has exhausted all priorities and found zero work.\n\nThis is considered an error state - there should always be more work.\n\n## Worker State\n\n- **Executor:** claude-code-glm-5\n- **Model:** glm-5\n- **Workspace:** /home/coder/FABRIC\n- **Root Boundary:** /home/coder/FABRIC\n- **Last completion:** \n- **Beads completed:** 0\n- **Claim success rate:** %\n- **Uptime:** 19113s (h)\n- **Consecutive empty iterations:** 5\n\n## Priorities Exhausted\n\n1. ✗ Local workspace (bottoms-up): No beads in /home/coder/FABRIC or subfolders\n2. ✗ Parent exploration: No suitable workspaces found\n3. ✓ Maintenance: Completed (cleaned orphaned claims/locks)\n4. ✗ Gap analysis: false - No gaps found or created\n5. ✗ HUMAN alternatives: true - No HUMAN beads found to unblock\n\n## Discovered Workspaces\n\nTotal: 1\n\n- /home/coder/FABRIC\n\n## Required Actions\n\n1. Review discovery roots: Are all project folders being scanned?\n2. Check if projects need new features/tasks\n3. Review ROADMAP.md files across projects\n4. Enable gap analysis if disabled: `--enable-gap-analysis`\n5. Enable HUMAN alternatives if disabled\n6. Create manual beads to bootstrap work\n\n---\n*This alert was created automatically by Priority 6*","status":"closed","priority":0,"issue_type":"human","created_at":"2026-03-03T09:42:05.483000034Z","created_by":"coder","updated_at":"2026-03-03T09:44:49.799179920Z","closed_at":"2026-03-03T09:44:49.798973717Z","source_repo":".","compaction_level":0,"original_size":0}
|
||||
{"id":"bd-1of","title":"ALERT: Worker claude-code-glm-5-bravo has no work available","description":"# Worker Starvation Alert\n\nWorker **claude-code-glm-5-bravo** has exhausted all priorities and found zero work.\n\nThis is considered an error state - there should always be more work.\n\n## Worker State\n\n- **Executor:** claude-code-glm-5\n- **Model:** glm-5\n- **Workspace:** /home/coder/FABRIC\n- **Root Boundary:** /home/coder/FABRIC\n- **Last completion:** \n- **Beads completed:** 0\n- **Claim success rate:** %\n- **Uptime:** 17909s (h)\n- **Consecutive empty iterations:** 5\n\n## Priorities Exhausted\n\n1. ✗ Local workspace (bottoms-up): No beads in /home/coder/FABRIC or subfolders\n2. ✗ Parent exploration: No suitable workspaces found\n3. ✓ Maintenance: Completed (cleaned orphaned claims/locks)\n4. ✗ Gap analysis: false - No gaps found or created\n5. ✗ HUMAN alternatives: true - No HUMAN beads found to unblock\n\n## Discovered Workspaces\n\nTotal: 1\n\n- /home/coder/FABRIC\n\n## Required Actions\n\n1. Review discovery roots: Are all project folders being scanned?\n2. Check if projects need new features/tasks\n3. Review ROADMAP.md files across projects\n4. Enable gap analysis if disabled: `--enable-gap-analysis`\n5. Enable HUMAN alternatives if disabled\n6. Create manual beads to bootstrap work\n\n---\n*This alert was created automatically by Priority 6*","status":"closed","priority":0,"issue_type":"human","created_at":"2026-03-03T09:22:01.376456909Z","created_by":"coder","updated_at":"2026-03-03T09:23:18.070431328Z","closed_at":"2026-03-03T09:23:09.029612467Z","source_repo":".","compaction_level":0,"original_size":0,"comments":[{"id":18,"issue_id":"bd-1of","author":"Jed Arden","text":"FALSE POSITIVE - ready-queue.json has 22 available beads. Worker discovery is not checking ready-queue.json properly (known issue tracked in bd-b02). Available work: bd-2zt, bd-2ed, bd-1fk, bd-1sk, bd-2qr and 17 more.","created_at":"2026-03-03T09:23:18Z"}]}
|
||||
|
|
|
|||
648
src/web/frontend/src/components/DependencyDag.tsx
Normal file
648
src/web/frontend/src/components/DependencyDag.tsx
Normal file
|
|
@ -0,0 +1,648 @@
|
|||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
DependencyGraph,
|
||||
DagStats,
|
||||
BeadNode,
|
||||
BeadStatus,
|
||||
DagOptions,
|
||||
DagViewMode,
|
||||
} from '../types';
|
||||
|
||||
interface DependencyDagProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// Status icons and colors
|
||||
const getStatusIcon = (status: BeadStatus): string => {
|
||||
switch (status) {
|
||||
case 'open': return '○';
|
||||
case 'in_progress': return '◐';
|
||||
case 'blocked': return '⛔';
|
||||
case 'completed': return '●';
|
||||
case 'closed': return '✓';
|
||||
case 'deferred': return '⏸';
|
||||
default: return '?';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: BeadStatus): string => {
|
||||
switch (status) {
|
||||
case 'open': return 'var(--text-secondary)';
|
||||
case 'in_progress': return '#00bcd4';
|
||||
case 'blocked': return 'var(--error)';
|
||||
case 'completed': return 'var(--success)';
|
||||
case 'closed': return 'var(--success)';
|
||||
case 'deferred': return 'var(--warning)';
|
||||
default: return 'var(--text-secondary)';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusClassName = (status: BeadStatus): string => {
|
||||
switch (status) {
|
||||
case 'open': return 'status-open';
|
||||
case 'in_progress': return 'status-progress';
|
||||
case 'blocked': return 'status-blocked';
|
||||
case 'completed': return 'status-completed';
|
||||
case 'closed': return 'status-closed';
|
||||
case 'deferred': return 'status-deferred';
|
||||
default: return 'status-unknown';
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityLabel = (priority: number): string => {
|
||||
switch (priority) {
|
||||
case 0: return 'P0';
|
||||
case 1: return 'P1';
|
||||
case 2: return 'P2';
|
||||
case 3: return 'P3';
|
||||
case 4: return 'P4';
|
||||
default: return 'P?';
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityClassName = (priority: number): string => {
|
||||
switch (priority) {
|
||||
case 0: return 'priority-critical';
|
||||
case 1: return 'priority-high';
|
||||
case 2: return 'priority-normal';
|
||||
case 3: return 'priority-low';
|
||||
case 4: return 'priority-backlog';
|
||||
default: return 'priority-unknown';
|
||||
}
|
||||
};
|
||||
|
||||
// Helper functions for graph analysis
|
||||
const getTopBlockers = (graph: DependencyGraph | null, limit: number = 15): BeadNode[] => {
|
||||
if (!graph) return [];
|
||||
const allNodes: BeadNode[] = [];
|
||||
for (const component of graph.components) {
|
||||
allNodes.push(...component.nodes);
|
||||
}
|
||||
allNodes.sort((a, b) => b.dependentCount - a.dependentCount);
|
||||
return allNodes.filter(n => n.dependentCount > 0).slice(0, limit);
|
||||
};
|
||||
|
||||
const getReadyBeads = (graph: DependencyGraph | null): BeadNode[] => {
|
||||
if (!graph) return [];
|
||||
const ready: BeadNode[] = [];
|
||||
for (const component of graph.components) {
|
||||
for (const node of component.nodes) {
|
||||
if (node.status === 'open' && node.dependencyCount === 0) {
|
||||
ready.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
ready.sort((a, b) => a.priority - b.priority);
|
||||
return ready;
|
||||
};
|
||||
|
||||
const DependencyDag: React.FC<DependencyDagProps> = ({ visible, onClose }) => {
|
||||
const [graph, setGraph] = useState<DependencyGraph | null>(null);
|
||||
const [stats, setStats] = useState<DagStats | null>(null);
|
||||
const [viewMode, setViewMode] = useState<DagViewMode>('tree');
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [filterOptions, setFilterOptions] = useState<DagOptions>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedBead, setSelectedBead] = useState<BeadNode | null>(null);
|
||||
|
||||
// Fetch dependency graph from API
|
||||
const fetchGraph = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (filterOptions.status && filterOptions.status !== 'all') {
|
||||
params.set('status', filterOptions.status);
|
||||
}
|
||||
if (filterOptions.criticalOnly) {
|
||||
params.set('criticalOnly', 'true');
|
||||
}
|
||||
if (filterOptions.maxDepth !== undefined) {
|
||||
params.set('maxDepth', filterOptions.maxDepth.toString());
|
||||
}
|
||||
if (filterOptions.includeClosed) {
|
||||
params.set('includeClosed', 'true');
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/dag?${params.toString()}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch DAG: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setGraph(data.graph);
|
||||
setStats(data.stats);
|
||||
setSelectedIndex(0);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filterOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
fetchGraph();
|
||||
}
|
||||
}, [visible, fetchGraph]);
|
||||
|
||||
// Get items based on view mode
|
||||
const currentItems = useMemo(() => {
|
||||
if (!graph) return [];
|
||||
switch (viewMode) {
|
||||
case 'blockers':
|
||||
return getTopBlockers(graph);
|
||||
case 'ready':
|
||||
return getReadyBeads(graph);
|
||||
default:
|
||||
const allNodes: BeadNode[] = [];
|
||||
for (const component of graph.components) {
|
||||
allNodes.push(...component.nodes);
|
||||
}
|
||||
return allNodes;
|
||||
}
|
||||
}, [graph, viewMode]);
|
||||
|
||||
// Cycle filter options
|
||||
const cycleFilter = useCallback(() => {
|
||||
const filters: Array<{ key: keyof DagOptions; value: any }> = [
|
||||
{ key: 'status', value: undefined },
|
||||
{ key: 'status', value: 'blocked' as BeadStatus },
|
||||
{ key: 'status', value: 'in_progress' as BeadStatus },
|
||||
{ key: 'criticalOnly', value: true },
|
||||
{ key: 'criticalOnly', value: false },
|
||||
];
|
||||
|
||||
const currentIdx = filters.findIndex(
|
||||
(f) =>
|
||||
(f.key === 'status' && filterOptions.status === f.value) ||
|
||||
(f.key === 'criticalOnly' && filterOptions.criticalOnly === f.value)
|
||||
);
|
||||
|
||||
const nextIdx = (currentIdx + 1) % filters.length;
|
||||
const nextFilter = filters[nextIdx];
|
||||
|
||||
setFilterOptions({ ...filterOptions, [nextFilter.key]: nextFilter.value });
|
||||
}, [filterOptions]);
|
||||
|
||||
// Get filter description
|
||||
const getFilterDescription = (): string => {
|
||||
const parts: string[] = [];
|
||||
if (filterOptions.status) {
|
||||
parts.push(`status=${filterOptions.status}`);
|
||||
}
|
||||
if (filterOptions.criticalOnly) {
|
||||
parts.push('critical-only');
|
||||
}
|
||||
if (filterOptions.maxDepth !== undefined) {
|
||||
parts.push(`depth≤${filterOptions.maxDepth}`);
|
||||
}
|
||||
return parts.length > 0 ? ` [${parts.join(', ')}]` : '';
|
||||
};
|
||||
|
||||
// Render tree node recursively
|
||||
const renderTreeNode = (
|
||||
node: BeadNode,
|
||||
componentIndex: number,
|
||||
depth: number,
|
||||
isLast: boolean,
|
||||
visited: Set<string>
|
||||
): React.ReactNode => {
|
||||
if (depth > 5) return null;
|
||||
if (visited.has(node.id)) {
|
||||
return (
|
||||
<div key={`${node.id}-cycle`} className="dag-tree-node cycle" style={{ paddingLeft: depth * 16 }}>
|
||||
↩ {node.id} (cycle)
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const component = graph?.components[componentIndex];
|
||||
if (!component) return null;
|
||||
|
||||
const children = component.edges
|
||||
.filter(e => e.from === node.id)
|
||||
.map(e => component.nodes.find(n => n.id === e.to))
|
||||
.filter((n): n is BeadNode => n !== undefined);
|
||||
|
||||
return (
|
||||
<div key={node.id} className="dag-tree-node-wrapper">
|
||||
<div
|
||||
className={`dag-tree-node ${selectedBead?.id === node.id ? 'selected' : ''}`}
|
||||
style={{ paddingLeft: depth * 16 }}
|
||||
onClick={() => setSelectedBead(node)}
|
||||
>
|
||||
<span className="dag-tree-connector">{isLast ? '└─' : '├─'}</span>
|
||||
<span className={`dag-status-icon ${getStatusClassName(node.status)}`}>
|
||||
{getStatusIcon(node.status)}
|
||||
</span>
|
||||
<span className="dag-node-id" style={{ color: getStatusColor(node.status) }}>
|
||||
{node.id}
|
||||
</span>
|
||||
<span className={`dag-priority ${getPriorityClassName(node.priority)}`}>
|
||||
[{getPriorityLabel(node.priority)}]
|
||||
</span>
|
||||
{node.isCriticalPath && <span className="dag-critical-icon">⚡</span>}
|
||||
</div>
|
||||
{children.map((child, i) =>
|
||||
renderTreeNode(
|
||||
child,
|
||||
componentIndex,
|
||||
depth + 1,
|
||||
i === children.length - 1,
|
||||
new Set([...visited, node.id])
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Render tree view
|
||||
const renderTreeView = (): React.ReactNode => {
|
||||
if (!graph) return null;
|
||||
|
||||
if (graph.components.length === 0) {
|
||||
return (
|
||||
<div className="dag-empty">
|
||||
<p>No dependencies found</p>
|
||||
<p className="dag-empty-hint">Tasks with dependencies will appear here.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dag-tree-container">
|
||||
{graph.components.map((component, componentIndex) => (
|
||||
<div key={`component-${componentIndex}`} className="dag-component">
|
||||
{component.hasCycle && (
|
||||
<div className="dag-cycle-warning">
|
||||
⚠ Cycle detected in this component!
|
||||
</div>
|
||||
)}
|
||||
{component.criticalPath.length > 0 && (
|
||||
<div className="dag-critical-path">
|
||||
⚡ Critical path: {component.criticalPath.map((id, i) => (
|
||||
<React.Fragment key={id}>
|
||||
<span className="dag-critical-node">{id}</span>
|
||||
{i < component.criticalPath.length - 1 && <span className="dag-arrow">→</span>}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="dag-tree">
|
||||
{component.roots.length > 0
|
||||
? component.roots.map((rootId, i) => {
|
||||
const rootNode = component.nodes.find(n => n.id === rootId);
|
||||
if (!rootNode) return null;
|
||||
return (
|
||||
<React.Fragment key={rootId}>
|
||||
{renderTreeNode(
|
||||
rootNode,
|
||||
componentIndex,
|
||||
0,
|
||||
i === component.roots.length - 1,
|
||||
new Set()
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
: component.nodes.map((node, i) =>
|
||||
renderTreeNode(node, componentIndex, 0, i === component.nodes.length - 1, new Set())
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Render blockers view
|
||||
const renderBlockersView = (): React.ReactNode => {
|
||||
const blockers = getTopBlockers(graph, 15);
|
||||
|
||||
if (blockers.length === 0) {
|
||||
return (
|
||||
<div className="dag-empty">
|
||||
<p className="dag-success-text">No blockers found!</p>
|
||||
<p>All tasks are unblocked.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dag-list-container">
|
||||
<p className="dag-list-header">Tasks blocking the most other tasks:</p>
|
||||
<div className="dag-list">
|
||||
{blockers.map((node, i) => (
|
||||
<div
|
||||
key={node.id}
|
||||
className={`dag-list-item ${selectedIndex === i ? 'selected' : ''}`}
|
||||
onClick={() => {
|
||||
setSelectedIndex(i);
|
||||
setSelectedBead(node);
|
||||
}}
|
||||
>
|
||||
<span className={`dag-status-icon ${getStatusClassName(node.status)}`}>
|
||||
{getStatusIcon(node.status)}
|
||||
</span>
|
||||
<span className="dag-node-id" style={{ color: getStatusColor(node.status) }}>
|
||||
{node.id}
|
||||
</span>
|
||||
<span className={`dag-priority ${getPriorityClassName(node.priority)}`}>
|
||||
[{getPriorityLabel(node.priority)}]
|
||||
</span>
|
||||
<span className="dag-blocked-count">
|
||||
<strong>{node.dependentCount}</strong> blocked
|
||||
</span>
|
||||
{node.isCriticalPath && <span className="dag-critical-icon">⚡</span>}
|
||||
<div className="dag-item-title">{node.title.slice(0, 50)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Render ready view
|
||||
const renderReadyView = (): React.ReactNode => {
|
||||
const ready = getReadyBeads(graph);
|
||||
|
||||
if (ready.length === 0) {
|
||||
return (
|
||||
<div className="dag-empty">
|
||||
<p className="dag-warning-text">No ready tasks found.</p>
|
||||
<p>All open tasks have blocking dependencies.</p>
|
||||
<p>Complete blockers to unlock new work.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dag-list-container">
|
||||
<p className="dag-list-header">{ready.length} tasks ready to work on:</p>
|
||||
<div className="dag-list">
|
||||
{ready.map((node, i) => (
|
||||
<div
|
||||
key={node.id}
|
||||
className={`dag-list-item ${selectedIndex === i ? 'selected' : ''}`}
|
||||
onClick={() => {
|
||||
setSelectedIndex(i);
|
||||
setSelectedBead(node);
|
||||
}}
|
||||
>
|
||||
<span className={`dag-status-icon ${getStatusClassName(node.status)}`}>
|
||||
{getStatusIcon(node.status)}
|
||||
</span>
|
||||
<span className="dag-node-id" style={{ color: getStatusColor(node.status) }}>
|
||||
{node.id}
|
||||
</span>
|
||||
<span className={`dag-priority ${getPriorityClassName(node.priority)}`}>
|
||||
[{getPriorityLabel(node.priority)}]
|
||||
</span>
|
||||
{node.isCriticalPath && <span className="dag-critical-icon">⚡</span>}
|
||||
<div className="dag-item-title">{node.title.slice(0, 50)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Render stats view
|
||||
const renderStatsView = (): React.ReactNode => {
|
||||
if (!stats || !graph) return null;
|
||||
|
||||
return (
|
||||
<div className="dag-stats-container">
|
||||
<div className="dag-stats-section">
|
||||
<h3>Overview</h3>
|
||||
<div className="dag-stats-row">
|
||||
<span className="dag-stats-label">Total Beads:</span>
|
||||
<span className="dag-stats-value">{stats.totalBeads}</span>
|
||||
</div>
|
||||
<div className="dag-stats-row">
|
||||
<span className="dag-stats-label">Components:</span>
|
||||
<span className="dag-stats-value">{graph.totalComponents}</span>
|
||||
</div>
|
||||
<div className="dag-stats-row">
|
||||
<span className="dag-stats-label">Total Edges:</span>
|
||||
<span className="dag-stats-value">{graph.totalEdges}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dag-stats-section">
|
||||
<h3>Status Breakdown</h3>
|
||||
<div className="dag-stats-row">
|
||||
<span className="dag-stats-label status-ready">Ready:</span>
|
||||
<span className="dag-stats-value">{stats.readyCount}</span>
|
||||
</div>
|
||||
<div className="dag-stats-row">
|
||||
<span className="dag-stats-label status-blocked">Blocked:</span>
|
||||
<span className="dag-stats-value">{stats.blockedCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dag-stats-section">
|
||||
<h3>Graph Depth</h3>
|
||||
<div className="dag-stats-row">
|
||||
<span className="dag-stats-label">Maximum:</span>
|
||||
<span className="dag-stats-value">{stats.maxDepth}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dag-stats-section">
|
||||
<h3>Critical Path</h3>
|
||||
<div className="dag-stats-row">
|
||||
<span className="dag-stats-label">Length:</span>
|
||||
<span className="dag-stats-value">{stats.criticalPathLength}</span>
|
||||
</div>
|
||||
<div className="dag-stats-row">
|
||||
<span className="dag-stats-label">Beads on path:</span>
|
||||
<span className="dag-stats-value">{stats.criticalPathBeads}</span>
|
||||
</div>
|
||||
{graph.globalCriticalPath.length > 0 && (
|
||||
<div className="dag-critical-path-preview">
|
||||
<span className="dag-stats-label">Path:</span>
|
||||
<div className="dag-path-nodes">
|
||||
{graph.globalCriticalPath.slice(0, 5).map((id, i) => (
|
||||
<React.Fragment key={id}>
|
||||
<span className="dag-path-node">→ {id}</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{graph.globalCriticalPath.length > 5 && (
|
||||
<span className="dag-path-more">
|
||||
... and {graph.globalCriticalPath.length - 5} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="dag-stats-section">
|
||||
<h3>Averages</h3>
|
||||
<div className="dag-stats-row">
|
||||
<span className="dag-stats-label">Dependencies:</span>
|
||||
<span className="dag-stats-value">{stats.avgDependencies.toFixed(1)}</span>
|
||||
</div>
|
||||
<div className="dag-stats-row">
|
||||
<span className="dag-stats-label">Dependents:</span>
|
||||
<span className="dag-stats-value">{stats.avgDependents.toFixed(1)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{stats.cycleCount > 0 && (
|
||||
<div className="dag-cycle-warning-section">
|
||||
⚠ {stats.cycleCount} cycle(s) detected!
|
||||
<p>Circular dependencies prevent proper execution.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div className="dag-panel">
|
||||
<div className="dag-header">
|
||||
<h2>
|
||||
<span className="dag-header-icon">🔗</span>
|
||||
Task Dependency DAG
|
||||
{graph && <span className="dag-count">{graph.totalNodes}</span>}
|
||||
</h2>
|
||||
<div className="dag-header-actions">
|
||||
<button
|
||||
className="dag-btn dag-btn-secondary"
|
||||
onClick={cycleFilter}
|
||||
title="Cycle filter options"
|
||||
>
|
||||
🔍 Filter
|
||||
</button>
|
||||
<button
|
||||
className="dag-btn dag-btn-secondary"
|
||||
onClick={fetchGraph}
|
||||
disabled={loading}
|
||||
title="Refresh"
|
||||
>
|
||||
{loading ? '⏳' : '🔄'} Refresh
|
||||
</button>
|
||||
<button className="dag-btn dag-btn-close" onClick={onClose}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dag-view-modes">
|
||||
<button
|
||||
className={`dag-mode-btn ${viewMode === 'tree' ? 'active' : ''}`}
|
||||
onClick={() => setViewMode('tree')}
|
||||
>
|
||||
🌳 Tree
|
||||
</button>
|
||||
<button
|
||||
className={`dag-mode-btn ${viewMode === 'blockers' ? 'active' : ''}`}
|
||||
onClick={() => setViewMode('blockers')}
|
||||
>
|
||||
🚫 Blockers
|
||||
</button>
|
||||
<button
|
||||
className={`dag-mode-btn ${viewMode === 'ready' ? 'active' : ''}`}
|
||||
onClick={() => setViewMode('ready')}
|
||||
>
|
||||
✅ Ready
|
||||
</button>
|
||||
<button
|
||||
className={`dag-mode-btn ${viewMode === 'stats' ? 'active' : ''}`}
|
||||
onClick={() => setViewMode('stats')}
|
||||
>
|
||||
📊 Stats
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="dag-content">
|
||||
{loading && !graph && (
|
||||
<div className="dag-loading">Loading dependency graph...</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="dag-error">
|
||||
<p>Error loading dependency graph</p>
|
||||
<p className="dag-error-message">{error}</p>
|
||||
<button className="dag-btn dag-btn-primary" onClick={fetchGraph}>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && (
|
||||
<>
|
||||
<div className="dag-filter-info">
|
||||
{viewMode.charAt(0).toUpperCase() + viewMode.slice(1)} View{getFilterDescription()}
|
||||
</div>
|
||||
<div className="dag-scroll-content">
|
||||
{viewMode === 'tree' && renderTreeView()}
|
||||
{viewMode === 'blockers' && renderBlockersView()}
|
||||
{viewMode === 'ready' && renderReadyView()}
|
||||
{viewMode === 'stats' && renderStatsView()}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedBead && (
|
||||
<div className="dag-detail-panel">
|
||||
<div className="dag-detail-header">
|
||||
<h3>{selectedBead.id}</h3>
|
||||
<button
|
||||
className="dag-detail-close"
|
||||
onClick={() => setSelectedBead(null)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="dag-detail-content">
|
||||
<div className="dag-detail-row">
|
||||
<span className="dag-detail-label">Title:</span>
|
||||
<span className="dag-detail-value">{selectedBead.title}</span>
|
||||
</div>
|
||||
<div className="dag-detail-row">
|
||||
<span className="dag-detail-label">Status:</span>
|
||||
<span
|
||||
className={`dag-detail-value dag-status ${getStatusClassName(selectedBead.status)}`}
|
||||
>
|
||||
{getStatusIcon(selectedBead.status)} {selectedBead.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="dag-detail-row">
|
||||
<span className="dag-detail-label">Priority:</span>
|
||||
<span className={`dag-detail-value ${getPriorityClassName(selectedBead.priority)}`}>
|
||||
{getPriorityLabel(selectedBead.priority)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="dag-detail-row">
|
||||
<span className="dag-detail-label">Depth:</span>
|
||||
<span className="dag-detail-value">{selectedBead.depth}</span>
|
||||
</div>
|
||||
<div className="dag-detail-row">
|
||||
<span className="dag-detail-label">Dependencies:</span>
|
||||
<span className="dag-detail-value">{selectedBead.dependencyCount}</span>
|
||||
</div>
|
||||
<div className="dag-detail-row">
|
||||
<span className="dag-detail-label">Blocking:</span>
|
||||
<span className="dag-detail-value">{selectedBead.dependentCount}</span>
|
||||
</div>
|
||||
{selectedBead.isCriticalPath && (
|
||||
<div className="dag-detail-critical">
|
||||
⚡ On critical path
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DependencyDag;
|
||||
|
|
@ -1005,3 +1005,873 @@ body {
|
|||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
File Heatmap Component Styles
|
||||
============================================ */
|
||||
|
||||
.file-heatmap-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--bg-tertiary);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
min-width: 350px;
|
||||
max-width: 450px;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.file-heatmap-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-tertiary);
|
||||
border-bottom: 1px solid var(--bg-primary);
|
||||
}
|
||||
|
||||
.file-heatmap-header h2 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.file-heatmap-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.file-heatmap-close:hover {
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.file-heatmap-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--bg-primary);
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.file-heatmap-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.file-heatmap-filter input {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--bg-secondary);
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary);
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.file-heatmap-filter input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.file-heatmap-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.file-heatmap-toggle:hover {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.file-heatmap-toggle.active {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.file-heatmap-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.file-heatmap-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.file-heatmap-stat-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.file-heatmap-stat-value {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.file-heatmap-stat-value.critical {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.file-heatmap-stat-value.warning {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.file-heatmap-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.file-heatmap-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.file-heatmap-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-secondary);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.file-heatmap-empty-icon {
|
||||
font-size: 2rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.file-heatmap-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.file-heatmap-item:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.file-heatmap-item.selected {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--accent);
|
||||
}
|
||||
|
||||
.file-heatmap-item.collision {
|
||||
border-left: 3px solid var(--warning);
|
||||
}
|
||||
|
||||
.file-heatmap-icon {
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.file-heatmap-bar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.file-heatmap-bar {
|
||||
height: 8px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.file-heatmap-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
.file-heatmap-bar-fill.cold { background: #4fc3f7; }
|
||||
.file-heatmap-bar-fill.warm { background: #ffb74d; }
|
||||
.file-heatmap-bar-fill.hot { background: #f06292; }
|
||||
.file-heatmap-bar-fill.critical { background: #e53935; }
|
||||
|
||||
.file-heatmap-path {
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-heatmap-mod-count {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.75rem;
|
||||
min-width: 30px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.file-heatmap-workers {
|
||||
color: #00bcd4;
|
||||
font-size: 0.75rem;
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.file-heatmap-collision-indicator {
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-heatmap-collision-indicator.has-collision {
|
||||
color: var(--warning);
|
||||
animation: pulse-warning 2s infinite;
|
||||
}
|
||||
|
||||
.file-heatmap-collision-indicator.multiple-workers {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.file-heatmap-detail {
|
||||
padding: 0.75rem;
|
||||
border-top: 1px solid var(--bg-tertiary);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.file-heatmap-detail-header {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.file-heatmap-detail-path {
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.file-heatmap-detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.25rem 0;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.file-heatmap-detail-label {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.file-heatmap-detail-value {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.file-heatmap-detail-workers {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.file-heatmap-worker-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.file-heatmap-worker-bar {
|
||||
height: 4px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 2px;
|
||||
width: 60px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-heatmap-worker-bar-fill {
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.file-heatmap-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--bg-primary);
|
||||
border-top: 1px solid var(--bg-tertiary);
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.file-heatmap-help {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.file-heatmap-help span {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for */
|
||||
@media (max-width: 768px) {
|
||||
.file-heatmap-panel {
|
||||
max-width: 100%;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.file-heatmap-stats {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Dependency DAG Panel Styles
|
||||
============================================ */
|
||||
|
||||
.dag-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--bg-tertiary);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
min-width: 400px;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.dag-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-tertiary);
|
||||
border-bottom: 1px solid var(--bg-primary);
|
||||
}
|
||||
|
||||
.dag-header h2 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dag-header-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.dag-count {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font-size: 0.7rem;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 10px;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.dag-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.dag-btn {
|
||||
background: var(--bg-tertiary);
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dag-btn:hover:not(:disabled) {
|
||||
background: var(--bg-primary);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.dag-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.dag-btn-secondary {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.dag-btn-close {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.dag-view-modes {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.dag-mode-btn {
|
||||
flex: 1;
|
||||
background: var(--bg-tertiary);
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dag-mode-btn:hover {
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dag-mode-btn.active {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dag-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dag-filter-info {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-tertiary);
|
||||
border-bottom: 1px solid var(--bg-primary);
|
||||
}
|
||||
|
||||
.dag-scroll-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.dag-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.dag-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: var(--error);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.dag-error-message {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.dag-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dag-empty-hint {
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.dag-success-text {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.dag-warning-text {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
/* Tree View Styles */
|
||||
.dag-tree-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dag-component {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.dag-cycle-warning {
|
||||
padding: 0.5rem;
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
border-radius: 4px;
|
||||
color: var(--error);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.dag-critical-path {
|
||||
padding: 0.5rem;
|
||||
background: rgba(255, 193, 7, 0.1);
|
||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||
border-radius: 4px;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--warning);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.dag-critical-node {
|
||||
font-weight: 600;
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.dag-arrow {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.dag-tree {
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.dag-tree-node-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dag-tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.25rem 0.375rem;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dag-tree-node:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.dag-tree-node.selected {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--accent);
|
||||
}
|
||||
|
||||
.dag-tree-node.cycle {
|
||||
color: var(--error);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.dag-tree-connector {
|
||||
color: var(--text-secondary);
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
.dag-status-icon {
|
||||
font-size: 0.875rem;
|
||||
min-width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dag-status-icon.status-open { color: var(--text-secondary); }
|
||||
.dag-status-icon.status-progress { color: #00bcd4; }
|
||||
.dag-status-icon.status-blocked { color: var(--error); }
|
||||
.dag-status-icon.status-completed { color: var(--success); }
|
||||
.dag-status-icon.status-closed { color: var(--success); }
|
||||
.dag-status-icon.status-deferred { color: var(--warning); }
|
||||
|
||||
.dag-node-id {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dag-priority {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.dag-priority.priority-critical { color: var(--error); font-weight: 600; }
|
||||
.dag-priority.priority-high { color: var(--warning); }
|
||||
.dag-priority.priority-normal { color: var(--text-primary); }
|
||||
.dag-priority.priority-low { color: var(--text-secondary); }
|
||||
.dag-priority.priority-backlog { color: var(--text-secondary); }
|
||||
|
||||
.dag-critical-icon {
|
||||
color: var(--warning);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* List View Styles */
|
||||
.dag-list-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dag-list-header {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dag-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.dag-list-item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dag-list-item:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.dag-list-item.selected {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--accent);
|
||||
}
|
||||
|
||||
.dag-blocked-count {
|
||||
font-size: 0.75rem;
|
||||
color: var(--error);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.dag-item-title {
|
||||
width: 100%;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
/* Stats View Styles */
|
||||
.dag-stats-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.dag-stats-section {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.dag-stats-section h3 {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.dag-stats-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.25rem 0;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.dag-stats-label {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.dag-stats-label.status-ready { color: var(--success); }
|
||||
.dag-stats-label.status-blocked { color: var(--error); }
|
||||
|
||||
.dag-stats-value {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dag-critical-path-preview {
|
||||
margin-top: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid var(--bg-primary);
|
||||
}
|
||||
|
||||
.dag-path-nodes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.dag-path-node {
|
||||
font-size: 0.75rem;
|
||||
color: #9c27b0;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
}
|
||||
|
||||
.dag-path-more {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.dag-cycle-warning-section {
|
||||
padding: 0.75rem;
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
border-radius: 4px;
|
||||
color: var(--error);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.dag-cycle-warning-section p {
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.25rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Detail Panel Styles */
|
||||
.dag-detail-panel {
|
||||
border-top: 1px solid var(--bg-tertiary);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.dag-detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.dag-detail-header h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.dag-detail-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dag-detail-close:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dag-detail-content {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.dag-detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.25rem 0;
|
||||
font-size: 0.8125rem;
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.dag-detail-row:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.dag-detail-label {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.dag-detail-value {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dag-detail-critical {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: rgba(255, 193, 7, 0.1);
|
||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||
border-radius: 4px;
|
||||
color: var(--warning);
|
||||
font-size: 0.8125rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for DAG */
|
||||
@media (max-width: 768px) {
|
||||
.dag-panel {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.dag-view-modes {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dag-mode-btn {
|
||||
flex: 1 1 45%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,3 +174,66 @@ export interface FileHeatmapStats {
|
|||
}
|
||||
|
||||
export type HeatmapSortMode = 'modifications' | 'recent' | 'workers' | 'collisions';
|
||||
|
||||
// Dependency DAG Types
|
||||
export type BeadStatus = 'open' | 'in_progress' | 'blocked' | 'completed' | 'closed' | 'deferred';
|
||||
|
||||
export interface BeadNode {
|
||||
id: string;
|
||||
title: string;
|
||||
status: BeadStatus;
|
||||
priority: number;
|
||||
depth: number;
|
||||
dependentCount: number;
|
||||
dependencyCount: number;
|
||||
isCriticalPath: boolean;
|
||||
estimatedEffort?: number;
|
||||
}
|
||||
|
||||
export interface DependencyEdge {
|
||||
from: string;
|
||||
to: string;
|
||||
isCritical: boolean;
|
||||
}
|
||||
|
||||
export interface DagComponent {
|
||||
nodes: BeadNode[];
|
||||
edges: DependencyEdge[];
|
||||
roots: string[];
|
||||
hasCycle: boolean;
|
||||
criticalPath: string[];
|
||||
maxDepth: number;
|
||||
}
|
||||
|
||||
export interface DependencyGraph {
|
||||
components: DagComponent[];
|
||||
totalNodes: number;
|
||||
totalEdges: number;
|
||||
totalComponents: number;
|
||||
globalCriticalPath: string[];
|
||||
generatedAt: number;
|
||||
}
|
||||
|
||||
export interface DagStats {
|
||||
totalBeads: number;
|
||||
blockedCount: number;
|
||||
readyCount: number;
|
||||
avgDependencies: number;
|
||||
avgDependents: number;
|
||||
maxDepth: number;
|
||||
cycleCount: number;
|
||||
criticalPathLength: number;
|
||||
criticalPathBeads: number;
|
||||
}
|
||||
|
||||
export interface DagOptions {
|
||||
status?: BeadStatus | 'all';
|
||||
minPriority?: number;
|
||||
maxPriority?: number;
|
||||
criticalOnly?: boolean;
|
||||
maxDepth?: number;
|
||||
sortBy?: 'priority' | 'depth' | 'dependents';
|
||||
includeClosed?: boolean;
|
||||
}
|
||||
|
||||
export type DagViewMode = 'tree' | 'blockers' | 'ready' | 'stats';
|
||||
|
|
|
|||
257
src/web/frontend/test/DependencyDag.test.tsx
Normal file
257
src/web/frontend/test/DependencyDag.test.tsx
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
/**
|
||||
* Tests for DependencyDag component
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
|
||||
import DependencyDag from '../src/components/DependencyDag';
|
||||
|
||||
// Mock fetch globally
|
||||
const mockFetch = vi.fn();
|
||||
global.fetch = mockFetch;
|
||||
|
||||
// Mock graph data
|
||||
const mockGraph = {
|
||||
components: [
|
||||
{
|
||||
nodes: [
|
||||
{ id: 'bd-abc', title: 'Test bead', status: 'open', priority: 1, depth: 0, dependentCount: 2, dependencyCount: 0, isCriticalPath: true },
|
||||
{ id: 'bd-def', title: 'Dependent bead', status: 'blocked', priority: 2, depth: 1, dependentCount: 0, dependencyCount: 1, isCriticalPath: false },
|
||||
],
|
||||
edges: [{ from: 'bd-def', to: 'bd-abc', isCritical: true }],
|
||||
roots: ['bd-abc'],
|
||||
hasCycle: false,
|
||||
criticalPath: ['bd-abc'],
|
||||
maxDepth: 1,
|
||||
},
|
||||
],
|
||||
totalNodes: 2,
|
||||
totalEdges: 1,
|
||||
totalComponents: 1,
|
||||
globalCriticalPath: ['bd-abc'],
|
||||
generatedAt: Date.now(),
|
||||
};
|
||||
|
||||
const mockStats = {
|
||||
totalBeads: 2,
|
||||
blockedCount: 1,
|
||||
readyCount: 1,
|
||||
avgDependencies: 0.5,
|
||||
avgDependents: 1,
|
||||
maxDepth: 1,
|
||||
cycleCount: 0,
|
||||
criticalPathLength: 1,
|
||||
criticalPathBeads: 1,
|
||||
};
|
||||
|
||||
// Helper to set up successful mock
|
||||
const setupSuccessMock = (times: number = 1) => {
|
||||
for (let i = 0; i < times; i++) {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ graph: mockGraph, stats: mockStats }),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to set up error mock
|
||||
const setupErrorMock = () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
statusText: 'Internal Server Error',
|
||||
});
|
||||
};
|
||||
|
||||
describe('DependencyDag Component', () => {
|
||||
beforeEach(() => {
|
||||
mockFetch.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Visibility', () => {
|
||||
it('should not render when visible is false', () => {
|
||||
render(<DependencyDag visible={false} onClose={() => {}} />);
|
||||
expect(screen.queryByText('Task Dependency DAG')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render when visible is true', async () => {
|
||||
setupSuccessMock();
|
||||
render(<DependencyDag visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Task Dependency DAG')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('View Modes', () => {
|
||||
it('should show tree view by default', async () => {
|
||||
setupSuccessMock();
|
||||
render(<DependencyDag visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Tree View/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should switch to blockers view when blockers button clicked', async () => {
|
||||
setupSuccessMock();
|
||||
render(<DependencyDag visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /blockers/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /blockers/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Blockers View/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should switch to ready view when ready button clicked', async () => {
|
||||
setupSuccessMock();
|
||||
render(<DependencyDag visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /ready/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /ready/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Ready View/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should switch to stats view when stats button clicked', async () => {
|
||||
setupSuccessMock();
|
||||
render(<DependencyDag visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /stats/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /stats/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Stats View/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Stats View', () => {
|
||||
it('should display stats correctly', async () => {
|
||||
setupSuccessMock();
|
||||
render(<DependencyDag visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /stats/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /stats/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Overview')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Close Button', () => {
|
||||
it('should call onClose when close button clicked', async () => {
|
||||
setupSuccessMock();
|
||||
const onClose = vi.fn();
|
||||
render(<DependencyDag visible={true} onClose={onClose} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /✕/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /✕/i }));
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Refresh', () => {
|
||||
it('should call API on mount when visible', async () => {
|
||||
setupSuccessMock();
|
||||
render(<DependencyDag visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalledWith('/api/dag?');
|
||||
});
|
||||
});
|
||||
|
||||
it('should refresh when refresh button clicked', async () => {
|
||||
setupSuccessMock(2); // Need 2 calls - initial + refresh
|
||||
render(<DependencyDag visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /refresh/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /refresh/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should show error message when API fails', async () => {
|
||||
setupErrorMock();
|
||||
render(<DependencyDag visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Error loading dependency graph/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tree View', () => {
|
||||
it('should display critical path', async () => {
|
||||
setupSuccessMock();
|
||||
render(<DependencyDag visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Critical path:/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display bead nodes', async () => {
|
||||
setupSuccessMock();
|
||||
render(<DependencyDag visible={true} onClose={() => {}} />);
|
||||
|
||||
// Look for any element containing the bead ID text
|
||||
await waitFor(() => {
|
||||
const beadElements = screen.getAllByText((content, element) => {
|
||||
return element?.classList?.contains('dag-node-id') && content.includes('bd-abc');
|
||||
});
|
||||
expect(beadElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filter Options', () => {
|
||||
it('should cycle filters when filter button clicked', async () => {
|
||||
setupSuccessMock(2); // Need 2 calls - initial + filter refresh
|
||||
render(<DependencyDag visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /filter/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /filter/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -10,9 +10,10 @@ import { EventEmitter } from 'events';
|
|||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import { LogEvent, EventFilter, CrossReferenceEntityType, CrossReferenceRelationship } from '../types.js';
|
||||
import { LogEvent, EventFilter, CrossReferenceEntityType, CrossReferenceRelationship, DagOptions, BeadStatus } from '../types.js';
|
||||
import { InMemoryEventStore } from '../store.js';
|
||||
import { CrossReferenceManager, getCrossReferenceManager } from '../crossReferenceManager.js';
|
||||
import { refreshDependencyGraph, getDagStats } from '../tui/dagUtils.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
|
|
@ -125,6 +126,72 @@ export function createWebServer(options: WebServerOptions): WebServer {
|
|||
res.json(collisions);
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// File Heatmap API Endpoints
|
||||
// ============================================
|
||||
|
||||
// Get file heatmap entries
|
||||
app.get('/api/heatmap', (req: Request, res: Response) => {
|
||||
const sortBy = req.query.sortBy as 'modifications' | 'recent' | 'workers' | 'collisions' || undefined;
|
||||
const maxEntries = req.query.maxEntries ? parseInt(req.query.maxEntries as string) : 100;
|
||||
const collisionsOnly = req.query.collisionsOnly === 'true';
|
||||
const directoryFilter = req.query.directoryFilter as string | undefined;
|
||||
|
||||
const entries = store.getFileHeatmap({
|
||||
sortBy,
|
||||
maxEntries,
|
||||
collisionsOnly,
|
||||
directoryFilter,
|
||||
});
|
||||
|
||||
res.json(entries);
|
||||
});
|
||||
|
||||
// Get file heatmap statistics
|
||||
app.get('/api/heatmap/stats', (_req: Request, res: Response) => {
|
||||
const stats = store.getFileHeatmapStats();
|
||||
res.json(stats);
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Dependency DAG API Endpoints
|
||||
// ============================================
|
||||
|
||||
// Get dependency graph
|
||||
app.get('/api/dag', (req: Request, res: Response) => {
|
||||
try {
|
||||
const status = req.query.status as BeadStatus | 'all' | undefined;
|
||||
const criticalOnly = req.query.criticalOnly === 'true';
|
||||
const maxDepth = req.query.maxDepth ? parseInt(req.query.maxDepth as string) : undefined;
|
||||
const includeClosed = req.query.includeClosed === 'true';
|
||||
|
||||
const options: DagOptions = {};
|
||||
if (status && status !== 'all') {
|
||||
options.status = status as BeadStatus;
|
||||
}
|
||||
if (criticalOnly) {
|
||||
options.criticalOnly = true;
|
||||
}
|
||||
if (maxDepth !== undefined) {
|
||||
options.maxDepth = maxDepth;
|
||||
}
|
||||
if (includeClosed) {
|
||||
options.includeClosed = true;
|
||||
}
|
||||
|
||||
const graph = refreshDependencyGraph(options);
|
||||
const stats = getDagStats(graph);
|
||||
|
||||
res.json({ graph, stats });
|
||||
} catch (error) {
|
||||
console.error('Error generating dependency graph:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to generate dependency graph',
|
||||
message: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Cross-Reference API Endpoints
|
||||
// ============================================
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue