Files
users/app/data-ingestion/sources/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

620 lines
25 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 { 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">MySQLAPIWebhook等多种接入方式</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>
)
}