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:
1 parent
45f7342d1e
commit
195b6945b4
9 files changed
+404
-249
No files matched your search
@@ -129,7 +129,7 @@
|
||||
| ~~`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) |
|
||||
| ~~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 🟡 中优先级
|
||||
|
||||
|
||||
+41
-1
@@ -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** |
|
||||
| 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** |
|
||||
| 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 { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Database,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Shield,
|
||||
Globe,
|
||||
Clock,
|
||||
Wrench,
|
||||
Settings,
|
||||
X,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Plug,
|
||||
AlertTriangle,
|
||||
} from 'lucide-react';
|
||||
import { Database, Globe, Clock, RefreshCw, Plug } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import {
|
||||
@@ -37,6 +20,11 @@ import { usePromDatasourceMutations } from '@/hooks/api';
|
||||
import type { PromDS, HeaderEntry, PromDsForm } from './types';
|
||||
import { defaultForm } from './types';
|
||||
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 {
|
||||
open: boolean;
|
||||
@@ -81,7 +69,7 @@ export default function PromDsFormDialog({
|
||||
setShowMisc(false);
|
||||
}, [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 }));
|
||||
};
|
||||
|
||||
@@ -215,185 +203,13 @@ export default function PromDsFormDialog({
|
||||
</div>
|
||||
|
||||
{/* === Auth 折叠 === */}
|
||||
<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={() => 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>
|
||||
<AuthSection form={form} updateForm={updateForm} open={showAuth} onToggle={() => setShowAuth(!showAuth)} />
|
||||
|
||||
{/* === Custom HTTP Headers === */}
|
||||
<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={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>
|
||||
<HeadersSection headers={headers} onAdd={addHeader} onRemove={removeHeader} onUpdate={updateHeader} />
|
||||
|
||||
{/* === Alerting 折叠 === */}
|
||||
<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={() => 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>
|
||||
<AlertingSection form={form} updateForm={updateForm} sources={sources} editingId={editingId} open={showAlerting} onToggle={() => setShowAlerting(!showAlerting)} />
|
||||
|
||||
{/* === 采集与查询 === */}
|
||||
<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>
|
||||
|
||||
{/* === Misc 折叠 === */}
|
||||
<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={() => 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>
|
||||
<MiscSection form={form} updateForm={updateForm} open={showMisc} onToggle={() => setShowMisc(!showMisc)} />
|
||||
|
||||
{/* 连接测试结果 */}
|
||||
{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>
|
||||
)}
|
||||
<TestResultBanner result={testResult} />
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex items-center gap-2 pt-2 border-t border-[var(--dg-border)]/50">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user