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

934 lines
36 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 {
Database,
Plus,
Search,
Play,
Pause,
RefreshCw,
Settings,
Trash2,
CheckCircle,
XCircle,
Clock,
Server,
FileText,
Globe,
AlertTriangle,
Link2,
} from "lucide-react"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Textarea } from "@/components/ui/textarea"
interface DataSource {
id: string
name: string
type: "mysql" | "postgresql" | "restapi" | "webhook" | "sftp" | "kafka"
status: "running" | "stopped" | "error" | "syncing"
syncMode: "full" | "incremental" | "realtime"
frequency: string
lastSync: string
records: number
errorCount: number
config: Record<string, string>
}
const MOCK_SOURCES: DataSource[] = [
{
id: "1",
name: "存客宝-用户主库",
type: "mysql",
status: "running",
syncMode: "incremental",
frequency: "每5分钟",
lastSync: "2025-12-12 14:30:00",
records: 128956342,
errorCount: 0,
config: { host: "db.cunkebao.com", port: "3306", database: "user_main" },
},
{
id: "2",
name: "触客宝-行为数据",
type: "kafka",
status: "running",
syncMode: "realtime",
frequency: "实时",
lastSync: "2025-12-12 14:35:12",
records: 89234567,
errorCount: 2,
config: { brokers: "kafka.chukebao.com:9092", topic: "user_behavior" },
},
{
id: "3",
name: "数智员工-账号API",
type: "restapi",
status: "running",
syncMode: "incremental",
frequency: "每小时",
lastSync: "2025-12-12 14:00:00",
records: 45678901,
errorCount: 0,
config: { endpoint: "https://api.shuzhi.com/v1/accounts", method: "GET", auth: "Bearer Token" },
},
{
id: "4",
name: "征信数据-外部接口",
type: "restapi",
status: "stopped",
syncMode: "incremental",
frequency: "每天",
lastSync: "2025-12-11 00:00:00",
records: 12890456,
errorCount: 0,
config: { endpoint: "https://credit.external.com/api", method: "POST", auth: "API Key" },
},
{
id: "5",
name: "交易流水-SFTP",
type: "sftp",
status: "running",
syncMode: "incremental",
frequency: "每30分钟",
lastSync: "2025-12-12 14:30:00",
records: 256789012,
errorCount: 1,
config: { host: "sftp.bank.com", path: "/data/transactions" },
},
{
id: "6",
name: "聚宝盆-指标数据",
type: "postgresql",
status: "error",
syncMode: "full",
frequency: "每天",
lastSync: "2025-12-10 00:00:00",
records: 8901234,
errorCount: 15,
config: { host: "pg.jubao.com", port: "5432", database: "metrics" },
},
{
id: "7",
name: "微信消息-Webhook",
type: "webhook",
status: "running",
syncMode: "realtime",
frequency: "实时推送",
lastSync: "2025-12-12 14:36:00",
records: 34567890,
errorCount: 0,
config: { webhookUrl: "/api/webhook/wechat", secret: "***" },
},
]
const TYPE_ICONS = {
mysql: Database,
postgresql: Database,
restapi: Globe,
webhook: Link2,
sftp: FileText,
kafka: Server,
}
const TYPE_COLORS = {
mysql: "bg-blue-100 text-blue-700",
postgresql: "bg-indigo-100 text-indigo-700",
restapi: "bg-green-100 text-green-700",
webhook: "bg-teal-100 text-teal-700",
sftp: "bg-yellow-100 text-yellow-700",
kafka: "bg-purple-100 text-purple-700",
}
const TYPE_LABELS = {
mysql: "MySQL",
postgresql: "PostgreSQL",
restapi: "REST API",
webhook: "Webhook",
sftp: "SFTP",
kafka: "Kafka",
}
const STATUS_CONFIG = {
running: { color: "bg-green-100 text-green-700", icon: CheckCircle, label: "运行中" },
stopped: { color: "bg-gray-100 text-gray-700", icon: Pause, label: "已停止" },
error: { color: "bg-red-100 text-red-700", icon: XCircle, label: "错误" },
syncing: { color: "bg-blue-100 text-blue-700", icon: RefreshCw, label: "同步中" },
}
const syncLogs = [
{ id: 1, time: "2025-12-12 14:30:00", level: "info", message: "开始增量同步...", records: null },
{ id: 2, time: "2025-12-12 14:30:05", level: "info", message: "获取增量数据 2,350 条", records: 2350 },
{ id: 3, time: "2025-12-12 14:30:12", level: "success", message: "同步完成,写入 2,350 条", records: 2350 },
{ id: 4, time: "2025-12-12 14:25:00", level: "info", message: "开始增量同步...", records: null },
{ id: 5, time: "2025-12-12 14:25:08", level: "success", message: "同步完成,写入 1,890 条", records: 1890 },
]
export default function DataSourcesPage() {
const [sources, setSources] = useState<DataSource[]>(MOCK_SOURCES)
const [searchQuery, setSearchQuery] = useState("")
const [filterType, setFilterType] = useState<string>("all")
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false)
const [logsDialogOpen, setLogsDialogOpen] = useState(false)
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [selectedSource, setSelectedSource] = useState<DataSource | null>(null)
const [newSource, setNewSource] = useState({
name: "",
type: "mysql",
host: "",
port: "",
database: "",
// API配置
endpoint: "",
method: "GET",
authType: "none",
authToken: "",
headers: "",
// Webhook配置
webhookPath: "",
webhookSecret: "",
})
const [addStep, setAddStep] = useState(1)
const filteredSources = sources.filter((source) => {
const matchesSearch = source.name.toLowerCase().includes(searchQuery.toLowerCase())
const matchesType = filterType === "all" || source.type === filterType
return matchesSearch && matchesType
})
const stats = {
total: sources.length,
running: sources.filter((s) => s.status === "running").length,
error: sources.filter((s) => s.status === "error").length,
totalRecords: sources.reduce((sum, s) => sum + s.records, 0),
apiCount: sources.filter((s) => s.type === "restapi" || s.type === "webhook").length,
}
const formatNumber = (num: number): string => {
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
return num.toString()
}
const handleToggleStatus = (id: string) => {
setSources((prev) =>
prev.map((s) => {
if (s.id === id) {
return { ...s, status: s.status === "running" ? "stopped" : "running" }
}
return s
}),
)
}
const handleAddSource = () => {
const newId = (sources.length + 1).toString()
const newSourceData: DataSource = {
id: newId,
name: newSource.name,
type: newSource.type as DataSource["type"],
status: "stopped",
syncMode: newSource.type === "webhook" ? "realtime" : "incremental",
frequency: newSource.type === "webhook" ? "实时推送" : "每小时",
lastSync: "-",
records: 0,
errorCount: 0,
config:
newSource.type === "restapi"
? { endpoint: newSource.endpoint, method: newSource.method, auth: newSource.authType }
: newSource.type === "webhook"
? { webhookUrl: newSource.webhookPath, secret: "***" }
: { host: newSource.host, port: newSource.port, database: newSource.database },
}
setSources((prev) => [...prev, newSourceData])
setIsAddDialogOpen(false)
setAddStep(1)
setNewSource({
name: "",
type: "mysql",
host: "",
port: "",
database: "",
endpoint: "",
method: "GET",
authType: "none",
authToken: "",
headers: "",
webhookPath: "",
webhookSecret: "",
})
}
const handleDeleteSource = () => {
if (selectedSource) {
setSources((prev) => prev.filter((s) => s.id !== selectedSource.id))
setDeleteDialogOpen(false)
setSelectedSource(null)
}
}
const renderConfigForm = () => {
switch (newSource.type) {
case "restapi":
return (
<div className="space-y-4">
<div className="space-y-2">
<Label>API端点URL</Label>
<Input
placeholder="https://api.example.com/v1/users"
value={newSource.endpoint}
onChange={(e) => setNewSource({ ...newSource, endpoint: e.target.value })}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Select value={newSource.method} onValueChange={(v) => setNewSource({ ...newSource, method: v })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="GET">GET</SelectItem>
<SelectItem value="POST">POST</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Select value={newSource.authType} onValueChange={(v) => setNewSource({ ...newSource, authType: v })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
<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>
{newSource.authType !== "none" && (
<div className="space-y-2">
<Label>
{newSource.authType === "bearer" ? "Token" : newSource.authType === "apikey" ? "API Key" : "凭证"}
</Label>
<Input
type="password"
placeholder="输入认证凭证"
value={newSource.authToken}
onChange={(e) => setNewSource({ ...newSource, authToken: e.target.value })}
/>
</div>
)}
<div className="space-y-2">
<Label>Headers (JSON格式)</Label>
<Textarea
placeholder='{"Content-Type": "application/json"}'
value={newSource.headers}
onChange={(e) => setNewSource({ ...newSource, headers: e.target.value })}
rows={3}
/>
</div>
</div>
)
case "webhook":
return (
<div className="space-y-4">
<div className="p-4 bg-blue-50 rounded-lg">
<p className="text-sm text-blue-700">
Webhook模式下Webhook URL提供给数据推送方
</p>
</div>
<div className="space-y-2">
<Label>Webhook路径</Label>
<div className="flex gap-2">
<span className="flex items-center px-3 bg-gray-100 rounded-l-md text-sm text-gray-600">
https://api.yourplatform.com
</span>
<Input
placeholder="/webhook/your-source"
value={newSource.webhookPath}
onChange={(e) => setNewSource({ ...newSource, webhookPath: e.target.value })}
className="rounded-l-none"
/>
</div>
</div>
<div className="space-y-2">
<Label> ()</Label>
<div className="flex gap-2">
<Input
type="password"
placeholder="自动生成或手动输入"
value={newSource.webhookSecret}
onChange={(e) => setNewSource({ ...newSource, webhookSecret: e.target.value })}
/>
<Button
variant="outline"
onClick={() => setNewSource({ ...newSource, webhookSecret: Math.random().toString(36).slice(2) })}
>
</Button>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<Select defaultValue="json">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="json">JSON</SelectItem>
<SelectItem value="xml">XML</SelectItem>
<SelectItem value="form">Form Data</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)
default:
return (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Input
placeholder="localhost"
value={newSource.host}
onChange={(e) => setNewSource({ ...newSource, host: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
placeholder="3306"
value={newSource.port}
onChange={(e) => setNewSource({ ...newSource, port: e.target.value })}
/>
</div>
</div>
<div className="space-y-2">
<Label>{newSource.type === "kafka" ? "Topic" : "数据库名称"}</Label>
<Input
placeholder={newSource.type === "kafka" ? "topic_name" : "database_name"}
value={newSource.database}
onChange={(e) => setNewSource({ ...newSource, database: e.target.value })}
/>
</div>
{newSource.type !== "kafka" && (
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Input placeholder="root" />
</div>
<div className="space-y-2">
<Label></Label>
<Input type="password" placeholder="••••••••" />
</div>
</div>
)}
</div>
)
}
}
return (
<div className="space-y-6 p-6">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-gray-900"></h1>
<p className="text-gray-500 mt-1">API</p>
</div>
<Dialog
open={isAddDialogOpen}
onOpenChange={(open) => {
setIsAddDialogOpen(open)
if (!open) setAddStep(1)
}}
>
<DialogTrigger asChild>
<Button className="bg-gradient-to-r from-blue-500 to-purple-500 text-white">
<Plus className="w-4 h-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>{addStep === 1 ? "选择数据源类型并填写基本信息" : "配置连接参数"}</DialogDescription>
</DialogHeader>
{addStep === 1 ? (
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label></Label>
<Input
placeholder="例如:用户主库"
value={newSource.name}
onChange={(e) => setNewSource({ ...newSource, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label></Label>
<div className="grid grid-cols-3 gap-3">
{[
{ type: "mysql", label: "MySQL", icon: Database, desc: "关系型数据库" },
{ type: "postgresql", label: "PostgreSQL", icon: Database, desc: "关系型数据库" },
{ type: "restapi", label: "REST API", icon: Globe, desc: "HTTP接口拉取" },
{ type: "webhook", label: "Webhook", icon: Link2, desc: "数据推送接收" },
{ type: "kafka", label: "Kafka", icon: Server, desc: "消息队列" },
{ type: "sftp", label: "SFTP", icon: FileText, desc: "文件传输" },
].map((item) => (
<button
key={item.type}
onClick={() => setNewSource({ ...newSource, type: item.type })}
className={`p-3 rounded-lg border-2 text-left transition-all ${
newSource.type === item.type
? "border-blue-500 bg-blue-50"
: "border-gray-200 hover:border-gray-300"
}`}
>
<item.icon
className={`w-5 h-5 mb-1 ${newSource.type === item.type ? "text-blue-500" : "text-gray-400"}`}
/>
<div className="font-medium text-sm">{item.label}</div>
<div className="text-xs text-gray-500">{item.desc}</div>
</button>
))}
</div>
</div>
</div>
) : (
<div className="py-4">{renderConfigForm()}</div>
)}
<DialogFooter>
{addStep === 2 && (
<Button variant="outline" onClick={() => setAddStep(1)}>
</Button>
)}
<Button
variant="outline"
onClick={() => {
setIsAddDialogOpen(false)
setAddStep(1)
}}
>
</Button>
{addStep === 1 ? (
<Button onClick={() => setAddStep(2)} disabled={!newSource.name}>
</Button>
) : (
<Button onClick={handleAddSource}></Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
</div>
{/* Stats - 添加API数据源统计 */}
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4">
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
<Database className="w-4 h-4" />
</div>
<div className="text-2xl font-bold text-gray-900">{stats.total}</div>
</CardContent>
</Card>
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
<Globe className="w-4 h-4 text-green-500" />
API接入
</div>
<div className="text-2xl font-bold text-green-600">{stats.apiCount}</div>
</CardContent>
</Card>
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
<CheckCircle className="w-4 h-4 text-green-500" />
</div>
<div className="text-2xl font-bold text-green-600">{stats.running}</div>
</CardContent>
</Card>
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
<AlertTriangle className="w-4 h-4 text-red-500" />
</div>
<div className="text-2xl font-bold text-red-600">{stats.error}</div>
</CardContent>
</Card>
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
<FileText className="w-4 h-4" />
</div>
<div className="text-2xl font-bold text-gray-900">{formatNumber(stats.totalRecords)}</div>
</CardContent>
</Card>
</div>
{/* Filters - 更新筛选选项 */}
<div className="flex flex-col md:flex-row gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="搜索数据源..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-white/60 backdrop-blur-sm"
/>
</div>
<Select value={filterType} onValueChange={setFilterType}>
<SelectTrigger className="w-full md:w-48 bg-white/60 backdrop-blur-sm">
<SelectValue placeholder="筛选类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="mysql">MySQL</SelectItem>
<SelectItem value="postgresql">PostgreSQL</SelectItem>
<SelectItem value="restapi">REST API</SelectItem>
<SelectItem value="webhook">Webhook</SelectItem>
<SelectItem value="sftp">SFTP文件</SelectItem>
<SelectItem value="kafka">Kafka</SelectItem>
</SelectContent>
</Select>
</div>
{/* Data Source List */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{filteredSources.map((source) => {
const TypeIcon = TYPE_ICONS[source.type]
const StatusConfig = STATUS_CONFIG[source.status]
const StatusIcon = StatusConfig.icon
return (
<Card
key={source.id}
className="border-none shadow-sm bg-white/60 backdrop-blur-sm hover:shadow-md transition-shadow"
>
<CardContent className="p-5">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div
className={`w-10 h-10 rounded-xl ${TYPE_COLORS[source.type]} flex items-center justify-center`}
>
<TypeIcon className="w-5 h-5" />
</div>
<div>
<h3 className="font-semibold text-gray-900">{source.name}</h3>
<div className="flex items-center gap-2 mt-1">
<Badge variant="secondary" className="text-xs">
{TYPE_LABELS[source.type]}
</Badge>
<Badge className={`text-xs ${StatusConfig.color}`}>
<StatusIcon className={`w-3 h-3 mr-1 ${source.status === "syncing" ? "animate-spin" : ""}`} />
{StatusConfig.label}
</Badge>
</div>
</div>
</div>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleToggleStatus(source.id)}
className="h-8 w-8 p-0"
>
{source.status === "running" ? (
<Pause className="w-4 h-4 text-gray-500" />
) : (
<Play className="w-4 h-4 text-green-500" />
)}
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => {
setSelectedSource(source)
setSettingsDialogOpen(true)
}}
>
<Settings className="w-4 h-4 text-gray-500" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => {
setSelectedSource(source)
setDeleteDialogOpen(true)
}}
>
<Trash2 className="w-4 h-4 text-red-500" />
</Button>
</div>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-500"></span>
<span className="text-gray-900 ml-1">
{source.syncMode === "full" ? "全量" : source.syncMode === "realtime" ? "实时" : "增量"}
</span>
</div>
<div>
<span className="text-gray-500"></span>
<span className="text-gray-900 ml-1">{source.frequency}</span>
</div>
<div>
<span className="text-gray-500"></span>
<span className="text-gray-900 ml-1 font-medium">{formatNumber(source.records)}</span>
</div>
<div>
<span className="text-gray-500"></span>
<span className={`ml-1 font-medium ${source.errorCount > 0 ? "text-red-600" : "text-green-600"}`}>
{source.errorCount}
</span>
</div>
</div>
{(source.type === "restapi" || source.type === "webhook") && (
<div className="mt-3 p-2 bg-gray-50 rounded-lg">
<code className="text-xs text-gray-600">
{source.type === "restapi"
? `${source.config.method} ${source.config.endpoint}`
: source.config.webhookUrl}
</code>
</div>
)}
<div className="mt-4 pt-4 border-t border-gray-100">
<div className="flex items-center justify-between text-xs text-gray-500">
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
: {source.lastSync}
</span>
<Button
variant="link"
size="sm"
className="h-auto p-0 text-blue-500"
onClick={() => {
setSelectedSource(source)
setLogsDialogOpen(true)
}}
>
</Button>
</div>
</div>
</CardContent>
</Card>
)
})}
</div>
{/* Settings Dialog */}
<Dialog open={settingsDialogOpen} onOpenChange={setSettingsDialogOpen}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle> - {selectedSource?.name}</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{selectedSource && (
<Tabs defaultValue="connection" className="py-4">
<TabsList>
<TabsTrigger value="connection"></TabsTrigger>
<TabsTrigger value="sync"></TabsTrigger>
<TabsTrigger value="mapping"></TabsTrigger>
</TabsList>
<TabsContent value="connection" className="space-y-4 mt-4">
{selectedSource.type === "restapi" ? (
<>
<div className="space-y-2">
<Label>API端点</Label>
<Input defaultValue={selectedSource.config.endpoint} />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Select defaultValue={selectedSource.config.method}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="GET">GET</SelectItem>
<SelectItem value="POST">POST</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Input defaultValue={selectedSource.config.auth} />
</div>
</div>
</>
) : selectedSource.type === "webhook" ? (
<>
<div className="space-y-2">
<Label>Webhook URL</Label>
<Input defaultValue={selectedSource.config.webhookUrl} readOnly />
</div>
<div className="space-y-2">
<Label></Label>
<Input type="password" defaultValue={selectedSource.config.secret} />
</div>
</>
) : (
<>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Input defaultValue={selectedSource.config.host} />
</div>
<div className="space-y-2">
<Label></Label>
<Input defaultValue={selectedSource.config.port} />
</div>
</div>
<div className="space-y-2">
<Label>/Topic</Label>
<Input defaultValue={selectedSource.config.database || selectedSource.config.topic} />
</div>
</>
)}
</TabsContent>
<TabsContent value="sync" className="space-y-4 mt-4">
<div className="space-y-2">
<Label></Label>
<Select defaultValue={selectedSource.syncMode}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="full"></SelectItem>
<SelectItem value="incremental"></SelectItem>
<SelectItem value="realtime"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Select defaultValue="5min">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="realtime"></SelectItem>
<SelectItem value="1min"></SelectItem>
<SelectItem value="5min">5</SelectItem>
<SelectItem value="30min">30</SelectItem>
<SelectItem value="1hour"></SelectItem>
<SelectItem value="1day"></SelectItem>
</SelectContent>
</Select>
</div>
</TabsContent>
<TabsContent value="mapping" className="mt-4">
<div className="p-4 bg-gray-50 rounded-lg text-center text-gray-500">
</div>
</TabsContent>
</Tabs>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setSettingsDialogOpen(false)}>
</Button>
<Button onClick={() => setSettingsDialogOpen(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Logs Dialog */}
<Dialog open={logsDialogOpen} onOpenChange={setLogsDialogOpen}>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle> - {selectedSource?.name}</DialogTitle>
</DialogHeader>
<div className="max-h-96 overflow-y-auto">
<div className="space-y-2">
{syncLogs.map((log) => (
<div
key={log.id}
className={`flex items-start gap-3 p-3 rounded-lg text-sm ${
log.level === "success" ? "bg-green-50" : log.level === "warning" ? "bg-yellow-50" : "bg-gray-50"
}`}
>
<span className="text-gray-400 font-mono text-xs">{log.time}</span>
<span
className={`px-2 py-0.5 rounded text-xs ${
log.level === "success"
? "bg-green-100 text-green-700"
: log.level === "warning"
? "bg-yellow-100 text-yellow-700"
: "bg-gray-100 text-gray-700"
}`}
>
{log.level.toUpperCase()}
</span>
<span className="flex-1">{log.message}</span>
</div>
))}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setLogsDialogOpen(false)}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription> "{selectedSource?.name}" </DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
</Button>
<Button variant="destructive" onClick={handleDeleteSource}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}