feat(log-center): 日志中心 v4 全量组件 — 统一查询面板/时间选择器/统计图表/聚类/数据源管理
This commit is contained in:
1 parent
880cf050fe
commit
bdf3c2daf4
30 files changed
+4276
-136
No files matched your search
@@ -0,0 +1,245 @@
|
||||
// DatasourceCard.tsx — 数据源卡片组件 // Logan.Li
|
||||
|
||||
import { Check, X, Link, Pencil, Trash2, Loader2 } from 'lucide-react';
|
||||
import { ENGINE_CONFIG } from './logLibraryTypes';
|
||||
import type { LogDatasource } from './logLibraryTypes';
|
||||
import { StatusDot } from './StatusDot';
|
||||
|
||||
interface DatasourceCardProps {
|
||||
ds: LogDatasource;
|
||||
onEdit: (ds: LogDatasource) => void;
|
||||
onDelete: (ds: LogDatasource) => void;
|
||||
onTest: (ds: LogDatasource) => void;
|
||||
testingId: number | null;
|
||||
testResult: { id: number; success: boolean; message: string } | null;
|
||||
}
|
||||
|
||||
/** 单个数据源卡片:展示引擎、名称、URL、状态及操作按钮 */
|
||||
export function DatasourceCard({ ds, onEdit, onDelete, onTest, testingId, testResult }: DatasourceCardProps) {
|
||||
const cfg = ENGINE_CONFIG[ds.engine];
|
||||
const EngineIcon = cfg.icon;
|
||||
const isTesting = testingId === ds.id;
|
||||
const showResult = testResult?.id === ds.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="dg-card"
|
||||
style={{
|
||||
padding: '16px 18px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 10,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLDivElement).style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLDivElement).style.boxShadow = 'none';
|
||||
}}
|
||||
>
|
||||
{/* 引擎色条 */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 3,
|
||||
background: cfg.color,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 头部:图标 + 名称 + 默认标记 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, paddingTop: 4 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 8,
|
||||
background: `${cfg.color}18`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<EngineIcon size={18} style={{ color: cfg.color }} />
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: 'var(--dg-text-primary)',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
title={ds.name}
|
||||
>
|
||||
{ds.name}
|
||||
</span>
|
||||
{ds.isDefault && (
|
||||
<span
|
||||
style={{
|
||||
fontSize: 11,
|
||||
padding: '1px 6px',
|
||||
borderRadius: 4,
|
||||
background: 'rgba(245,158,11,0.15)',
|
||||
color: 'var(--dg-amber)',
|
||||
fontWeight: 500,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
默认
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span style={{ fontSize: 12, color: 'var(--dg-text-muted)' }}>{cfg.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* URL */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
fontSize: 12,
|
||||
color: 'var(--dg-text-secondary)',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Link size={12} style={{ color: 'var(--dg-text-muted)', flexShrink: 0 }} />
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={ds.url}>
|
||||
{ds.url}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 描述 */}
|
||||
{ds.description && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: 'var(--dg-text-muted)',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{ds.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 状态行 + 操作按钮 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 2 }}>
|
||||
<StatusDot status={ds.status} />
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
{/* 测试连接 */}
|
||||
<button
|
||||
onClick={() => onTest(ds)}
|
||||
disabled={isTesting}
|
||||
title="测试连接"
|
||||
style={{
|
||||
padding: '5px 8px',
|
||||
borderRadius: 5,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: isTesting ? 'var(--dg-blue)' : 'var(--dg-text-muted)',
|
||||
cursor: isTesting ? 'wait' : 'pointer',
|
||||
fontSize: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isTesting) (e.currentTarget as HTMLButtonElement).style.color = 'var(--dg-blue)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isTesting) (e.currentTarget as HTMLButtonElement).style.color = 'var(--dg-text-muted)';
|
||||
}}
|
||||
>
|
||||
{isTesting ? (
|
||||
<Loader2 size={13} className="spin-animation" />
|
||||
) : (
|
||||
<Check size={13} />
|
||||
)}
|
||||
{isTesting ? '测试中' : '测试'}
|
||||
</button>
|
||||
|
||||
{/* 编辑 */}
|
||||
<button
|
||||
onClick={() => onEdit(ds)}
|
||||
title="编辑"
|
||||
style={{
|
||||
padding: 5,
|
||||
borderRadius: 5,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: 'var(--dg-text-muted)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLButtonElement).style.color = 'var(--dg-blue)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLButtonElement).style.color = 'var(--dg-text-muted)';
|
||||
}}
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
|
||||
{/* 删除 */}
|
||||
<button
|
||||
onClick={() => onDelete(ds)}
|
||||
title="删除"
|
||||
style={{
|
||||
padding: 5,
|
||||
borderRadius: 5,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: 'var(--dg-text-muted)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLButtonElement).style.color = 'var(--dg-red)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLButtonElement).style.color = 'var(--dg-text-muted)';
|
||||
}}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 测试结果提示 */}
|
||||
{showResult && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 4,
|
||||
padding: '8px 10px',
|
||||
borderRadius: 6,
|
||||
fontSize: 12,
|
||||
background: testResult.success ? 'rgba(34,197,94,0.1)' : 'rgba(239,68,68,0.1)',
|
||||
color: testResult.success ? 'var(--dg-green)' : 'var(--dg-red)',
|
||||
border: `1px solid ${testResult.success ? 'rgba(34,197,94,0.25)' : 'rgba(239,68,68,0.25)'}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
animation: 'fadeIn 0.2s ease-out',
|
||||
}}
|
||||
>
|
||||
{testResult.success ? <Check size={13} /> : <X size={13} />}
|
||||
{testResult.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user