refactor(quality): Q31 前端巨型组件拆分 — PromDsFormDialog 528L→294L (-44%), 提取 prometheus-ds/ 6 个文件 (CollapsibleSection 42L 通用折叠包装器 DRY 3 个重复折叠模式 + AuthSection 107L 认证折叠 + HeadersSection 64L 自定义Headers + AlertingSection 58L 告警折叠 + MiscSection 47L 杂项折叠 + TestResultBanner 32L 测试结果横幅), Auth/Alerting/Misc 三个折叠区块统一使用 CollapsibleSection 消除 ~30 行重复折叠 JSX, updateForm 泛型化 <K extends keyof> 类型安全提升, 零逻辑变更, ESLint errors=0/warnings=65/any=0, tsc -b=0, npm build✓ (17.65s), go build=0, 17/17 测试通过

This commit is contained in:
attacker committed 2026-07-09 20:51:11 +08:00
1 parent 45f7342d1e
commit 195b6945b4
9 files changed
+405 -250

No files matched your search

+1 -1
View File
@@ -129,7 +129,7 @@
| ~~`api.ts` 1324 行/352 方法~~ → 已拆分为 12 个域文件 (api/kubernetes.ts, deploy.ts, monitor.ts 等) | ✅ 已解决 | 前端 | ~~P0~~ done | | ~~`api.ts` 1324 行/352 方法~~ → 已拆分为 12 个域文件 (api/kubernetes.ts, deploy.ts, monitor.ts 等) | ✅ 已解决 | 前端 | ~~P0~~ done |
| 前端 **1026 处** `any` 类型 | 类型安全缺失,重构风险高 | 前端全局 | ~~P0~~ **done** (any 1076→40, unused 25→6, console 93→1, tsc --noEmit=0) | | 前端 **1026 处** `any` 类型 | 类型安全缺失,重构风险高 | 前端全局 | ~~P0~~ **done** (any 1076→40, unused 25→6, console 93→1, tsc --noEmit=0) |
| ~~K8s 双路由体系(集群域+Kuboard 域)~~ → code 路由统一为主,ID 路由标记 deprecated 向后兼容 | ✅ 已解决 | 后端 | ~~P1~~ done | | ~~K8s 双路由体系(集群域+Kuboard 域)~~ → code 路由统一为主,ID 路由标记 deprecated 向后兼容 | ✅ 已解决 | 后端 | ~~P1~~ done |
| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-Q27 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%) + NamespaceResourcePage 719L→300L (-58%) + ApprovalWorkflowPage 705L→167L (-76%) + PipelineRunDetail 678L→273L (-60%) + CloudServerImport 699L→404L (-42%,any[]→具体接口) + UnifiedQueryPanel 682L→529L (-22%) + ServerManager 652L→442L (-32%) + PipelineEditor 676L→406L (-40%) + RdpDialog 642L→442L (-31%) + DeploymentEditService 627L→394L (-37%) + TraceManager 605L→331L (-45%,修复 SpanTreeNode 内联定义反模式) + GrafanaDashboard 624L→78L (-87%,DRY 合并 Zabbix/Prometheus Tab + 移除文件级 eslint-disable + 类型安全升级) + TopologyView 596L→472L (-21%) + DeploymentEditAdvanced 590L→289L (-51%) + WorkloadViewPage 572L→348L (-39%) + CloudAccountManager 572L→308L + ExcelServerImport 560L→385L + PipelineCreateDialog 559L→498L + PodLogsViewer 553L→331L (-40%) + HostMonitor 547L→224L (-59%) + KubernetesClusterDetail 547L→130L (-76%) + **AlertCenter 542L→312L (-42%,提取 4 个受控组件: AlertsToolbar + AlertsTab + ContactsTab + ContactGroupsTab,客户端搜索过滤+删除确认逻辑下沉到子组件)** + **CloudBilling 534L→295L (-45%,提取 cloud-billing/ 3 个文件: OverviewTab + RecordsTab(含 RecordsFilterToolbar) + AlertRulesTab)**,累计提取 106 个独立组件文件** | | 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-Q27 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%) + NamespaceResourcePage 719L→300L (-58%) + ApprovalWorkflowPage 705L→167L (-76%) + PipelineRunDetail 678L→273L (-60%) + CloudServerImport 699L→404L (-42%,any[]→具体接口) + UnifiedQueryPanel 682L→529L (-22%) + ServerManager 652L→442L (-32%) + PipelineEditor 676L→406L (-40%) + RdpDialog 642L→442L (-31%) + DeploymentEditService 627L→394L (-37%) + TraceManager 605L→331L (-45%,修复 SpanTreeNode 内联定义反模式) + GrafanaDashboard 624L→78L (-87%,DRY 合并 Zabbix/Prometheus Tab + 移除文件级 eslint-disable + 类型安全升级) + TopologyView 596L→472L (-21%) + DeploymentEditAdvanced 590L→289L (-51%) + WorkloadViewPage 572L→348L (-39%) + CloudAccountManager 572L→308L + ExcelServerImport 560L→385L + PipelineCreateDialog 559L→498L + PodLogsViewer 553L→331L (-40%) + HostMonitor 547L→224L (-59%) + KubernetesClusterDetail 547L→130L (-76%) + **AlertCenter 542L→312L (-42%,提取 4 个受控组件: AlertsToolbar + AlertsTab + ContactsTab + ContactGroupsTab,客户端搜索过滤+删除确认逻辑下沉到子组件)** + **CloudBilling 534L→295L (-45%,提取 cloud-billing/ 3 个文件: OverviewTab + RecordsTab(含 RecordsFilterToolbar) + AlertRulesTab)** + **PromDsFormDialog 528L→294L (-44%,提取 6 个文件: CollapsibleSection 通用折叠包装器 DRY 3 个重复折叠模式 + AuthSection + HeadersSection + AlertingSection + MiscSection + TestResultBanner)**,累计提取 112 个独立组件文件** |
### 3.2 🟡 中优先级 ### 3.2 🟡 中优先级
+41 -1
View File
@@ -501,7 +501,47 @@ services/api.ts (1324L)
| Q27 | 前端巨型组件拆分(HostMonitor 547L→224L (-59%),提取 host-monitor/tabs/ 8 个文件 (BasicTab + DiskTab + NetworkTab + PortMonitorTab + ProcessTable + FDUsageCard + ContextSwitchCard + ProcessTab),TABS 常量数组提取为模块级,PCT_REF_LINES 常量提升到 types.ts,端口/进程数组类型 JsonData→JsonData[] 类型安全升级) | P0 | **done** | | Q27 | 前端巨型组件拆分(HostMonitor 547L→224L (-59%),提取 host-monitor/tabs/ 8 个文件 (BasicTab + DiskTab + NetworkTab + PortMonitorTab + ProcessTable + FDUsageCard + ContextSwitchCard + ProcessTab),TABS 常量数组提取为模块级,PCT_REF_LINES 常量提升到 types.ts,端口/进程数组类型 JsonData→JsonData[] 类型安全升级) | P0 | **done** |
| Q28 | 前端巨型组件拆分(KubernetesClusterDetail 547L→130L (-76%),提取 cluster-detail/ 8 个文件 (types(computeClusterMetrics/buildTrendData 纯函数) + OverviewTab + NodesTab + PodsTab + DeploymentsTab + ServicesTab + IngressesTab + EventsTab)) | P0 | **done** | | Q28 | 前端巨型组件拆分(KubernetesClusterDetail 547L→130L (-76%),提取 cluster-detail/ 8 个文件 (types(computeClusterMetrics/buildTrendData 纯函数) + OverviewTab + NodesTab + PodsTab + DeploymentsTab + ServicesTab + IngressesTab + EventsTab)) | P0 | **done** |
| Q29 | 前端巨型组件拆分(AlertCenter 542L→312L (-42%),提取 4 个受控组件 (AlertsToolbar 89L 筛选工具栏 + AlertsTab 149L 告警列表表格+分页 + ContactsTab 87L 联系人卡片网格 + ContactGroupsTab 88L 联系组卡片网格),客户端搜索过滤逻辑移入 AlertsTab,删除确认逻辑下沉到子组件) | P0 | **done** | | Q29 | 前端巨型组件拆分(AlertCenter 542L→312L (-42%),提取 4 个受控组件 (AlertsToolbar 89L 筛选工具栏 + AlertsTab 149L 告警列表表格+分页 + ContactsTab 87L 联系人卡片网格 + ContactGroupsTab 88L 联系组卡片网格),客户端搜索过滤逻辑移入 AlertsTab,删除确认逻辑下沉到子组件) | P0 | **done** |
| Q30 | 前端巨型组件拆分(CloudBilling 534L→295L (-45%),提取 cloud-billing/ 3 个文件 (OverviewTab 110L 概览统计:同步信息+成本告警横幅+趋势图+厂商分布 + RecordsTab 146L 成本记录表格+筛选工具栏 + AlertRulesTab 91L 预警规则卡片网格)) | P0 | **done** | | Q30 | 前端巨型组件拆分(CloudBilling 534L→295L (-45%),提取 cloud-billing/ 3 个文件 (OverviewTab 110L 概览统计:同步信息+成本告警横幅+趋势图+厂商分布 + RecordsTab 146L 成本记录表格+RecordsFilterToolbar筛选工具栏 + AlertRulesTab 91L 预警规则卡片网格)) | P0 | **done** |
| Q31 | 前端巨型组件拆分(PromDsFormDialog 528L→294L (-44%),提取 6 个文件 (CollapsibleSection 42L 通用折叠包装器 DRY 3 个重复折叠模式 + AuthSection 107L 认证折叠 + HeadersSection 64L 自定义Headers + AlertingSection 58L 告警折叠 + MiscSection 47L 杂项折叠 + TestResultBanner 32L 测试结果横幅),3 个折叠区块全部使用共享 CollapsibleSection 消除重复 JSX)) | P0 | **done** |
---
## Q31: 前端巨型组件拆分 — PromDsFormDialog [done]
**问题**: `PromDsFormDialog.tsx`(528 行)是 Q30 拆分后项目最大的监控业务组件之一,包含 Prometheus 数据源添加/编辑弹窗的全部逻辑:表单状态管理(8 个 useState + form 对象)、初始化 effect、保存/测试连接业务逻辑、以及 8 个内联 JSX 区块(Name&Default ~23 行 / HTTP 设置 ~29 行 / Auth 折叠 ~97 行 / Custom HTTP Headers ~38 行 / Alerting 折叠 ~40 行 / 采集与查询 ~32 行 / HTTP Method ~12 行 / Misc 折叠 ~35 行 / 测试结果横幅 ~18 行)。其中 3 个折叠区块(Auth/Alerting/Misc)各自重复实现了相同的折叠 header+展开/折叠 UI 模式(按钮 + ChevronUp/Down 图标 + children 容器)。单文件过大导致维护困难。
**已完成 (2026-07-09)**:
将 5 个内容区块和 1 个共享折叠包装器提取为 `prometheus-ds/` 目录下的独立文件(该目录此前已有 types.ts + helpers.ts):
| 新文件 | 行数 | 职责 |
|--------|------|------|
| `CollapsibleSection.tsx` | 42 | 通用可折叠区块包装器(按钮 header + icon + title + ChevronUp/Down 切换 + children 容器),DRY 消除 Auth/Alerting/Misc 三处重复的折叠 UI 模式 |
| `AuthSection.tsx` | 107 | Auth 认证折叠区块(Basic auth 开关+用户名密码 / With Credentials / TLS Client Auth+CA Cert 文本域 / Skip TLS Verify / Forward OAuth Identity 提示),受控组件接收 form + updateForm + open + onToggle |
| `HeadersSection.tsx` | 64 | Custom HTTP Headers 区块(标题栏+Add header 按钮 + header/value 键值对 Input 行增删改 + 空状态提示),受控组件接收 headers + onAdd/onRemove/onUpdate |
| `AlertingSection.tsx` | 58 | Alerting 告警折叠区块(Manage alerts 开关 + Alertmanager 数据源 Select 下拉,过滤自身),受控组件接收 form + updateForm + sources + editingId + open + onToggle |
| `MiscSection.tsx` | 47 | Misc 杂项折叠区块(Disable metrics lookup 开关 + Custom query parameters Input),受控组件接收 form + updateForm + open + onToggle |
| `TestResultBanner.tsx` | 32 | 连接测试结果横幅(成功绿色 CheckCircle / 失败红色 XCircle + target_count 徽章),纯展示组件接收 result(null 时不渲染) |
主文件 `PromDsFormDialog.tsx` 从 **528 行降至 294 行**(-44%),仅保留:8 个 useState 声明(form/headers/showAuth/showTLS/showAlerting/showMisc/testing/testResult)、初始化 useEffect、updateForm/addHeader/removeHeader/updateHeader 回调、handleSave/handleTestConnection 业务逻辑、Dialog 外壳(header + Name&Default + HTTP 设置 + 5 个子组件编排 + 采集与查询 + HTTP Method + Footer 按钮)。
**关键改进**:
- **DRY 折叠模式**: 原 Auth/Alerting/Misc 三个折叠区块各自重复了相同的折叠 header(button + icon + title + ChevronUp/Down + 展开 children 容器)共约 30 行重复 JSX。提取为共享 `CollapsibleSection` 组件后,三个 Section 组件只需传入 icon/title/open/onToggle + children 即可,消除全部重复。
- **受控组件架构**: 5 个子组件全部为受控展示组件(接收 form/headers 状态 + updateForm/onToggle 回调 props),无独立状态。所有状态管理保留在主组件,拆分为纯机械操作(移动 JSX + 添加 props 透传),零逻辑变更。
- **updateForm 泛型化**: 原 `updateForm(field, value)` 函数参数类型为 `keyof PromDsForm` + `PromDsForm[keyof PromDsForm]`(宽松联合类型)。提取到子组件时改为 `<K extends keyof PromDsForm>(field: K, value: PromDsForm[K])` 泛型约束,使子组件调用 `updateForm('basic_auth', v)` 时 v 自动推断为 boolean,类型安全更好。
**验收标准**:
- [x] PromDsFormDialog.tsx 从 528 行降至 **294 行**(-44%)
- [x] 6 个提取的文件全部 < 108 行(最大 AuthSection 107 行,最小 TestResultBanner 32 行)
- [x] `tsc -b` 零错误
- [x] `npm run build` 零错误(✓ built in 17.65s)
- [x] ESLint errors 保持 **0**(prometheus-ds/ 目录零 errors/warnings)
- [x] ESLint warnings 保持 **65**(无新增)
- [x] 全局 ESLint any 保持 **0**
- [x] `go build` 零错误
- [x] 17/17 集成回归测试通过(零回归)
- [x] Docker 部署 healthy,前端页面正常服务
- [x] Prometheus 数据源弹窗功能不变(Name/Default/HTTP/Auth折叠/Custom Headers/Alerting折叠/采集与查询/HTTP Method/Misc折叠/测试连接/保存全部正常)
--- ---
@@ -0,0 +1,58 @@
// Prometheus 数据源表单 — Alerting 告警设置区块
import { AlertTriangle } from 'lucide-react';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select';
import { useTranslation } from 'react-i18next';
import type { PromDS, PromDsForm } from './types';
import CollapsibleSection from './CollapsibleSection';
interface AlertingSectionProps {
form: PromDsForm;
updateForm: <K extends keyof PromDsForm>(field: K, value: PromDsForm[K]) => void;
sources: PromDS[];
editingId: number | null;
open: boolean;
onToggle: () => void;
}
/** Alerting 折叠区块(启用告警 + Alertmanager 数据源选择) */
export default function AlertingSection({
form, updateForm, sources, editingId, open, onToggle,
}: AlertingSectionProps) {
const { t } = useTranslation(['monitor', 'common']);
return (
<CollapsibleSection icon={AlertTriangle} title="Alerting" open={open} onToggle={onToggle}>
<div className="flex items-center gap-3">
<Switch
id="ds-enable-alerting"
checked={form.enable_alerting}
onCheckedChange={v => updateForm('enable_alerting', v)}
/>
<Label htmlFor="ds-enable-alerting" className="text-sm cursor-pointer">
Manage alerts via Alerting UI
</Label>
</div>
<div className="space-y-2">
<Label className="text-sm text-[var(--dg-text-secondary)]">
Alertmanager data source
</Label>
<Select value={form.alertmanager_ds} onValueChange={v => updateForm('alertmanager_ds', v)}>
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-sm">
<SelectValue placeholder="Choose" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none">{t('common:none')}</SelectItem>
{sources.filter(s => s.id !== editingId).map(s => (
<SelectItem key={s.id} value={String(s.id)}>{s.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CollapsibleSection>
);
}
@@ -0,0 +1,107 @@
// Prometheus 数据源表单 — Auth 认证设置区块
import { Shield } from 'lucide-react';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { useTranslation } from 'react-i18next';
import type { PromDsForm } from './types';
import CollapsibleSection from './CollapsibleSection';
interface AuthSectionProps {
form: PromDsForm;
updateForm: <K extends keyof PromDsForm>(field: K, value: PromDsForm[K]) => void;
open: boolean;
onToggle: () => void;
}
/** Auth 认证折叠区块(Basic auth / With Credentials / TLS Client Auth / Skip TLS Verify) */
export default function AuthSection({ form, updateForm, open, onToggle }: AuthSectionProps) {
const { t } = useTranslation(['monitor']);
return (
<CollapsibleSection icon={Shield} title="Auth" open={open} onToggle={onToggle}>
<div className="space-y-3">
<div className="flex items-center gap-3">
<Switch
id="ds-basic-auth"
checked={form.basic_auth}
onCheckedChange={v => updateForm('basic_auth', v)}
/>
<Label htmlFor="ds-basic-auth" className="text-sm cursor-pointer">Basic auth</Label>
</div>
{form.basic_auth && (
<div className="grid grid-cols-2 gap-3 ml-9">
<Input
placeholder={t('monitor:promDs.username')}
value={form.basic_auth_user}
onChange={e => updateForm('basic_auth_user', e.target.value)}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-sm"
/>
<Input
type="password"
placeholder={t('monitor:promDs.password')}
value={form.basic_auth_pass}
onChange={e => updateForm('basic_auth_pass', e.target.value)}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-sm"
/>
</div>
)}
</div>
<div className="flex items-center gap-3">
<Switch
id="ds-with-creds"
checked={form.with_credentials}
onCheckedChange={v => updateForm('with_credentials', v)}
/>
<Label htmlFor="ds-with-creds" className="text-sm cursor-pointer">With Credentials</Label>
</div>
<div className="space-y-3">
<div className="flex items-center gap-3">
<Switch
id="ds-tls-auth"
checked={form.tls_client_auth}
onCheckedChange={v => updateForm('tls_client_auth', v)}
/>
<Label htmlFor="ds-tls-auth" className="text-sm cursor-pointer">TLS Client Auth</Label>
</div>
{form.tls_client_auth && (
<div className="ml-9 space-y-3">
<div className="flex items-center gap-3">
<Switch
id="ds-ca-cert"
checked={form.with_ca_cert}
onCheckedChange={v => updateForm('with_ca_cert', v)}
/>
<Label htmlFor="ds-ca-cert" className="text-sm cursor-pointer">With CA Cert</Label>
</div>
{form.with_ca_cert && (
<textarea
placeholder={t('monitor:promDs.caCertPlaceholder')}
value={form.ca_cert}
onChange={e => updateForm('ca_cert', e.target.value)}
rows={4}
className="w-full bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md p-3 text-xs font-mono text-[var(--dg-text-primary)] resize-y"
/>
)}
</div>
)}
</div>
<div className="flex items-center gap-3">
<Switch
id="ds-skip-tls"
checked={form.skip_tls_verify}
onCheckedChange={v => updateForm('skip_tls_verify', v)}
/>
<Label htmlFor="ds-skip-tls" className="text-sm cursor-pointer">Skip TLS Verify</Label>
</div>
<div className="text-xs text-[var(--dg-text-secondary)] italic">
Forward OAuth Identity
</div>
</CollapsibleSection>
);
}
@@ -0,0 +1,42 @@
// Prometheus 数据源表单 — 可折叠区块通用包装器
import type { ReactNode } from 'react';
import type { LucideIcon } from 'lucide-react';
import { ChevronDown, ChevronUp } from 'lucide-react';
interface CollapsibleSectionProps {
icon: LucideIcon;
title: string;
open: boolean;
onToggle: () => void;
children: ReactNode;
}
/** 可折叠区块(Grafana 风格 — 按钮 header + 展开内容) */
export default function CollapsibleSection({
icon: Icon,
title,
open,
onToggle,
children,
}: CollapsibleSectionProps) {
return (
<div className="border border-[var(--dg-border)]/50 rounded-lg overflow-hidden">
<button
type="button"
className="w-full flex items-center justify-between px-4 py-3 bg-[var(--dg-bg-tertiary)]/30 hover:bg-[var(--dg-bg-tertiary)]/60 transition-colors"
onClick={onToggle}
>
<span className="text-sm font-medium flex items-center gap-2">
<Icon className="w-4 h-4" /> {title}
</span>
{open ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
{open && (
<div className="p-4 space-y-4 border-t border-[var(--dg-border)]/50">
{children}
</div>
)}
</div>
);
}
@@ -0,0 +1,64 @@
// Prometheus 数据源表单 — Custom HTTP Headers 区块
import { Plus, Wrench, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useTranslation } from 'react-i18next';
import type { HeaderEntry } from './types';
interface HeadersSectionProps {
headers: HeaderEntry[];
onAdd: () => void;
onRemove: (idx: number) => void;
onUpdate: (idx: number, field: 'header' | 'value', val: string) => void;
}
/** Custom HTTP Headers 区块(键值对增删改) */
export default function HeadersSection({
headers, onAdd, onRemove, onUpdate,
}: HeadersSectionProps) {
const { t } = useTranslation(['monitor']);
return (
<div className="space-y-3 bg-[var(--dg-bg-tertiary)]/30 rounded-lg p-4 border border-[var(--dg-border)]/50">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium flex items-center gap-2 text-[var(--dg-text-primary)]">
<Wrench className="w-4 h-4" /> Custom HTTP Headers
</h4>
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={onAdd}>
<Plus className="w-3 h-3 mr-1" /> Add header
</Button>
</div>
{headers.length === 0 ? (
<p className="text-xs text-[var(--dg-text-secondary)] italic">
{t('monitor:promDs.noHeaders')}
</p>
) : (
<div className="space-y-2">
{headers.map((h, idx) => (
<div key={idx} className="flex items-center gap-2">
<Input
placeholder="Header"
value={h.header}
onChange={e => onUpdate(idx, 'header', e.target.value)}
className="flex-1 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-sm h-8"
/>
<Input
placeholder="Value"
value={h.value}
onChange={e => onUpdate(idx, 'value', e.target.value)}
className="flex-1 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-sm h-8"
/>
<Button
variant="ghost" size="sm" className="h-8 w-8 p-0 dg-text-red hover:dg-text-red"
onClick={() => onRemove(idx)}
>
<X className="w-3.5 h-3.5" />
</Button>
</div>
))}
</div>
)}
</div>
);
}
@@ -0,0 +1,47 @@
// Prometheus 数据源表单 — Misc 杂项设置区块
import { Settings } from 'lucide-react';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { useTranslation } from 'react-i18next';
import type { PromDsForm } from './types';
import CollapsibleSection from './CollapsibleSection';
interface MiscSectionProps {
form: PromDsForm;
updateForm: <K extends keyof PromDsForm>(field: K, value: PromDsForm[K]) => void;
open: boolean;
onToggle: () => void;
}
/** Misc 折叠区块(禁用 metrics 查找 + 自定义查询参数) */
export default function MiscSection({ form, updateForm, open, onToggle }: MiscSectionProps) {
const { t } = useTranslation(['monitor']);
return (
<CollapsibleSection icon={Settings} title="Misc" open={open} onToggle={onToggle}>
<div className="flex items-center gap-3">
<Switch
id="ds-disable-lookup"
checked={form.disable_metrics_lookup}
onCheckedChange={v => updateForm('disable_metrics_lookup', v)}
/>
<Label htmlFor="ds-disable-lookup" className="text-sm cursor-pointer">
Disable metrics lookup
</Label>
</div>
<div className="space-y-2">
<Label className="text-sm text-[var(--dg-text-secondary)]">
Custom query parameters
</Label>
<Input
placeholder={t('monitor:promDs.queryParamsPlaceholder')}
value={form.custom_query_params}
onChange={e => updateForm('custom_query_params', e.target.value)}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-sm font-mono"
/>
</div>
</CollapsibleSection>
);
}
@@ -2,27 +2,10 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import { Database, Globe, Clock, RefreshCw, Plug } from 'lucide-react';
Database,
Plus,
RefreshCw,
CheckCircle,
XCircle,
Shield,
Globe,
Clock,
Wrench,
Settings,
X,
ChevronDown,
ChevronUp,
Plug,
AlertTriangle,
} from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { import {
@@ -37,6 +20,11 @@ import { usePromDatasourceMutations } from '@/hooks/api';
import type { PromDS, HeaderEntry, PromDsForm } from './types'; import type { PromDS, HeaderEntry, PromDsForm } from './types';
import { defaultForm } from './types'; import { defaultForm } from './types';
import { dsToForm, parseHeaders, buildPayload, testPromConnection, type TestConnectionResult } from './helpers'; import { dsToForm, parseHeaders, buildPayload, testPromConnection, type TestConnectionResult } from './helpers';
import AuthSection from './AuthSection';
import HeadersSection from './HeadersSection';
import AlertingSection from './AlertingSection';
import MiscSection from './MiscSection';
import TestResultBanner from './TestResultBanner';
interface PromDsFormDialogProps { interface PromDsFormDialogProps {
open: boolean; open: boolean;
@@ -81,7 +69,7 @@ export default function PromDsFormDialog({
setShowMisc(false); setShowMisc(false);
}, [open, editingDs]); }, [open, editingDs]);
const updateForm = (field: keyof PromDsForm, value: PromDsForm[keyof PromDsForm]) => { const updateForm = <K extends keyof PromDsForm>(field: K, value: PromDsForm[K]) => {
setForm(prev => ({ ...prev, [field]: value })); setForm(prev => ({ ...prev, [field]: value }));
}; };
@@ -215,185 +203,13 @@ export default function PromDsFormDialog({
</div> </div>
{/* === Auth 折叠 === */} {/* === Auth 折叠 === */}
<div className="border border-[var(--dg-border)]/50 rounded-lg overflow-hidden"> <AuthSection form={form} updateForm={updateForm} open={showAuth} onToggle={() => setShowAuth(!showAuth)} />
<button
type="button"
className="w-full flex items-center justify-between px-4 py-3 bg-[var(--dg-bg-tertiary)]/30 hover:bg-[var(--dg-bg-tertiary)]/60 transition-colors"
onClick={() => setShowAuth(!showAuth)}
>
<span className="text-sm font-medium flex items-center gap-2">
<Shield className="w-4 h-4" /> Auth
</span>
{showAuth ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
{showAuth && (
<div className="p-4 space-y-4 border-t border-[var(--dg-border)]/50">
<div className="space-y-3">
<div className="flex items-center gap-3">
<Switch
id="ds-basic-auth"
checked={form.basic_auth}
onCheckedChange={v => updateForm('basic_auth', v)}
/>
<Label htmlFor="ds-basic-auth" className="text-sm cursor-pointer">Basic auth</Label>
</div>
{form.basic_auth && (
<div className="grid grid-cols-2 gap-3 ml-9">
<Input
placeholder={t('monitor:promDs.username')}
value={form.basic_auth_user}
onChange={e => updateForm('basic_auth_user', e.target.value)}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-sm"
/>
<Input
type="password"
placeholder={t('monitor:promDs.password')}
value={form.basic_auth_pass}
onChange={e => updateForm('basic_auth_pass', e.target.value)}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-sm"
/>
</div>
)}
</div>
<div className="flex items-center gap-3">
<Switch
id="ds-with-creds"
checked={form.with_credentials}
onCheckedChange={v => updateForm('with_credentials', v)}
/>
<Label htmlFor="ds-with-creds" className="text-sm cursor-pointer">With Credentials</Label>
</div>
<div className="space-y-3">
<div className="flex items-center gap-3">
<Switch
id="ds-tls-auth"
checked={form.tls_client_auth}
onCheckedChange={v => updateForm('tls_client_auth', v)}
/>
<Label htmlFor="ds-tls-auth" className="text-sm cursor-pointer">TLS Client Auth</Label>
</div>
{form.tls_client_auth && (
<div className="ml-9 space-y-3">
<div className="flex items-center gap-3">
<Switch
id="ds-ca-cert"
checked={form.with_ca_cert}
onCheckedChange={v => updateForm('with_ca_cert', v)}
/>
<Label htmlFor="ds-ca-cert" className="text-sm cursor-pointer">With CA Cert</Label>
</div>
{form.with_ca_cert && (
<textarea
placeholder={t('monitor:promDs.caCertPlaceholder')}
value={form.ca_cert}
onChange={e => updateForm('ca_cert', e.target.value)}
rows={4}
className="w-full bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md p-3 text-xs font-mono text-[var(--dg-text-primary)] resize-y"
/>
)}
</div>
)}
</div>
<div className="flex items-center gap-3">
<Switch
id="ds-skip-tls"
checked={form.skip_tls_verify}
onCheckedChange={v => updateForm('skip_tls_verify', v)}
/>
<Label htmlFor="ds-skip-tls" className="text-sm cursor-pointer">Skip TLS Verify</Label>
</div>
<div className="text-xs text-[var(--dg-text-secondary)] italic">
Forward OAuth Identity
</div>
</div>
)}
</div>
{/* === Custom HTTP Headers === */} {/* === Custom HTTP Headers === */}
<div className="space-y-3 bg-[var(--dg-bg-tertiary)]/30 rounded-lg p-4 border border-[var(--dg-border)]/50"> <HeadersSection headers={headers} onAdd={addHeader} onRemove={removeHeader} onUpdate={updateHeader} />
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium flex items-center gap-2 text-[var(--dg-text-primary)]">
<Wrench className="w-4 h-4" /> Custom HTTP Headers
</h4>
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={addHeader}>
<Plus className="w-3 h-3 mr-1" /> Add header
</Button>
</div>
{headers.length === 0 ? (
<p className="text-xs text-[var(--dg-text-secondary)] italic">{t('monitor:promDs.noHeaders')}</p>
) : (
<div className="space-y-2">
{headers.map((h, idx) => (
<div key={idx} className="flex items-center gap-2">
<Input
placeholder="Header"
value={h.header}
onChange={e => updateHeader(idx, 'header', e.target.value)}
className="flex-1 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-sm h-8"
/>
<Input
placeholder="Value"
value={h.value}
onChange={e => updateHeader(idx, 'value', e.target.value)}
className="flex-1 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-sm h-8"
/>
<Button
variant="ghost" size="sm" className="h-8 w-8 p-0 dg-text-red hover:dg-text-red"
onClick={() => removeHeader(idx)}
>
<X className="w-3.5 h-3.5" />
</Button>
</div>
))}
</div>
)}
</div>
{/* === Alerting 折叠 === */} {/* === Alerting 折叠 === */}
<div className="border border-[var(--dg-border)]/50 rounded-lg overflow-hidden"> <AlertingSection form={form} updateForm={updateForm} sources={sources} editingId={editingId} open={showAlerting} onToggle={() => setShowAlerting(!showAlerting)} />
<button
type="button"
className="w-full flex items-center justify-between px-4 py-3 bg-[var(--dg-bg-tertiary)]/30 hover:bg-[var(--dg-bg-tertiary)]/60 transition-colors"
onClick={() => setShowAlerting(!showAlerting)}
>
<span className="text-sm font-medium flex items-center gap-2">
<AlertTriangle className="w-4 h-4" /> Alerting
</span>
{showAlerting ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
{showAlerting && (
<div className="p-4 space-y-4 border-t border-[var(--dg-border)]/50">
<div className="flex items-center gap-3">
<Switch
id="ds-enable-alerting"
checked={form.enable_alerting}
onCheckedChange={v => updateForm('enable_alerting', v)}
/>
<Label htmlFor="ds-enable-alerting" className="text-sm cursor-pointer">
Manage alerts via Alerting UI
</Label>
</div>
<div className="space-y-2">
<Label className="text-sm text-[var(--dg-text-secondary)]">Alertmanager data source</Label>
<Select value={form.alertmanager_ds} onValueChange={v => updateForm('alertmanager_ds', v)}>
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-sm">
<SelectValue placeholder="Choose" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none">{t('common:none')}</SelectItem>
{sources.filter(s => s.id !== editingId).map(s => (
<SelectItem key={s.id} value={String(s.id)}>{s.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
)}
</div>
{/* === 采集与查询 === */} {/* === 采集与查询 === */}
<div className="grid grid-cols-2 gap-4 bg-[var(--dg-bg-tertiary)]/30 rounded-lg p-4 border border-[var(--dg-border)]/50"> <div className="grid grid-cols-2 gap-4 bg-[var(--dg-bg-tertiary)]/30 rounded-lg p-4 border border-[var(--dg-border)]/50">
@@ -444,61 +260,10 @@ export default function PromDsFormDialog({
</div> </div>
{/* === Misc 折叠 === */} {/* === Misc 折叠 === */}
<div className="border border-[var(--dg-border)]/50 rounded-lg overflow-hidden"> <MiscSection form={form} updateForm={updateForm} open={showMisc} onToggle={() => setShowMisc(!showMisc)} />
<button
type="button"
className="w-full flex items-center justify-between px-4 py-3 bg-[var(--dg-bg-tertiary)]/30 hover:bg-[var(--dg-bg-tertiary)]/60 transition-colors"
onClick={() => setShowMisc(!showMisc)}
>
<span className="text-sm font-medium flex items-center gap-2">
<Settings className="w-4 h-4" /> Misc
</span>
{showMisc ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
{showMisc && (
<div className="p-4 space-y-4 border-t border-[var(--dg-border)]/50">
<div className="flex items-center gap-3">
<Switch
id="ds-disable-lookup"
checked={form.disable_metrics_lookup}
onCheckedChange={v => updateForm('disable_metrics_lookup', v)}
/>
<Label htmlFor="ds-disable-lookup" className="text-sm cursor-pointer">
Disable metrics lookup
</Label>
</div>
<div className="space-y-2">
<Label className="text-sm text-[var(--dg-text-secondary)]">Custom query parameters</Label>
<Input
placeholder={t('monitor:promDs.queryParamsPlaceholder')}
value={form.custom_query_params}
onChange={e => updateForm('custom_query_params', e.target.value)}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-sm font-mono"
/>
</div>
</div>
)}
</div>
{/* 连接测试结果 */} {/* 连接测试结果 */}
{testResult && ( <TestResultBanner result={testResult} />
<div className={`rounded-lg p-3 flex items-center gap-2 text-sm ${
testResult.success
? 'bg-emerald-500/10 border border-emerald-500/30 dg-text-green'
: 'bg-red-500/10 border border-red-500/30 dg-text-red'
}`}>
{testResult.success
? <CheckCircle className="w-4 h-4 shrink-0" />
: <XCircle className="w-4 h-4 shrink-0" />
}
<span>{testResult.message}</span>
{testResult.target_count !== undefined && (
<Badge variant="secondary" className="ml-auto text-xs">
{testResult.target_count} targets
</Badge>
)}
</div>
)}
</div> </div>
<DialogFooter className="flex items-center gap-2 pt-2 border-t border-[var(--dg-border)]/50"> <DialogFooter className="flex items-center gap-2 pt-2 border-t border-[var(--dg-border)]/50">
@@ -526,4 +291,4 @@ export default function PromDsFormDialog({
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
} }
@@ -0,0 +1,32 @@
// Prometheus 数据源表单 — 连接测试结果横幅
import { CheckCircle, XCircle } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import type { TestConnectionResult } from './helpers';
interface TestResultBannerProps {
result: TestConnectionResult | null;
}
/** 连接测试结果横幅(成功绿色 / 失败红色 + target 计数徽章) */
export default function TestResultBanner({ result }: TestResultBannerProps) {
if (!result) return null;
return (
<div className={`rounded-lg p-3 flex items-center gap-2 text-sm ${
result.success
? 'bg-emerald-500/10 border border-emerald-500/30 dg-text-green'
: 'bg-red-500/10 border border-red-500/30 dg-text-red'
}`}>
{result.success
? <CheckCircle className="w-4 h-4 shrink-0" />
: <XCircle className="w-4 h-4 shrink-0" />
}
<span>{result.message}</span>
{result.target_count !== undefined && (
<Badge variant="secondary" className="ml-auto text-xs">
{result.target_count} targets
</Badge>
)}
</div>
);
}