238 lines
8.4 KiB
TypeScript
238 lines
8.4 KiB
TypeScript
import { useState, useCallback, useRef } from 'react';
|
|
import { WorkflowLog } from '../../components/UiComponents/Log/LogTypes';
|
|
|
|
interface OperationData {
|
|
logs: Map<string, WorkflowLog>;
|
|
parentId: string | null;
|
|
expanded: boolean;
|
|
latestProgress: number | null;
|
|
latestStatus: string | null;
|
|
operationName: string | null; // Stable name from first log
|
|
latestMessage: string | null; // Latest status message that updates
|
|
}
|
|
|
|
interface DashboardLogTree {
|
|
operations: Map<string, OperationData>;
|
|
rootOperations: string[];
|
|
logExpandedStates: Map<string, boolean>;
|
|
currentRound: number | null;
|
|
}
|
|
|
|
export function useDashboardLogTree() {
|
|
const [tree, setTree] = useState<DashboardLogTree>({
|
|
operations: new Map(),
|
|
rootOperations: [],
|
|
logExpandedStates: new Map(),
|
|
currentRound: null
|
|
});
|
|
|
|
const treeRef = useRef<DashboardLogTree>(tree);
|
|
treeRef.current = tree;
|
|
|
|
const generateLogId = useCallback((log: WorkflowLog): string => {
|
|
if (log.id) {
|
|
return log.id;
|
|
}
|
|
return `log_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
}, []);
|
|
|
|
const processDashboardLogs = useCallback((logs: WorkflowLog[]) => {
|
|
setTree(prevTree => {
|
|
const newTree: DashboardLogTree = {
|
|
operations: new Map(prevTree.operations),
|
|
rootOperations: [...prevTree.rootOperations],
|
|
logExpandedStates: new Map(prevTree.logExpandedStates),
|
|
currentRound: prevTree.currentRound
|
|
};
|
|
|
|
// Process each log
|
|
logs.forEach(log => {
|
|
if (!log.operationId) {
|
|
return; // Skip logs without operationId
|
|
}
|
|
|
|
const operationId = log.operationId;
|
|
const logId = generateLogId(log);
|
|
|
|
// Get or create operation
|
|
const existingOperation = newTree.operations.get(operationId);
|
|
|
|
// Create new logs Map (copy existing logs if updating)
|
|
const logsMap = existingOperation
|
|
? new Map(existingOperation.logs)
|
|
: new Map();
|
|
|
|
// Store log (Map ensures uniqueness by logId)
|
|
logsMap.set(logId, log);
|
|
|
|
// Determine stable operation name (only set once, never change)
|
|
// Always use formatted operationId as the stable name - don't use log messages
|
|
// Log messages are status updates and should go in latestMessage, not operationName
|
|
let operationName = existingOperation?.operationName || null;
|
|
if (operationName === null) {
|
|
// Remove UUIDs and timestamps from operationId before formatting
|
|
// UUID pattern: 8-4-4-4-12 hex digits (e.g., "1e6d7b14-4f30-40e2-b7a6-748b63b6a7f5")
|
|
// Also remove standalone long hex strings that might be timestamps or IDs
|
|
let cleanedId = operationId
|
|
.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '') // Remove UUIDs
|
|
.replace(/\b[0-9a-f]{32,}\b/gi, '') // Remove long hex strings (timestamps/IDs)
|
|
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
.trim();
|
|
|
|
// Format by splitting on dashes/underscores and capitalizing
|
|
// This creates a stable, readable name like "Workflow Planning" from "workflow-planning"
|
|
const formattedName = cleanedId
|
|
.split(/[-_\s]+/)
|
|
.filter(word => word.length > 0) // Remove empty strings
|
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
.join(' ');
|
|
operationName = formattedName || operationId.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '').trim();
|
|
}
|
|
|
|
// Update latest message (for status tag) - this updates with each poll
|
|
const latestMessage = log.message || existingOperation?.latestMessage || null;
|
|
|
|
// Update parentId if not set yet (from first log entry)
|
|
const parentId = existingOperation?.parentId !== null && existingOperation?.parentId !== undefined
|
|
? existingOperation.parentId
|
|
: (log.parentId !== undefined && log.parentId !== null ? log.parentId : null);
|
|
|
|
// Update latest progress (use latest value)
|
|
const latestProgress = log.progress !== undefined && log.progress !== null
|
|
? log.progress
|
|
: existingOperation?.latestProgress ?? null;
|
|
|
|
// Update latest status (use latest value)
|
|
const latestStatus = log.status !== undefined && log.status !== null
|
|
? log.status
|
|
: existingOperation?.latestStatus ?? null;
|
|
|
|
// Create new operation object to ensure React detects the change
|
|
const operation: OperationData = {
|
|
logs: logsMap,
|
|
parentId,
|
|
expanded: existingOperation?.expanded ?? false,
|
|
latestProgress,
|
|
latestStatus,
|
|
operationName,
|
|
latestMessage
|
|
};
|
|
|
|
newTree.operations.set(operationId, operation);
|
|
});
|
|
|
|
// Rebuild root operations list (operations without parentId)
|
|
// Use Set to ensure uniqueness, then convert back to array
|
|
const rootOpsSet = new Set<string>();
|
|
newTree.operations.forEach((op, opId) => {
|
|
if (op.parentId === null) {
|
|
rootOpsSet.add(opId);
|
|
}
|
|
});
|
|
// Sort by timestamp of earliest log entry (chronological order)
|
|
newTree.rootOperations = Array.from(rootOpsSet).sort((opIdA, opIdB) => {
|
|
const opA = newTree.operations.get(opIdA);
|
|
const opB = newTree.operations.get(opIdB);
|
|
if (!opA || !opB) return 0;
|
|
|
|
// Get earliest log timestamp for each operation
|
|
const logsA = Array.from(opA.logs.values());
|
|
const logsB = Array.from(opB.logs.values());
|
|
|
|
if (logsA.length === 0 && logsB.length === 0) return 0;
|
|
if (logsA.length === 0) return 1; // Put operations without logs at the end
|
|
if (logsB.length === 0) return -1;
|
|
|
|
const earliestA = Math.min(...logsA.map(log => log.timestamp || 0));
|
|
const earliestB = Math.min(...logsB.map(log => log.timestamp || 0));
|
|
|
|
return earliestA - earliestB; // Ascending order (oldest first)
|
|
});
|
|
|
|
return newTree;
|
|
});
|
|
}, [generateLogId]);
|
|
|
|
const clearDashboard = useCallback((resetRound: boolean = false) => {
|
|
setTree({
|
|
operations: new Map(),
|
|
rootOperations: [],
|
|
logExpandedStates: new Map(),
|
|
currentRound: resetRound ? null : treeRef.current.currentRound
|
|
});
|
|
}, []);
|
|
|
|
const toggleOperationExpanded = useCallback((operationId: string) => {
|
|
setTree(prevTree => {
|
|
const operation = prevTree.operations.get(operationId);
|
|
if (!operation) {
|
|
return prevTree;
|
|
}
|
|
|
|
const newTree: DashboardLogTree = {
|
|
...prevTree,
|
|
operations: new Map(prevTree.operations)
|
|
};
|
|
|
|
const updatedOperation = {
|
|
...operation,
|
|
expanded: !operation.expanded
|
|
};
|
|
|
|
newTree.operations.set(operationId, updatedOperation);
|
|
|
|
return newTree;
|
|
});
|
|
}, []);
|
|
|
|
const updateCurrentRound = useCallback((round: number | null) => {
|
|
setTree(prevTree => {
|
|
// Clear dashboard if round changes
|
|
if (prevTree.currentRound !== null && round !== null && prevTree.currentRound !== round) {
|
|
return {
|
|
operations: new Map(),
|
|
rootOperations: [],
|
|
logExpandedStates: new Map(),
|
|
currentRound: round
|
|
};
|
|
}
|
|
|
|
return {
|
|
...prevTree,
|
|
currentRound: round
|
|
};
|
|
});
|
|
}, []);
|
|
|
|
const getChildOperations = useCallback((parentId: string | null): string[] => {
|
|
const currentTree = treeRef.current;
|
|
const childOps = Array.from(currentTree.operations.entries())
|
|
.filter(([_, op]) => op.parentId === parentId)
|
|
.map(([opId, op]) => ({ opId, op }));
|
|
|
|
// Sort by timestamp of earliest log entry (chronological order)
|
|
return childOps.sort((a, b) => {
|
|
const logsA = Array.from(a.op.logs.values());
|
|
const logsB = Array.from(b.op.logs.values());
|
|
|
|
if (logsA.length === 0 && logsB.length === 0) return 0;
|
|
if (logsA.length === 0) return 1; // Put operations without logs at the end
|
|
if (logsB.length === 0) return -1;
|
|
|
|
const earliestA = Math.min(...logsA.map(log => log.timestamp || 0));
|
|
const earliestB = Math.min(...logsB.map(log => log.timestamp || 0));
|
|
|
|
return earliestA - earliestB; // Ascending order (oldest first)
|
|
}).map(({ opId }) => opId);
|
|
}, []);
|
|
|
|
return {
|
|
tree,
|
|
processDashboardLogs,
|
|
clearDashboard,
|
|
toggleOperationExpanded,
|
|
updateCurrentRound,
|
|
getChildOperations
|
|
};
|
|
}
|
|
|