312 lines
8.2 KiB
Markdown
312 lines
8.2 KiB
Markdown
1. Home
|
|
2. Components
|
|
3. Waterfall
|
|
|
|
# Waterfall
|
|
|
|
A hierarchical log display component for showing time-based log data with interactive controls and custom actions.
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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 <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>
|
|
);
|
|
``` |