VIBN Frontend for Coolify deployment
This commit is contained in:
163
components/ui/tree-view.tsx
Normal file
163
components/ui/tree-view.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { ChevronRight, ChevronDown, Circle, CheckCircle2, Clock } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface TreeNode {
|
||||
id: string;
|
||||
label: string;
|
||||
status?: "built" | "in_progress" | "missing";
|
||||
children?: TreeNode[];
|
||||
metadata?: {
|
||||
sessionsCount?: number;
|
||||
commitsCount?: number;
|
||||
cost?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface TreeViewProps {
|
||||
data: TreeNode[];
|
||||
selectedId?: string | null;
|
||||
onSelect?: (node: TreeNode) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface TreeNodeItemProps {
|
||||
node: TreeNode;
|
||||
level: number;
|
||||
selectedId?: string | null;
|
||||
onSelect?: (node: TreeNode) => void;
|
||||
}
|
||||
|
||||
function TreeNodeItem({ node, level, selectedId, onSelect }: TreeNodeItemProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(level === 0); // Auto-expand top level
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
const isSelected = selectedId === node.id;
|
||||
|
||||
const getStatusIcon = () => {
|
||||
if (!node.status) return null;
|
||||
|
||||
switch (node.status) {
|
||||
case "built":
|
||||
return <CheckCircle2 className="h-3 w-3 text-green-600" />;
|
||||
case "in_progress":
|
||||
return <Clock className="h-3 w-3 text-blue-600" />;
|
||||
case "missing":
|
||||
return <Circle className="h-3 w-3 text-gray-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = () => {
|
||||
if (!node.status) return "";
|
||||
|
||||
switch (node.status) {
|
||||
case "built":
|
||||
return "bg-green-50 hover:bg-green-100 border-l-2 border-l-green-500";
|
||||
case "in_progress":
|
||||
return "bg-blue-50 hover:bg-blue-100 border-l-2 border-l-blue-500";
|
||||
case "missing":
|
||||
return "hover:bg-gray-100 border-l-2 border-l-transparent";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 py-1.5 px-2 rounded-md cursor-pointer transition-all group",
|
||||
isSelected
|
||||
? "bg-primary/10 text-primary font-medium"
|
||||
: getStatusColor(),
|
||||
level > 0 && "text-sm"
|
||||
)}
|
||||
style={{ paddingLeft: `${level * 12 + 8}px` }}
|
||||
onClick={() => {
|
||||
if (hasChildren) {
|
||||
setIsExpanded(!isExpanded);
|
||||
}
|
||||
if (onSelect) {
|
||||
onSelect(node);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{hasChildren ? (
|
||||
<button
|
||||
className="hover:bg-muted/50 rounded p-0.5 transition-transform"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsExpanded(!isExpanded);
|
||||
}}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<div className="w-4 flex items-center justify-center">
|
||||
{getStatusIcon()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className={cn(
|
||||
"flex-1 truncate",
|
||||
level === 0 && "font-semibold text-xs uppercase tracking-wide text-muted-foreground",
|
||||
level === 1 && "font-medium",
|
||||
level > 1 && "text-muted-foreground"
|
||||
)}>
|
||||
{node.label}
|
||||
</span>
|
||||
|
||||
{node.metadata && (
|
||||
<div className="flex items-center gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{node.metadata.sessionsCount && node.metadata.sessionsCount > 0 && (
|
||||
<span className="text-[10px] text-blue-600 font-medium bg-blue-100 px-1.5 py-0.5 rounded">
|
||||
{node.metadata.sessionsCount}s
|
||||
</span>
|
||||
)}
|
||||
{node.metadata.commitsCount && node.metadata.commitsCount > 0 && (
|
||||
<span className="text-[10px] text-green-600 font-medium bg-green-100 px-1.5 py-0.5 rounded">
|
||||
{node.metadata.commitsCount}c
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasChildren && isExpanded && (
|
||||
<div className="space-y-0.5">
|
||||
{node.children!.map((child) => (
|
||||
<TreeNodeItem
|
||||
key={child.id}
|
||||
node={child}
|
||||
level={level + 1}
|
||||
selectedId={selectedId}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TreeView({ data, selectedId, onSelect, className }: TreeViewProps) {
|
||||
return (
|
||||
<div className={cn("space-y-0.5", className)}>
|
||||
{data.map((node) => (
|
||||
<TreeNodeItem
|
||||
key={node.id}
|
||||
node={node}
|
||||
level={0}
|
||||
selectedId={selectedId}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user