- api/server/ 与 api/cloud/ 子目录拆分,保持 @/api 导入不变 - hooks/api 新增 useCredentials、usePromDatasources,增强 useServers - 拆分 CredentialManager、DeploymentWorkload、PrometheusDatasourceManager - ServerManager、CredentialManager、PromDs 接入 TanStack Query
220 lines
8.6 KiB
TypeScript
220 lines
8.6 KiB
TypeScript
// Prometheus 数据源管理 — 列表编排器 // Logan.Li
|
|
import { useState, useEffect } from 'react';
|
|
import {
|
|
Database,
|
|
Plus,
|
|
Edit,
|
|
Trash2,
|
|
Star,
|
|
RefreshCw,
|
|
Key,
|
|
Plug,
|
|
} from 'lucide-react';
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { toast } from 'sonner';
|
|
import { useConfirm } from '@/components/ui/confirm-dialog';
|
|
import { usePromDatasources, usePromDatasourceMutations } from '@/hooks/api';
|
|
|
|
import type { PromDS } from './prometheus-ds/types';
|
|
import { handleTestQuick } from './prometheus-ds/helpers';
|
|
import PromDsFormDialog from './prometheus-ds/PromDsFormDialog';
|
|
|
|
export default function PrometheusDatasourceManager() {
|
|
const _confirm = useConfirm();
|
|
const { data: sources = [], isLoading, isError } = usePromDatasources();
|
|
const { deleteMutation, setDefaultMutation } = usePromDatasourceMutations();
|
|
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const [editingDs, setEditingDs] = useState<PromDS | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (isError) toast.error('获取数据源列表失败');
|
|
}, [isError]);
|
|
|
|
const openCreate = () => {
|
|
setEditingDs(null);
|
|
setDialogOpen(true);
|
|
};
|
|
|
|
const openEdit = (ds: PromDS) => {
|
|
setEditingDs(ds);
|
|
setDialogOpen(true);
|
|
};
|
|
|
|
const handleDelete = async (ds: PromDS) => {
|
|
const ok = await _confirm({
|
|
title: '删除数据源',
|
|
description: `确定删除数据源「${ds.name}」吗?此操作不可撤销。`,
|
|
variant: 'destructive',
|
|
});
|
|
if (!ok) return;
|
|
try {
|
|
await deleteMutation.mutateAsync(ds.id);
|
|
toast.success('已删除');
|
|
} catch {
|
|
toast.error('删除失败');
|
|
}
|
|
};
|
|
|
|
const handleSetDefault = async (ds: PromDS) => {
|
|
try {
|
|
await setDefaultMutation.mutateAsync(ds.id);
|
|
toast.success(`已将「${ds.name}」设为默认数据源`);
|
|
} catch {
|
|
toast.error('设置失败');
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-96">
|
|
<RefreshCw className="animate-spin w-8 h-8 text-[var(--dg-text-secondary)]" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="p-6 space-y-6 max-w-7xl mx-auto">
|
|
{/* 页头 */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">数据源管理</h1>
|
|
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">
|
|
Prometheus 数据源配置,用于指标监控与监控大屏
|
|
</p>
|
|
</div>
|
|
<Button onClick={openCreate} className="gap-2">
|
|
<Plus className="w-4 h-4" />
|
|
添加数据源
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 数据源列表 */}
|
|
<Card className="border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-base flex items-center gap-2">
|
|
<Database className="w-4 h-4 text-blue-400" />
|
|
已配置的数据源
|
|
<Badge variant="secondary" className="ml-2">{sources.length}</Badge>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
{sources.length === 0 ? (
|
|
<div className="py-12 text-center text-[var(--dg-text-secondary)]">
|
|
<Database className="w-12 h-12 mx-auto mb-3 opacity-30" />
|
|
<p className="text-sm">暂无数据源</p>
|
|
<p className="text-xs mt-1">点击「添加数据源」开始配置</p>
|
|
</div>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="border-b border-[var(--dg-border)]">
|
|
<tr className="text-xs text-[var(--dg-text-secondary)]">
|
|
<th className="text-left py-3 px-4 font-medium">名称</th>
|
|
<th className="text-left py-3 px-4 font-medium">URL</th>
|
|
<th className="text-left py-3 px-4 font-medium w-20">默认</th>
|
|
<th className="text-left py-3 px-4 font-medium w-16">状态</th>
|
|
<th className="text-right py-3 px-4 font-medium w-36">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{sources.map(ds => (
|
|
<tr
|
|
key={ds.id}
|
|
className="border-b border-[var(--dg-border)]/50 hover:bg-[var(--dg-bg-tertiary)]/50 transition-colors"
|
|
>
|
|
<td className="py-3 px-4">
|
|
<div className="flex items-center gap-2">
|
|
<img
|
|
src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/prometheus.png"
|
|
alt="Prometheus"
|
|
className="w-5 h-5 rounded"
|
|
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
|
|
/>
|
|
<div>
|
|
<span className="text-sm font-medium text-[var(--dg-text-primary)]">{ds.name}</span>
|
|
{ds.basic_auth && <Key className="w-3 h-3 inline ml-1.5 text-amber-400" />}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="py-3 px-4">
|
|
<code className="text-xs text-blue-300 bg-slate-800/50 px-2 py-0.5 rounded max-w-[320px] block truncate"
|
|
title={ds.url}>
|
|
{ds.url}
|
|
</code>
|
|
</td>
|
|
<td className="py-3 px-4">
|
|
{ds.is_default ? (
|
|
<Badge variant="default" className="bg-amber-500/15 text-amber-400 border-amber-500/30 text-xs">
|
|
<Star className="w-3 h-3 mr-1" /> 默认
|
|
</Badge>
|
|
) : (
|
|
<span className="text-xs text-[var(--dg-text-secondary)]">--</span>
|
|
)}
|
|
</td>
|
|
<td className="py-3 px-4">
|
|
{ds.status === 1 ? (
|
|
<Badge variant="default" className="bg-emerald-500/15 text-emerald-400 border-emerald-500/30 text-xs">
|
|
启用
|
|
</Badge>
|
|
) : (
|
|
<Badge variant="secondary" className="text-xs">禁用</Badge>
|
|
)}
|
|
</td>
|
|
<td className="py-3 px-4">
|
|
<div className="flex items-center justify-end gap-1">
|
|
<Button
|
|
variant="ghost" size="sm"
|
|
className="h-8 px-2"
|
|
onClick={() => handleTestQuick(ds)}
|
|
title="测试连接"
|
|
>
|
|
<Plug className="w-3.5 h-3.5" />
|
|
</Button>
|
|
{!ds.is_default && (
|
|
<Button
|
|
variant="ghost" size="sm"
|
|
className="h-8 px-2 text-amber-400 hover:text-amber-300"
|
|
onClick={() => handleSetDefault(ds)}
|
|
title="设为默认"
|
|
>
|
|
<Star className="w-3.5 h-3.5" />
|
|
</Button>
|
|
)}
|
|
<Button
|
|
variant="ghost" size="sm"
|
|
className="h-8 px-2"
|
|
onClick={() => openEdit(ds)}
|
|
>
|
|
<Edit className="w-3.5 h-3.5" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost" size="sm"
|
|
className="h-8 px-2 text-red-400 hover:text-red-300"
|
|
onClick={() => handleDelete(ds)}
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
</Button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<PromDsFormDialog
|
|
open={dialogOpen}
|
|
onOpenChange={setDialogOpen}
|
|
editingDs={editingDs}
|
|
sources={sources}
|
|
/>
|
|
</div>
|
|
);
|
|
} |