Files
users/app/data-ingestion/lineage/page.tsx
v0 b17b488f8e refactor: restructure navigation and module layout
Reorganize navigation and module structure based on new requirements.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
2026-01-31 04:32:36 +00:00

271 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import {
GitBranch,
Search,
ZoomIn,
ZoomOut,
Maximize2,
Database,
Table,
ArrowRight,
Filter,
Download,
} from "lucide-react"
// 第二部分:数据接入 - 数据血缘
export default function DataLineagePage() {
const [searchQuery, setSearchQuery] = useState("")
const [selectedNode, setSelectedNode] = useState<string | null>(null)
// 数据血缘节点
const lineageNodes = [
// 数据源层
{ id: "source-1", type: "source", name: "存客宝-MySQL", level: 0, x: 50, y: 100 },
{ id: "source-2", type: "source", name: "触客宝-MySQL", level: 0, x: 50, y: 200 },
{ id: "source-3", type: "source", name: "数智员工-API", level: 0, x: 50, y: 300 },
// 原始表层
{ id: "raw-1", type: "table", name: "raw_users", level: 1, x: 250, y: 100 },
{ id: "raw-2", type: "table", name: "raw_transactions", level: 1, x: 250, y: 200 },
{ id: "raw-3", type: "table", name: "raw_behaviors", level: 1, x: 250, y: 300 },
// 清洗层
{ id: "clean-1", type: "table", name: "clean_users", level: 2, x: 450, y: 150 },
{ id: "clean-2", type: "table", name: "clean_transactions", level: 2, x: 450, y: 250 },
// 标签层
{ id: "tag-1", type: "table", name: "user_tags", level: 3, x: 650, y: 150 },
{ id: "tag-2", type: "table", name: "user_portraits", level: 3, x: 650, y: 250 },
// 输出层
{ id: "output-1", type: "output", name: "流量包-高价值用户", level: 4, x: 850, y: 150 },
{ id: "output-2", type: "output", name: "API-用户画像", level: 4, x: 850, y: 250 },
]
// 血缘关系
const lineageEdges = [
{ from: "source-1", to: "raw-1" },
{ from: "source-1", to: "raw-2" },
{ from: "source-2", to: "raw-3" },
{ from: "source-3", to: "raw-1" },
{ from: "raw-1", to: "clean-1" },
{ from: "raw-2", to: "clean-2" },
{ from: "raw-3", to: "clean-1" },
{ from: "clean-1", to: "tag-1" },
{ from: "clean-2", to: "tag-1" },
{ from: "clean-1", to: "tag-2" },
{ from: "tag-1", to: "output-1" },
{ from: "tag-2", to: "output-2" },
]
const getNodeColor = (type: string) => {
switch (type) {
case "source":
return "bg-blue-100 border-blue-300 text-blue-700"
case "table":
return "bg-green-100 border-green-300 text-green-700"
case "output":
return "bg-purple-100 border-purple-300 text-purple-700"
default:
return "bg-gray-100 border-gray-300 text-gray-700"
}
}
const getNodeIcon = (type: string) => {
switch (type) {
case "source":
return <Database className="h-4 w-4" />
case "table":
return <Table className="h-4 w-4" />
case "output":
return <ArrowRight className="h-4 w-4" />
default:
return <GitBranch className="h-4 w-4" />
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
<div className="p-6 space-y-6">
{/* 顶部标题 */}
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-gray-900"></h1>
<p className="text-sm text-gray-500 mt-1"></p>
</div>
<div className="flex items-center gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索表名或数据源..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 w-64 bg-white"
/>
</div>
<Button variant="outline">
<Filter className="h-4 w-4 mr-2" />
</Button>
<Button variant="outline">
<Download className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
{/* 图例 */}
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-blue-100 border border-blue-300" />
<span className="text-sm text-gray-600"></span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-green-100 border border-green-300" />
<span className="text-sm text-gray-600"></span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-purple-100 border border-purple-300" />
<span className="text-sm text-gray-600"></span>
</div>
</div>
{/* 血缘图 */}
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-base font-semibold"></CardTitle>
<div className="flex items-center gap-2">
<Button variant="outline" size="icon">
<ZoomOut className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon">
<ZoomIn className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon">
<Maximize2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="relative h-[500px] bg-gray-50 rounded-lg overflow-hidden">
{/* SVG连线 */}
<svg className="absolute inset-0 w-full h-full pointer-events-none">
{lineageEdges.map((edge, index) => {
const fromNode = lineageNodes.find((n) => n.id === edge.from)
const toNode = lineageNodes.find((n) => n.id === edge.to)
if (!fromNode || !toNode) return null
return (
<line
key={index}
x1={fromNode.x + 80}
y1={fromNode.y + 20}
x2={toNode.x}
y2={toNode.y + 20}
stroke="#94a3b8"
strokeWidth="2"
markerEnd="url(#arrowhead)"
/>
)
})}
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#94a3b8" />
</marker>
</defs>
</svg>
{/* 节点 */}
{lineageNodes.map((node) => (
<div
key={node.id}
className={`absolute px-3 py-2 rounded-lg border-2 cursor-pointer transition-all hover:shadow-md ${getNodeColor(node.type)} ${
selectedNode === node.id ? "ring-2 ring-blue-500 ring-offset-2" : ""
}`}
style={{ left: node.x, top: node.y }}
onClick={() => setSelectedNode(node.id)}
>
<div className="flex items-center gap-2">
{getNodeIcon(node.type)}
<span className="text-sm font-medium whitespace-nowrap">{node.name}</span>
</div>
</div>
))}
{/* 层级标签 */}
{[
{ label: "数据源", x: 50 },
{ label: "原始层", x: 250 },
{ label: "清洗层", x: 450 },
{ label: "标签层", x: 650 },
{ label: "输出层", x: 850 },
].map((level, index) => (
<div
key={index}
className="absolute top-2 text-xs text-gray-400 font-medium"
style={{ left: level.x }}
>
{level.label}
</div>
))}
</div>
</CardContent>
</Card>
{/* 节点详情 */}
{selectedNode && (
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-base font-semibold">
- {lineageNodes.find((n) => n.id === selectedNode)?.name}
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<h4 className="text-sm font-medium text-gray-500 mb-2"></h4>
<div className="space-y-2">
{lineageEdges
.filter((e) => e.to === selectedNode)
.map((edge, index) => (
<Badge key={index} variant="secondary" className="mr-2">
{lineageNodes.find((n) => n.id === edge.from)?.name}
</Badge>
))}
{lineageEdges.filter((e) => e.to === selectedNode).length === 0 && (
<span className="text-sm text-gray-400"></span>
)}
</div>
</div>
<div>
<h4 className="text-sm font-medium text-gray-500 mb-2"></h4>
<div className="space-y-2">
{lineageEdges
.filter((e) => e.from === selectedNode)
.map((edge, index) => (
<Badge key={index} variant="secondary" className="mr-2">
{lineageNodes.find((n) => n.id === edge.to)?.name}
</Badge>
))}
{lineageEdges.filter((e) => e.from === selectedNode).length === 0 && (
<span className="text-sm text-gray-400"></span>
)}
</div>
</div>
<div>
<h4 className="text-sm font-medium text-gray-500 mb-2"></h4>
<p className="text-sm text-gray-700">2</p>
<p className="text-sm text-gray-700">1,234,567</p>
</div>
</div>
</CardContent>
</Card>
)}
</div>
</div>
)
}