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>
This commit is contained in:
270
app/data-ingestion/lineage/page.tsx
Normal file
270
app/data-ingestion/lineage/page.tsx
Normal file
@@ -0,0 +1,270 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
619
app/data-ingestion/sources/page.tsx
Normal file
619
app/data-ingestion/sources/page.tsx
Normal file
@@ -0,0 +1,619 @@
|
||||
"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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import {
|
||||
Database,
|
||||
Plus,
|
||||
Search,
|
||||
RefreshCw,
|
||||
Settings,
|
||||
Trash2,
|
||||
PlayCircle,
|
||||
PauseCircle,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Clock,
|
||||
FileText,
|
||||
Server,
|
||||
Globe,
|
||||
Webhook,
|
||||
MoreVertical,
|
||||
Eye,
|
||||
Edit,
|
||||
Activity,
|
||||
} from "lucide-react"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
|
||||
// 第二部分:数据接入 - 数据源管理
|
||||
export default function DataSourcesPage() {
|
||||
const [activeTab, setActiveTab] = useState("all")
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [showAddDialog, setShowAddDialog] = useState(false)
|
||||
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
|
||||
const [showLogsDialog, setShowLogsDialog] = useState(false)
|
||||
const [selectedSource, setSelectedSource] = useState<any>(null)
|
||||
const [newSourceType, setNewSourceType] = useState("")
|
||||
|
||||
// 数据源列表
|
||||
const dataSources = [
|
||||
{
|
||||
id: "1",
|
||||
name: "存客宝-MySQL主库",
|
||||
type: "mysql",
|
||||
status: "connected",
|
||||
host: "10.88.182.62:3305",
|
||||
database: "cunke_prod",
|
||||
lastSync: "2分钟前",
|
||||
recordCount: 12584567,
|
||||
syncFrequency: "实时",
|
||||
tables: 45,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "触客宝-行为数据",
|
||||
type: "mysql",
|
||||
status: "connected",
|
||||
host: "10.88.182.63:3306",
|
||||
database: "chuke_behavior",
|
||||
lastSync: "5分钟前",
|
||||
recordCount: 89234156,
|
||||
syncFrequency: "5分钟",
|
||||
tables: 28,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "数智员工-API接口",
|
||||
type: "api",
|
||||
status: "connected",
|
||||
endpoint: "https://api.shuzhi.com/v1",
|
||||
lastSync: "1分钟前",
|
||||
recordCount: 4567890,
|
||||
syncFrequency: "实时",
|
||||
apiCalls: 125680,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "外部征信数据",
|
||||
type: "api",
|
||||
status: "warning",
|
||||
endpoint: "https://credit.external.com/api",
|
||||
lastSync: "30分钟前",
|
||||
recordCount: 234567,
|
||||
syncFrequency: "每小时",
|
||||
apiCalls: 8956,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "Webhook-实时事件",
|
||||
type: "webhook",
|
||||
status: "connected",
|
||||
webhookUrl: "/api/webhook/events",
|
||||
lastSync: "实时",
|
||||
recordCount: 567890,
|
||||
syncFrequency: "实时",
|
||||
events: 45678,
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "腾讯云MySQL",
|
||||
type: "mysql",
|
||||
status: "disconnected",
|
||||
host: "56b4c23f6853c.gz.cdb.myqcloud.com:14413",
|
||||
database: "analytics_db",
|
||||
lastSync: "2小时前",
|
||||
recordCount: 0,
|
||||
syncFrequency: "停止",
|
||||
tables: 0,
|
||||
},
|
||||
]
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case "connected":
|
||||
return <Badge className="bg-green-100 text-green-700">已连接</Badge>
|
||||
case "warning":
|
||||
return <Badge className="bg-yellow-100 text-yellow-700">警告</Badge>
|
||||
case "disconnected":
|
||||
return <Badge className="bg-red-100 text-red-700">已断开</Badge>
|
||||
default:
|
||||
return <Badge variant="secondary">未知</Badge>
|
||||
}
|
||||
}
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "mysql":
|
||||
return <Database className="h-5 w-5 text-blue-500" />
|
||||
case "api":
|
||||
return <Globe className="h-5 w-5 text-green-500" />
|
||||
case "webhook":
|
||||
return <Webhook className="h-5 w-5 text-purple-500" />
|
||||
default:
|
||||
return <Server className="h-5 w-5 text-gray-500" />
|
||||
}
|
||||
}
|
||||
|
||||
const filteredSources = dataSources.filter((source) => {
|
||||
if (activeTab !== "all" && source.type !== activeTab) return false
|
||||
if (searchQuery && !source.name.toLowerCase().includes(searchQuery.toLowerCase())) return false
|
||||
return true
|
||||
})
|
||||
|
||||
const handleOpenSettings = (source: any) => {
|
||||
setSelectedSource(source)
|
||||
setShowSettingsDialog(true)
|
||||
}
|
||||
|
||||
const handleOpenLogs = (source: any) => {
|
||||
setSelectedSource(source)
|
||||
setShowLogsDialog(true)
|
||||
}
|
||||
|
||||
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">统一管理所有数据接入源,支持MySQL、API、Webhook等多种接入方式</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button onClick={() => setShowAddDialog(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
添加数据源
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">数据源总数</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{dataSources.length}</p>
|
||||
</div>
|
||||
<Database className="h-8 w-8 text-blue-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">已连接</p>
|
||||
<p className="text-2xl font-bold text-green-600">
|
||||
{dataSources.filter((s) => s.status === "connected").length}
|
||||
</p>
|
||||
</div>
|
||||
<CheckCircle2 className="h-8 w-8 text-green-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">总记录数</p>
|
||||
<p className="text-2xl font-bold text-gray-900">107.2M</p>
|
||||
</div>
|
||||
<Activity className="h-8 w-8 text-purple-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">今日同步</p>
|
||||
<p className="text-2xl font-bold text-gray-900">2.3M</p>
|
||||
</div>
|
||||
<RefreshCw className="h-8 w-8 text-orange-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 搜索和筛选 */}
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<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 bg-white"
|
||||
/>
|
||||
</div>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="bg-white">
|
||||
<TabsTrigger value="all">全部</TabsTrigger>
|
||||
<TabsTrigger value="mysql">MySQL</TabsTrigger>
|
||||
<TabsTrigger value="api">API</TabsTrigger>
|
||||
<TabsTrigger value="webhook">Webhook</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* 数据源列表 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{filteredSources.map((source) => (
|
||||
<Card key={source.id} className="bg-white/80 backdrop-blur border-0 shadow-sm hover:shadow-md transition-shadow">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 rounded-xl bg-gray-50">
|
||||
{getTypeIcon(source.type)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-gray-900">{source.name}</h3>
|
||||
{getStatusBadge(source.status)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{source.type === "mysql" ? source.host : source.type === "api" ? source.endpoint : source.webhookUrl}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 mt-3 text-sm text-gray-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="h-4 w-4" />
|
||||
{source.lastSync}
|
||||
</span>
|
||||
<span>{source.recordCount.toLocaleString()} 条记录</span>
|
||||
<span>同步频率: {source.syncFrequency}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleOpenSettings(source)}>
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
设置
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleOpenLogs(source)}>
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
查看日志
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
立即同步
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
数据预览
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-red-600">
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 添加数据源弹窗 */}
|
||||
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加数据源</DialogTitle>
|
||||
<DialogDescription>选择数据源类型并配置连接参数</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-6 py-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{[
|
||||
{ type: "mysql", label: "MySQL数据库", icon: Database, desc: "连接MySQL/MariaDB数据库" },
|
||||
{ type: "api", label: "REST API", icon: Globe, desc: "通过API接口获取数据" },
|
||||
{ type: "webhook", label: "Webhook", icon: Webhook, desc: "接收实时推送数据" },
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.type}
|
||||
onClick={() => setNewSourceType(item.type)}
|
||||
className={`p-4 rounded-xl border-2 text-left transition-all ${
|
||||
newSourceType === item.type
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "border-gray-200 hover:border-gray-300"
|
||||
}`}
|
||||
>
|
||||
<item.icon className={`h-8 w-8 mb-2 ${newSourceType === item.type ? "text-blue-500" : "text-gray-400"}`} />
|
||||
<p className="font-medium text-gray-900">{item.label}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">{item.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{newSourceType === "mysql" && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>数据源名称</Label>
|
||||
<Input placeholder="例如:生产环境主库" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>主机地址</Label>
|
||||
<Input placeholder="例如:10.88.182.62:3306" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>数据库名</Label>
|
||||
<Input placeholder="数据库名称" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>用户名</Label>
|
||||
<Input placeholder="数据库用户名" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>密码</Label>
|
||||
<Input type="password" placeholder="数据库密码" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>同步频率</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择同步频率" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="realtime">实时</SelectItem>
|
||||
<SelectItem value="5min">每5分钟</SelectItem>
|
||||
<SelectItem value="15min">每15分钟</SelectItem>
|
||||
<SelectItem value="1hour">每小时</SelectItem>
|
||||
<SelectItem value="daily">每天</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{newSourceType === "api" && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>数据源名称</Label>
|
||||
<Input placeholder="例如:外部API接口" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>API端点</Label>
|
||||
<Input placeholder="https://api.example.com/v1" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>认证方式</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择认证方式" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="bearer">Bearer Token</SelectItem>
|
||||
<SelectItem value="apikey">API Key</SelectItem>
|
||||
<SelectItem value="basic">Basic Auth</SelectItem>
|
||||
<SelectItem value="oauth2">OAuth 2.0</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>认证凭证</Label>
|
||||
<Input type="password" placeholder="Token或密钥" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>请求方法</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择请求方法" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{newSourceType === "webhook" && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>数据源名称</Label>
|
||||
<Input placeholder="例如:实时事件推送" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Webhook路径</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-500">https://your-domain.com</span>
|
||||
<Input placeholder="/api/webhook/your-path" className="flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>签名密钥(可选)</Label>
|
||||
<Input placeholder="用于验证请求来源" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowAddDialog(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={() => setShowAddDialog(false)}>
|
||||
测试连接并保存
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 设置弹窗 */}
|
||||
<Dialog open={showSettingsDialog} onOpenChange={setShowSettingsDialog}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>数据源设置 - {selectedSource?.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Tabs defaultValue="connection" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="connection">连接配置</TabsTrigger>
|
||||
<TabsTrigger value="sync">同步设置</TabsTrigger>
|
||||
<TabsTrigger value="mapping">字段映射</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="connection" className="space-y-4 pt-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>主机地址</Label>
|
||||
<Input defaultValue={selectedSource?.host} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>数据库名</Label>
|
||||
<Input defaultValue={selectedSource?.database} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>用户名</Label>
|
||||
<Input defaultValue="root" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>密码</Label>
|
||||
<Input type="password" defaultValue="********" />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="sync" className="space-y-4 pt-4">
|
||||
<div className="space-y-2">
|
||||
<Label>同步频率</Label>
|
||||
<Select defaultValue="realtime">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="realtime">实时</SelectItem>
|
||||
<SelectItem value="5min">每5分钟</SelectItem>
|
||||
<SelectItem value="15min">每15分钟</SelectItem>
|
||||
<SelectItem value="1hour">每小时</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>增量字段</Label>
|
||||
<Input defaultValue="updated_at" placeholder="用于增量同步的时间字段" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="mapping" className="space-y-4 pt-4">
|
||||
<p className="text-sm text-gray-500">配置源表字段与目标字段的映射关系</p>
|
||||
<div className="border rounded-lg p-4 bg-gray-50">
|
||||
<p className="text-sm text-gray-600">字段映射配置将在下个版本支持</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowSettingsDialog(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={() => setShowSettingsDialog(false)}>
|
||||
保存设置
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 日志弹窗 */}
|
||||
<Dialog open={showLogsDialog} onOpenChange={setShowLogsDialog}>
|
||||
<DialogContent className="max-w-3xl max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>同步日志 - {selectedSource?.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{[
|
||||
{ time: "2026-01-31 14:32:45", level: "info", message: "同步任务开始执行" },
|
||||
{ time: "2026-01-31 14:32:46", level: "info", message: "连接数据库成功" },
|
||||
{ time: "2026-01-31 14:32:47", level: "info", message: "开始读取增量数据,起始时间: 2026-01-31 14:27:45" },
|
||||
{ time: "2026-01-31 14:32:50", level: "info", message: "读取到 1,256 条新记录" },
|
||||
{ time: "2026-01-31 14:32:52", level: "info", message: "数据写入目标表完成" },
|
||||
{ time: "2026-01-31 14:32:53", level: "success", message: "同步任务完成,耗时 8秒" },
|
||||
{ time: "2026-01-31 14:27:45", level: "info", message: "同步任务开始执行" },
|
||||
{ time: "2026-01-31 14:27:46", level: "info", message: "连接数据库成功" },
|
||||
{ time: "2026-01-31 14:27:48", level: "warning", message: "检测到 3 条数据格式异常,已跳过" },
|
||||
{ time: "2026-01-31 14:27:50", level: "info", message: "读取到 2,134 条新记录" },
|
||||
{ time: "2026-01-31 14:27:53", level: "success", message: "同步任务完成,耗时 8秒" },
|
||||
].map((log, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex items-start gap-3 p-3 rounded-lg text-sm ${
|
||||
log.level === "error"
|
||||
? "bg-red-50"
|
||||
: log.level === "warning"
|
||||
? "bg-yellow-50"
|
||||
: log.level === "success"
|
||||
? "bg-green-50"
|
||||
: "bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<span className="text-gray-400 font-mono text-xs whitespace-nowrap">{log.time}</span>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={`text-xs ${
|
||||
log.level === "error"
|
||||
? "bg-red-100 text-red-700"
|
||||
: log.level === "warning"
|
||||
? "bg-yellow-100 text-yellow-700"
|
||||
: log.level === "success"
|
||||
? "bg-green-100 text-green-700"
|
||||
: "bg-blue-100 text-blue-700"
|
||||
}`}
|
||||
>
|
||||
{log.level.toUpperCase()}
|
||||
</Badge>
|
||||
<span className="text-gray-700">{log.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowLogsDialog(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
导出日志
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user