Files
2025-11-30 09:05:04 +08:00

8.2 KiB

  1. Home
  2. Components
  3. Waterfall

Waterfall

A hierarchical log display component for showing time-based log data with interactive controls and custom actions.

interface WaterfallProps  {
  logData: LogItemType[];
  temporalCursor?: number;
  panelWidth?: number;
  onTemporalCursorChange?: (time: number) => void;
  getIcon: (item: LogItemType) => ReactNode;
  hoveredId?: string | null;
  onItemHover?: (id: string | null) => void;
  minWindow?: number;
  maxWindow?: number;
  zoomFactor?: number;
  enabled?: boolean;
  children?: React.ReactNode;
  className?: string;
}

Types

type LogItemType = TreeDataItemV2 & {
  type: "task" | "attempt" | "info" | "step";
  icon?: "history" | "file-code" | "bot" | "check-circle" | "pause-circle";
  createTime?: number;
  startTime?: number;
  duration?: number;
  time?: number;
  color?: "blue" | "green" | "orange" | "gray-light" | "gray-medium" | "purple";
  isCollapsible?: boolean;
  hasStripes?: boolean;
  isHaltedStep?: boolean;
};

type TreeDataItemV2 = {
  id: string;
  parentId: string | null;
  label: string;
  isCollapsible?: boolean;
  actions?: ReactNode;
  disable?: boolean;
  [key: string]: unknown;
};

Basic Usage

Job registered in queue generate-report Attempt 1 Fetch database records Job halted, waiting for resources... Waiting for image renderer... Render charts Assemble PDF Fetch database records Memory checkpoint data-validation Schema validation Data integrity check Generate validation report Cache cleared email-notification Prepare email template Attach report files Send via SMTP Webhook triggered System health check cleanup-process Remove temporary files Update job status All tasks completed -10s -5s 0ms 5s 10s 15s 20s 25s 30s 20.000s 19.900s 3.000s Halted 4.600s 6.200s 3.000s 12.000s 3.500s 33.000s ```jsx import { Waterfall, LogItemType } from "@vuer-ai/vuer-uikit"; import React, { useState, useMemo } from "react"; import { Bot, CheckCircle2, FileCode, History, Info, PauseCircle, Eye, EyeClosed, Trash2, } from "lucide-react"; import { cn } from "@vuer-ai/vuer-uikit";

const logData: LogItemType[] = [ { id: "0", parentId: null, indent: 0, etype: "info", label: "Job registered in queue", icon: "history", time: 0, color: "purple", }, { id: "1", parentId: null, indent: 0, etype: "task", label: "generate-report", icon: "file-code", createTime: 0, startTime: 0, duration: 20, color: "blue", isCollapsible: true, hasStripes: true, }, { id: "2", parentId: "1", indent: 1, etype: "attempt", label: "Attempt 1", icon: "bot", createTime: 0.1, startTime: 0.1, duration: 19.9, color: "blue", isCollapsible: true, }, { id: "3", parentId: "2", indent: 2, etype: "step", label: "Fetch database records", icon: "check-circle", createTime: 0.2, startTime: 0.5, duration: 3, color: "green", }, { id: "4", parentId: "2", indent: 2, etype: "step", label: "Job halted, waiting for resources...", icon: "pause-circle", startTime: 4, duration: 2, color: "orange", isHaltedStep: true, }, // ... more log items ];

const getIcon = (item: LogItemType) => { const iconColor = (item: LogItemType) => { if (item.label === "generate-report" || item.label === "Assemble PDF") return "text-blue-500"; if (item.icon === "file-code") return "text-muted-foreground"; return ""; };

switch (item.icon) { case "history": return ; case "file-code": return <FileCode className={cn("size-4 shrink-0", iconColor(item))} />; case "bot": return ; case "check-circle": return ; case "pause-circle": return ; default: return ; } };

// Helper functions const getAllChildrenIds = (parentId: string, allItems: LogItemType[]): string[] => { const children: string[] = []; const directChildren = allItems.filter(item => item.parentId === parentId);

for (const child of directChildren) { children.push(child.id); children.push(...getAllChildrenIds(child.id, allItems)); }

return children; };

const isIndirectlyHidden = (itemId: string, hiddenItems: Set, allItems: LogItemType[]): boolean => { const item = allItems.find(i => i.id === itemId); if (!item || !item.parentId) return false;

if (hiddenItems.has(item.parentId)) { return true; }

return isIndirectlyHidden(item.parentId, hiddenItems, allItems); };

// State management const [expandedItems, setExpandedItems] = useState(new Set(["1", "2"])); const [hoveredId, setHoveredId] = useState<string | null>(null); const [hiddenItems, setHiddenItems] = useState<Set>(new Set()); const [deletedItems, setDeletedItems] = useState<Set>(new Set());

// Toggle visibility function const toggleItemVisibility = (itemId: string) => { const wasHidden = hiddenItems.has(itemId);

setHiddenItems(prev => { const newSet = new Set(prev); if (newSet.has(itemId)) { newSet.delete(itemId); } else { newSet.add(itemId); } return newSet; });

// If hiding an expanded item, collapse it if (!wasHidden && expandedItems.has(itemId)) { setExpandedItems(prev => { const newSet = new Set(prev); newSet.delete(itemId); return newSet; }); } };

// Delete item function const deleteItem = (itemId: string) => { const itemsToDelete = [itemId, ...getAllChildrenIds(itemId, logData)];

setDeletedItems(prev => { const newSet = new Set(prev); itemsToDelete.forEach(id => newSet.add(id)); return newSet; });

setHiddenItems(prev => { const newSet = new Set(prev); itemsToDelete.forEach(id => newSet.delete(id)); return newSet; });

setExpandedItems(prev => { const newSet = new Set(prev); itemsToDelete.forEach(id => newSet.delete(id)); return newSet; }); };

// Create actions for each item const createActions = (itemId: string, isDirectlyHidden: boolean, isIndirectlyHiddenItem: boolean, isHovered: boolean) => { const visibilityButton = ( <button onClick={(e) => { e.stopPropagation(); toggleItemVisibility(itemId); }} className='hover:bg-shadow-secondary rounded p-1' > {isDirectlyHidden ? ( ) : isIndirectlyHiddenItem ? (

) : ( )} );

const deleteButton = ( <button onClick={(e) => { e.stopPropagation(); deleteItem(itemId); }} className='hover:bg-shadow-secondary rounded p-1' > <Trash2 className={cn('size-3', (isDirectlyHidden || isIndirectlyHiddenItem) ? 'text-icon-tertiary' : 'text-icon-primary' )} /> );

return (

{isHovered && deleteButton} {visibilityButton}
); };

// Process log data with actions and visibility states const processedLogData = useMemo(() => { return logData .filter(item => !deletedItems.has(item.id)) .map(item => { const isDirectlyHidden = hiddenItems.has(item.id); const isIndirectlyHiddenItem = isIndirectlyHidden(item.id, hiddenItems, logData); const isItemHidden = isDirectlyHidden || isIndirectlyHiddenItem; const isHovered = hoveredId === item.id;

  return {
    ...item,
    actions: createActions(item.id, isDirectlyHidden, isIndirectlyHiddenItem, isHovered),
    disable: isItemHidden,
    isSelectable: !isItemHidden,
  };
});

}, [logData, deletedItems, hiddenItems, hoveredId]);

return (

); ```