refactor(quality): Q8 前端巨型组件拆分 — SystemSettingsTab 754L→173L (-77%), 提取通用/安全/LDAP 3 个独立子 Tab 组件

- GeneralSettingsTab.tsx (154L): 通用设置 Tab(审计策略 + 系统信息)
- SecuritySettingsTab.tsx (309L): 安全设置 Tab(安全策略 + 会话设置 9 项开关)
- LdapSettingsTab.tsx (243L): LDAP 设置 Tab(配置表单 + 管理区转发)
- 主文件仅保留 Props 接口 + Tab 路由 + 分发逻辑(173L)
- 每个子 Tab 用 Pick<SystemSettingsTabProps,...> 精确声明 props 子集
- tsc -b=0, npm build✓(17.71s), ESLint errors=0/warnings=59(无新增)
- 17/17 集成回归测试通过, Docker 部署 healthy
This commit is contained in:
attacker committed 2026-07-07 01:05:07 +08:00
1 parent 2cc5ad719b
commit 8a4dfb0db6
7 files changed
+797 -589

No files matched your search

@@ -1,37 +1,11 @@
import { useState, useEffect } from "react";
import { useSearchParams } from "react-router-dom";
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Lock,
History,
Key,
Settings,
Network,
RefreshCw,
Loader2,
} from "lucide-react";
import { PageTabs } from "@/components/layout/PageTabs";
import { LdapManagementSection } from "./LdapManagementSection";
import type { JsonData } from "@/types/common";
import { useTranslation } from "react-i18next";
import i18n from "@/i18n";
import { useUserStore } from "@/store";
import { GeneralSettingsTab } from "./system-settings/GeneralSettingsTab";
import { SecuritySettingsTab } from "./system-settings/SecuritySettingsTab";
import { LdapSettingsTab } from "./system-settings/LdapSettingsTab";
// 系统设置子 Tab:通用 / 安全 / LDAP // Logan.Li
const SETTINGS_TABS = [
@@ -51,7 +25,7 @@ export interface LdapFormData {
search_filter: string;
}
interface SystemSettingsTabProps {
export interface SystemSettingsTabProps {
// 审计策略 // Logan.Li
auditRetentionDays: string;
setAuditRetentionDays: (v: string) => void;
@@ -157,44 +131,6 @@ interface SystemSettingsTabProps {
}
export function SystemSettingsTab(props: SystemSettingsTabProps) {
const { t } = useTranslation();
const currentUsername = useUserStore(
(state) => state.user?.username || "admin",
);
const {
auditRetentionDays,
setAuditRetentionDays,
auditRecordAll,
setAuditRecordAll,
sessionWatermarkEnabled,
setSessionWatermarkEnabled,
sessionRecordEnabled,
setSessionRecordEnabled,
clipboardEnabled,
setClipboardEnabled,
fileUploadEnabled,
setFileUploadEnabled,
fileDownloadEnabled,
setFileDownloadEnabled,
commandPolicyEnabled,
setCommandPolicyEnabled,
approvalRequired,
setApprovalRequired,
auditLoading,
auditApplying,
handleAuditApply,
auditLastCleanupText,
auditRetentionOptions,
ldapConfig,
ldapForm,
setLdapForm,
ldapSaving,
ldapTesting,
ldapTestResult,
handleLdapSave,
handleLdapTest,
} = props;
const [searchParams, setSearchParams] = useSearchParams();
const tabFromUrl = searchParams.get("tab") as SettingsSubTab | null;
const [subTab, setSubTab] = useState<SettingsSubTab>(
@@ -229,526 +165,9 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
onChange={changeSubTab}
/>
{subTab === "general" && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
<Card className="dg-card">
<CardHeader>
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
<History className="h-5 w-5 dg-text-green" />
{t("system.audit")}
</CardTitle>
<CardDescription className="dg-text-muted">
{t("system.auditSettingsDesc")}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.recordAllOps")}
</p>
<p className="text-xs dg-text-muted">
{t("system.recordAllOpsDesc")}
</p>
</div>
<Switch
checked={auditRecordAll}
onCheckedChange={setAuditRecordAll}
disabled={auditLoading || auditApplying}
className="data-[state=checked]:bg-blue-600"
/>
</div>
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.logRetention")}
</p>
<p className="text-xs dg-text-muted">
{t("system.logRetentionHint")}
</p>
{auditLastCleanupText && (
<p className="text-xs dg-text-muted mt-1">
{auditLastCleanupText}
</p>
)}
</div>
<Select
value={auditRetentionDays}
onValueChange={setAuditRetentionDays}
disabled={auditLoading || auditApplying}
>
<SelectTrigger className="w-28 h-8 dg-input shrink-0">
<SelectValue />
</SelectTrigger>
<SelectContent className="dg-input">
{auditRetentionOptions.map((d) => (
<SelectItem key={d} value={String(d)}>
{d}天
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex justify-end pt-1">
<Button
size="sm"
className="dg-btn-primary"
onClick={handleAuditApply}
disabled={auditLoading || auditApplying}
>
{auditApplying ? (
<>
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
{t("system.applyingDot")}
</>
) : (
t("common.apply")
)}
</Button>
</div>
</CardContent>
</Card>
<Card className="dg-card">
<CardHeader>
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
<Settings className="h-5 w-5 dg-text-purple" />
{t("system.systemInfo")}
</CardTitle>
<CardDescription className="dg-text-muted">
{t("system.systemInfoDesc")}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm dg-text-secondary">
{t("system.systemVersion")}
</span>
<span className="text-sm text-[var(--dg-text-primary)]">
v2.5.1
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm dg-text-secondary">
{t("system.uptime")}
</span>
<span className="text-sm text-[var(--dg-text-primary)]">
{t("system.uptimeValue")}
</span>
</div>
</CardContent>
</Card>
</div>
)}
{subTab === "security" && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
<Card className="dg-card">
<CardHeader>
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
<Lock className="h-5 w-5 dg-text-blue" />
{t("system.security")}
</CardTitle>
<CardDescription className="dg-text-muted">
{t("system.securityDesc")}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.forcePasswordComplexity")}
</p>
<p className="text-xs dg-text-muted">
{t("system.forcePasswordComplexityDesc")}
</p>
</div>
<Switch disabled className="data-[state=checked]:bg-blue-600" />
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.loginLockout")}
</p>
<p className="text-xs dg-text-muted">
{t("system.loginLockoutDesc")}
</p>
</div>
<Switch
disabled
defaultChecked
className="data-[state=checked]:bg-blue-600"
/>
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.twoFactor")}
</p>
<p className="text-xs dg-text-muted">
{t("system.twoFactorDesc")}
</p>
</div>
<Switch disabled className="data-[state=checked]:bg-blue-600" />
</div>
</CardContent>
</Card>
<Card className="dg-card">
<CardHeader>
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
<Key className="h-5 w-5 dg-text-yellow" />
{t("system.sessionSettings")}
</CardTitle>
<CardDescription className="dg-text-muted">
{t("system.sessionSettingsDesc")}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.sessionWatermark")}
</p>
<p className="text-xs dg-text-muted">
{t("system.sessionWatermarkDesc", {
user: currentUsername,
})}
</p>
</div>
<Switch
checked={sessionWatermarkEnabled}
onCheckedChange={setSessionWatermarkEnabled}
className="data-[state=checked]:bg-blue-600"
/>
</div>
<div className="rounded border border-[var(--dg-border)] bg-[#101418] px-3 py-4 overflow-hidden">
<div className="relative h-24 rounded bg-black/70">
<div
className="absolute inset-0 opacity-80"
style={{
backgroundImage: `repeating-linear-gradient(-24deg, transparent 0 42px, rgba(255,255,255,0.12) 43px 44px, transparent 45px 120px)`,
}}
/>
<div className="absolute inset-0 grid grid-cols-2 place-items-center -rotate-[18deg] text-lg font-semibold tracking-normal text-white/30">
<span>{currentUsername}</span>
<span>{currentUsername}</span>
<span>{currentUsername}</span>
<span>{currentUsername}</span>
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-3">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.sessionRecordEnabled")}
</p>
<p className="text-xs dg-text-muted">
{t("system.sessionRecordEnabledDesc")}
</p>
</div>
<Switch
checked={sessionRecordEnabled}
onCheckedChange={setSessionRecordEnabled}
className="data-[state=checked]:bg-blue-600"
/>
</div>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.clipboardEnabled")}
</p>
<p className="text-xs dg-text-muted">
{t("system.clipboardEnabledDesc")}
</p>
</div>
<Switch
checked={clipboardEnabled}
onCheckedChange={setClipboardEnabled}
className="data-[state=checked]:bg-blue-600"
/>
</div>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.fileUploadEnabled")}
</p>
<p className="text-xs dg-text-muted">
{t("system.fileUploadEnabledDesc")}
</p>
</div>
<Switch
checked={fileUploadEnabled}
onCheckedChange={setFileUploadEnabled}
className="data-[state=checked]:bg-blue-600"
/>
</div>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.fileDownloadEnabled")}
</p>
<p className="text-xs dg-text-muted">
{t("system.fileDownloadEnabledDesc")}
</p>
</div>
<Switch
checked={fileDownloadEnabled}
onCheckedChange={setFileDownloadEnabled}
className="data-[state=checked]:bg-blue-600"
/>
</div>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.commandPolicyEnabled")}
</p>
<p className="text-xs dg-text-muted">
{t("system.commandPolicyEnabledDesc")}
</p>
</div>
<Switch
checked={commandPolicyEnabled}
onCheckedChange={setCommandPolicyEnabled}
className="data-[state=checked]:bg-blue-600"
/>
</div>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.approvalRequired")}
</p>
<p className="text-xs dg-text-muted">
{t("system.approvalRequiredDesc")}
</p>
</div>
<Switch
checked={approvalRequired}
onCheckedChange={setApprovalRequired}
className="data-[state=checked]:bg-blue-600"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.sessionTimeoutTime")}
</p>
<p className="text-xs dg-text-muted">
{t("system.sessionTimeoutDesc")}
</p>
</div>
<Select defaultValue="30" disabled>
<SelectTrigger className="w-32 h-8 dg-input">
<SelectValue />
</SelectTrigger>
<SelectContent className="dg-input">
<SelectItem value="15">
{t("monitor.traceMgr.min15")}
</SelectItem>
<SelectItem value="30">30分钟</SelectItem>
<SelectItem value="60">
{t("monitor.traceMgr.hour1")}
</SelectItem>
<SelectItem value="120">2小时</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.sso")}
</p>
<p className="text-xs dg-text-muted">
{t("system.singleSignOnDesc")}
</p>
</div>
<Switch disabled className="data-[state=checked]:bg-blue-600" />
</div>
<div className="flex justify-end pt-1">
<Button
size="sm"
className="dg-btn-primary"
onClick={handleAuditApply}
disabled={auditLoading || auditApplying}
>
{auditApplying ? (
<>
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
{t("system.applyingDot")}
</>
) : (
t("common.apply")
)}
</Button>
</div>
</CardContent>
</Card>
</div>
)}
{subTab === "ldap" && (
<>
<Card className="dg-card pt-2">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
<Network className="h-5 w-5 dg-text-cyan" />
LDAP / Active Directory
</CardTitle>
<CardDescription className="dg-text-muted mt-1">
{t("system.ldapConfigHint")}
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
<div className="space-y-1.5">
<label className="text-xs dg-text-muted">
{t("system.ldapServerAddr")}
</label>
<Input
value={ldapForm.ldap_host}
onChange={(e) =>
setLdapForm({ ...ldapForm, ldap_host: e.target.value })
}
className="dg-input"
placeholder="ldap.example.com"
/>
</div>
<div className="space-y-1.5">
<label className="text-xs dg-text-muted">
{t("system.ldapPort")}
</label>
<Input
type="number"
value={ldapForm.ldap_port}
onChange={(e) =>
setLdapForm({
...ldapForm,
ldap_port: Number(e.target.value),
})
}
className="dg-input"
placeholder="389"
/>
</div>
<div className="space-y-1.5 col-span-2">
<label className="text-xs dg-text-muted">
{t("system.baseDnHint")}
</label>
<Input
value={ldapForm.base_dn}
onChange={(e) =>
setLdapForm({ ...ldapForm, base_dn: e.target.value })
}
className="dg-input"
placeholder="ou=users,dc=example,dc=com"
/>
</div>
<div className="space-y-1.5">
<label className="text-xs dg-text-muted">
{t("system.adminBindDn")}
</label>
<Input
value={ldapForm.admin_dn}
onChange={(e) =>
setLdapForm({ ...ldapForm, admin_dn: e.target.value })
}
className="dg-input"
placeholder="cn=admin,dc=example,dc=com"
/>
</div>
<div className="space-y-1.5">
<label className="text-xs dg-text-muted">
管理员密码 {ldapConfig && t("system.leaveBlankNoModify")}
</label>
<Input
type="password"
value={ldapForm.admin_password}
onChange={(e) =>
setLdapForm({
...ldapForm,
admin_password: e.target.value,
})
}
className="dg-input"
placeholder="******"
/>
</div>
<div className="space-y-1.5 col-span-2">
<label className="text-xs dg-text-muted">
{t("system.searchFilterHint")}
</label>
<Input
value={ldapForm.search_filter}
onChange={(e) =>
setLdapForm({
...ldapForm,
search_filter: e.target.value,
})
}
className="dg-input"
placeholder="(uid=%s)"
/>
</div>
<div className="flex items-center gap-2 col-span-2">
<Switch
checked={ldapForm.use_ldaps}
onCheckedChange={(v) =>
setLdapForm({ ...ldapForm, use_ldaps: v })
}
className="data-[state=checked]:bg-blue-600"
/>
<span className="text-sm text-[var(--dg-text-secondary)]">
{t("system.enableLdaps")}
</span>
</div>
</div>
{/* 测试/保存按钮 */}
<div className="flex items-center gap-3 mt-4 pt-4 border-t border-[var(--dg-border)]">
{ldapTestResult && (
<span
className={`text-xs ${ldapTestResult.includes("成功") ? "dg-text-green" : "dg-text-red"}`}
>
{ldapTestResult}
</span>
)}
<div className="flex items-center gap-2 ml-auto">
<Button
variant="outline"
size="sm"
className="dg-btn-secondary"
onClick={handleLdapTest}
disabled={ldapTesting}
>
{ldapTesting ? (
<Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
) : (
<RefreshCw className="h-3.5 w-3.5 mr-1" />
)}
测试连接
</Button>
<Button
size="sm"
className="dg-btn-primary"
onClick={handleLdapSave}
disabled={ldapSaving}
>
{ldapSaving ? t("k8s.topology.saving") : t("aiops.llmSave")}
</Button>
</div>
</div>
</CardContent>
</Card>
{ldapConfig && <LdapManagementSection {...props} />}
</>
)}
{subTab === "general" && <GeneralSettingsTab {...props} />}
{subTab === "security" && <SecuritySettingsTab {...props} />}
{subTab === "ldap" && <LdapSettingsTab {...props} />}
</div>
);
}
@@ -0,0 +1,154 @@
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { History, Settings, Loader2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { SystemSettingsTabProps } from "../SystemSettingsTab";
// 通用设置 Tab:审计策略 + 系统信息 // Logan.Li
type GeneralSettingsTabProps = Pick<
SystemSettingsTabProps,
| "auditRetentionDays"
| "setAuditRetentionDays"
| "auditRecordAll"
| "setAuditRecordAll"
| "auditLoading"
| "auditApplying"
| "handleAuditApply"
| "auditLastCleanupText"
| "auditRetentionOptions"
>;
export function GeneralSettingsTab(props: GeneralSettingsTabProps) {
const { t } = useTranslation();
const {
auditRetentionDays,
setAuditRetentionDays,
auditRecordAll,
setAuditRecordAll,
auditLoading,
auditApplying,
handleAuditApply,
auditLastCleanupText,
auditRetentionOptions,
} = props;
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
<Card className="dg-card">
<CardHeader>
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
<History className="h-5 w-5 dg-text-green" />
{t("system.audit")}
</CardTitle>
<CardDescription className="dg-text-muted">
{t("system.auditSettingsDesc")}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.recordAllOps")}
</p>
<p className="text-xs dg-text-muted">
{t("system.recordAllOpsDesc")}
</p>
</div>
<Switch
checked={auditRecordAll}
onCheckedChange={setAuditRecordAll}
disabled={auditLoading || auditApplying}
className="data-[state=checked]:bg-blue-600"
/>
</div>
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.logRetention")}
</p>
<p className="text-xs dg-text-muted">
{t("system.logRetentionHint")}
</p>
{auditLastCleanupText && (
<p className="text-xs dg-text-muted mt-1">
{auditLastCleanupText}
</p>
)}
</div>
<Select
value={auditRetentionDays}
onValueChange={setAuditRetentionDays}
disabled={auditLoading || auditApplying}
>
<SelectTrigger className="w-28 h-8 dg-input shrink-0">
<SelectValue />
</SelectTrigger>
<SelectContent className="dg-input">
{auditRetentionOptions.map((d) => (
<SelectItem key={d} value={String(d)}>
{d}天
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex justify-end pt-1">
<Button
size="sm"
className="dg-btn-primary"
onClick={handleAuditApply}
disabled={auditLoading || auditApplying}
>
{auditApplying ? (
<>
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
{t("system.applyingDot")}
</>
) : (
t("common.apply")
)}
</Button>
</div>
</CardContent>
</Card>
<Card className="dg-card">
<CardHeader>
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
<Settings className="h-5 w-5 dg-text-purple" />
{t("system.systemInfo")}
</CardTitle>
<CardDescription className="dg-text-muted">
{t("system.systemInfoDesc")}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm dg-text-secondary">
{t("system.systemVersion")}
</span>
<span className="text-sm text-[var(--dg-text-primary)]">
v2.5.1
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm dg-text-secondary">
{t("system.uptime")}
</span>
<span className="text-sm text-[var(--dg-text-primary)]">
{t("system.uptimeValue")}
</span>
</div>
</CardContent>
</Card>
</div>
);
}
@@ -0,0 +1,243 @@
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Network, RefreshCw, Loader2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { LdapManagementSection } from "../LdapManagementSection";
import type { SystemSettingsTabProps } from "../SystemSettingsTab";
// LDAP 设置 Tab:LDAP/AD 配置表单 + 组织用户管理区 // Logan.Li
// 需要转发全部 props 给 LdapManagementSection,所以接收整个 props 对象
type LdapSettingsTabProps = Pick<
SystemSettingsTabProps,
| "ldapConfig"
| "ldapForm"
| "setLdapForm"
| "ldapSaving"
| "ldapTesting"
| "ldapTestResult"
| "handleLdapSave"
| "handleLdapTest"
> &
// LdapManagementSection 需要的透传字段
Pick<
SystemSettingsTabProps,
| "ldapMgmtOpen"
| "setLdapMgmtOpen"
| "ldapOUs"
| "selectedOUDN"
| "setSelectedOUDN"
| "ldapUsers"
| "selectedLDAPUsers"
| "setSelectedLDAPUsers"
| "loadLdapOUs"
| "loadLdapOUUsers"
| "handleLdapImport"
| "importing"
| "importFeedback"
| "importResultOpen"
| "setImportResultOpen"
| "createOUOpen"
| "setCreateOUOpen"
| "ouForm"
| "setOuForm"
| "handleCreateOU"
| "createUserOpen"
| "setCreateUserOpen"
| "newUser"
| "setNewUser"
| "handleCreateLDAPUser"
| "ldapUserTestOpen"
| "setLdapUserTestOpen"
| "ldapTestUser"
| "setLdapTestUser"
| "ldapTestPass"
| "setLdapTestPass"
| "ldapUserTesting"
| "ldapUserTestResult"
| "handleLdapUserTest"
| "editLdapUser"
| "setEditLdapUser"
| "editLdapForm"
| "setEditLdapForm"
| "handleUpdateLdapUser"
| "handleDeleteLDAPEntry"
| "openEditLdapUser"
>;
export function LdapSettingsTab(props: LdapSettingsTabProps) {
const { t } = useTranslation();
const {
ldapConfig,
ldapForm,
setLdapForm,
ldapSaving,
ldapTesting,
ldapTestResult,
handleLdapSave,
handleLdapTest,
} = props;
return (
<>
<Card className="dg-card pt-2">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
<Network className="h-5 w-5 dg-text-cyan" />
LDAP / Active Directory
</CardTitle>
<CardDescription className="dg-text-muted mt-1">
{t("system.ldapConfigHint")}
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
<div className="space-y-1.5">
<label className="text-xs dg-text-muted">
{t("system.ldapServerAddr")}
</label>
<Input
value={ldapForm.ldap_host}
onChange={(e) =>
setLdapForm({ ...ldapForm, ldap_host: e.target.value })
}
className="dg-input"
placeholder="ldap.example.com"
/>
</div>
<div className="space-y-1.5">
<label className="text-xs dg-text-muted">
{t("system.ldapPort")}
</label>
<Input
type="number"
value={ldapForm.ldap_port}
onChange={(e) =>
setLdapForm({
...ldapForm,
ldap_port: Number(e.target.value),
})
}
className="dg-input"
placeholder="389"
/>
</div>
<div className="space-y-1.5 col-span-2">
<label className="text-xs dg-text-muted">
{t("system.baseDnHint")}
</label>
<Input
value={ldapForm.base_dn}
onChange={(e) =>
setLdapForm({ ...ldapForm, base_dn: e.target.value })
}
className="dg-input"
placeholder="ou=users,dc=example,dc=com"
/>
</div>
<div className="space-y-1.5">
<label className="text-xs dg-text-muted">
{t("system.adminBindDn")}
</label>
<Input
value={ldapForm.admin_dn}
onChange={(e) =>
setLdapForm({ ...ldapForm, admin_dn: e.target.value })
}
className="dg-input"
placeholder="cn=admin,dc=example,dc=com"
/>
</div>
<div className="space-y-1.5">
<label className="text-xs dg-text-muted">
管理员密码 {ldapConfig && t("system.leaveBlankNoModify")}
</label>
<Input
type="password"
value={ldapForm.admin_password}
onChange={(e) =>
setLdapForm({
...ldapForm,
admin_password: e.target.value,
})
}
className="dg-input"
placeholder="******"
/>
</div>
<div className="space-y-1.5 col-span-2">
<label className="text-xs dg-text-muted">
{t("system.searchFilterHint")}
</label>
<Input
value={ldapForm.search_filter}
onChange={(e) =>
setLdapForm({
...ldapForm,
search_filter: e.target.value,
})
}
className="dg-input"
placeholder="(uid=%s)"
/>
</div>
<div className="flex items-center gap-2 col-span-2">
<Switch
checked={ldapForm.use_ldaps}
onCheckedChange={(v) =>
setLdapForm({ ...ldapForm, use_ldaps: v })
}
className="data-[state=checked]:bg-blue-600"
/>
<span className="text-sm text-[var(--dg-text-secondary)]">
{t("system.enableLdaps")}
</span>
</div>
</div>
{/* 测试/保存按钮 */}
<div className="flex items-center gap-3 mt-4 pt-4 border-t border-[var(--dg-border)]">
{ldapTestResult && (
<span
className={`text-xs ${ldapTestResult.includes("成功") ? "dg-text-green" : "dg-text-red"}`}
>
{ldapTestResult}
</span>
)}
<div className="flex items-center gap-2 ml-auto">
<Button
variant="outline"
size="sm"
className="dg-btn-secondary"
onClick={handleLdapTest}
disabled={ldapTesting}
>
{ldapTesting ? (
<Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
) : (
<RefreshCw className="h-3.5 w-3.5 mr-1" />
)}
测试连接
</Button>
<Button
size="sm"
className="dg-btn-primary"
onClick={handleLdapSave}
disabled={ldapSaving}
>
{ldapSaving ? t("k8s.topology.saving") : t("aiops.llmSave")}
</Button>
</div>
</div>
</CardContent>
</Card>
{ldapConfig && <LdapManagementSection {...props} />}
</>
);
}
@@ -0,0 +1,309 @@
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Lock, Key, Loader2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useUserStore } from "@/store";
import type { SystemSettingsTabProps } from "../SystemSettingsTab";
// 安全设置 Tab:安全策略 + 会话设置(水印/录制/剪贴板/上传/下载/命令策略/审批/超时/SSO) // Logan.Li
type SecuritySettingsTabProps = Pick<
SystemSettingsTabProps,
| "sessionWatermarkEnabled"
| "setSessionWatermarkEnabled"
| "sessionRecordEnabled"
| "setSessionRecordEnabled"
| "clipboardEnabled"
| "setClipboardEnabled"
| "fileUploadEnabled"
| "setFileUploadEnabled"
| "fileDownloadEnabled"
| "setFileDownloadEnabled"
| "commandPolicyEnabled"
| "setCommandPolicyEnabled"
| "approvalRequired"
| "setApprovalRequired"
| "auditLoading"
| "auditApplying"
| "handleAuditApply"
>;
export function SecuritySettingsTab(props: SecuritySettingsTabProps) {
const { t } = useTranslation();
const currentUsername = useUserStore(
(state) => state.user?.username || "admin",
);
const {
sessionWatermarkEnabled,
setSessionWatermarkEnabled,
sessionRecordEnabled,
setSessionRecordEnabled,
clipboardEnabled,
setClipboardEnabled,
fileUploadEnabled,
setFileUploadEnabled,
fileDownloadEnabled,
setFileDownloadEnabled,
commandPolicyEnabled,
setCommandPolicyEnabled,
approvalRequired,
setApprovalRequired,
auditLoading,
auditApplying,
handleAuditApply,
} = props;
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
<Card className="dg-card">
<CardHeader>
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
<Lock className="h-5 w-5 dg-text-blue" />
{t("system.security")}
</CardTitle>
<CardDescription className="dg-text-muted">
{t("system.securityDesc")}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.forcePasswordComplexity")}
</p>
<p className="text-xs dg-text-muted">
{t("system.forcePasswordComplexityDesc")}
</p>
</div>
<Switch disabled className="data-[state=checked]:bg-blue-600" />
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.loginLockout")}
</p>
<p className="text-xs dg-text-muted">
{t("system.loginLockoutDesc")}
</p>
</div>
<Switch
disabled
defaultChecked
className="data-[state=checked]:bg-blue-600"
/>
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.twoFactor")}
</p>
<p className="text-xs dg-text-muted">
{t("system.twoFactorDesc")}
</p>
</div>
<Switch disabled className="data-[state=checked]:bg-blue-600" />
</div>
</CardContent>
</Card>
<Card className="dg-card">
<CardHeader>
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
<Key className="h-5 w-5 dg-text-yellow" />
{t("system.sessionSettings")}
</CardTitle>
<CardDescription className="dg-text-muted">
{t("system.sessionSettingsDesc")}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.sessionWatermark")}
</p>
<p className="text-xs dg-text-muted">
{t("system.sessionWatermarkDesc", {
user: currentUsername,
})}
</p>
</div>
<Switch
checked={sessionWatermarkEnabled}
onCheckedChange={setSessionWatermarkEnabled}
className="data-[state=checked]:bg-blue-600"
/>
</div>
<div className="rounded border border-[var(--dg-border)] bg-[#101418] px-3 py-4 overflow-hidden">
<div className="relative h-24 rounded bg-black/70">
<div
className="absolute inset-0 opacity-80"
style={{
backgroundImage: `repeating-linear-gradient(-24deg, transparent 0 42px, rgba(255,255,255,0.12) 43px 44px, transparent 45px 120px)`,
}}
/>
<div className="absolute inset-0 grid grid-cols-2 place-items-center -rotate-[18deg] text-lg font-semibold tracking-normal text-white/30">
<span>{currentUsername}</span>
<span>{currentUsername}</span>
<span>{currentUsername}</span>
<span>{currentUsername}</span>
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-3">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.sessionRecordEnabled")}
</p>
<p className="text-xs dg-text-muted">
{t("system.sessionRecordEnabledDesc")}
</p>
</div>
<Switch
checked={sessionRecordEnabled}
onCheckedChange={setSessionRecordEnabled}
className="data-[state=checked]:bg-blue-600"
/>
</div>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.clipboardEnabled")}
</p>
<p className="text-xs dg-text-muted">
{t("system.clipboardEnabledDesc")}
</p>
</div>
<Switch
checked={clipboardEnabled}
onCheckedChange={setClipboardEnabled}
className="data-[state=checked]:bg-blue-600"
/>
</div>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.fileUploadEnabled")}
</p>
<p className="text-xs dg-text-muted">
{t("system.fileUploadEnabledDesc")}
</p>
</div>
<Switch
checked={fileUploadEnabled}
onCheckedChange={setFileUploadEnabled}
className="data-[state=checked]:bg-blue-600"
/>
</div>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.fileDownloadEnabled")}
</p>
<p className="text-xs dg-text-muted">
{t("system.fileDownloadEnabledDesc")}
</p>
</div>
<Switch
checked={fileDownloadEnabled}
onCheckedChange={setFileDownloadEnabled}
className="data-[state=checked]:bg-blue-600"
/>
</div>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.commandPolicyEnabled")}
</p>
<p className="text-xs dg-text-muted">
{t("system.commandPolicyEnabledDesc")}
</p>
</div>
<Switch
checked={commandPolicyEnabled}
onCheckedChange={setCommandPolicyEnabled}
className="data-[state=checked]:bg-blue-600"
/>
</div>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.approvalRequired")}
</p>
<p className="text-xs dg-text-muted">
{t("system.approvalRequiredDesc")}
</p>
</div>
<Switch
checked={approvalRequired}
onCheckedChange={setApprovalRequired}
className="data-[state=checked]:bg-blue-600"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.sessionTimeoutTime")}
</p>
<p className="text-xs dg-text-muted">
{t("system.sessionTimeoutDesc")}
</p>
</div>
<Select defaultValue="30" disabled>
<SelectTrigger className="w-32 h-8 dg-input">
<SelectValue />
</SelectTrigger>
<SelectContent className="dg-input">
<SelectItem value="15">
{t("monitor.traceMgr.min15")}
</SelectItem>
<SelectItem value="30">30分钟</SelectItem>
<SelectItem value="60">
{t("monitor.traceMgr.hour1")}
</SelectItem>
<SelectItem value="120">2小时</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
{t("system.sso")}
</p>
<p className="text-xs dg-text-muted">
{t("system.singleSignOnDesc")}
</p>
</div>
<Switch disabled className="data-[state=checked]:bg-blue-600" />
</div>
<div className="flex justify-end pt-1">
<Button
size="sm"
className="dg-btn-primary"
onClick={handleAuditApply}
disabled={auditLoading || auditApplying}
>
{auditApplying ? (
<>
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
{t("system.applyingDot")}
</>
) : (
t("common.apply")
)}
</Button>
</div>
</CardContent>
</Card>
</div>
);
}