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
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 <History className="size-4 text-purple-500 shrink-0" />;
case "file-code":
return <FileCode className={cn("size-4 shrink-0", iconColor(item))} />;
case "bot":
return <Bot className="size-4 text-muted-foreground shrink-0" />;
case "check-circle":
return <CheckCircle2 className="size-4 text-green-500 shrink-0" />;
case "pause-circle":
return <PauseCircle className="size-4 text-orange-500 shrink-0" />;
default:
return <Info className="size-4 text-muted-foreground shrink-0" />;
}
};
// 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<string>, 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<string>>(new Set());
const [deletedItems, setDeletedItems] = useState<Set<string>>(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 ? (
<EyeClosed className='text-icon-tertiary size-3' />
) : isIndirectlyHiddenItem ? (
<div className='flex size-4 items-center justify-center'>
<div className='text-icon-tertiary size-[3px] bg-current rounded-full' />
</div>
) : (
<Eye className='text-icon-primary size-3' />
)}
</button>
);
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'
)} />
</button>
);
return (
<div className='flex gap-1'>
{isHovered && deleteButton}
{visibilityButton}
</div>
);
};
// 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 (
<div className="w-full">
<Waterfall
className="h-[300px]"
logData={processedLogData}
getIcon={getIcon}
hoveredId={hoveredId}
onItemHover={setHoveredId}
/>
</div>
);