Initial commit
This commit is contained in:
312
docs/components/waterfall.md
Normal file
312
docs/components/waterfall.md
Normal file
@@ -0,0 +1,312 @@
|
||||
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>
|
||||
);
|
||||
```
|
||||
Reference in New Issue
Block a user