Files
aiops/frontend/src/components/system/SystemSettingsTab.tsx
T
attacker 72bfb0e12d fix(i18n): 回退到稳定的 32% 覆盖率状态 (tsc + Vite 构建通过)
批量转换脚本引入的语法错误已回退,保留稳定的 i18n 转换:
- 翻译词典: 22 namespaces, 3600+ keys
- 已接入: 145+ 文件 (Dashboard/Login/K8s 主页面/Monitor/System/Deploy)
- 语言切换器在 Header 中可用 (🇨🇳/🇺🇸)
- tsc 零错误, Vite 构建成功
2026-07-03 02:02:29 +08:00

434 lines
17 KiB
TypeScript

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';
// 系统设置子 Tab:通用 / 安全 / LDAP // Logan.Li
const SETTINGS_TABS = [
{ id: 'general', label: i18n.t('system.settingsGeneral') },
{ id: 'security', label: i18n.t('system.settingsSecurity') },
{ id: 'ldap', label: 'LDAP' },
] as const;
type SettingsSubTab = (typeof SETTINGS_TABS)[number]['id'];
export interface LdapFormData {
ldap_host: string;
ldap_port: number;
use_ldaps: boolean;
admin_dn: string;
admin_password: string;
base_dn: string;
search_filter: string;
}
interface SystemSettingsTabProps {
// 审计策略 // Logan.Li
auditRetentionDays: string;
setAuditRetentionDays: (v: string) => void;
auditRecordAll: boolean;
setAuditRecordAll: (v: boolean) => void;
auditLoading: boolean;
auditApplying: boolean;
handleAuditApply: () => void;
auditLastCleanupText: string | null;
auditRetentionOptions: number[];
// LDAP config
ldapConfig: unknown;
ldapForm: LdapFormData;
setLdapForm: React.Dispatch<React.SetStateAction<LdapFormData>>;
ldapSaving: boolean;
ldapTesting: boolean;
ldapTestResult: string | null;
handleLdapSave: () => void;
handleLdapTest: () => void;
// LDAP management - pass-through to LdapManagementSection
ldapMgmtOpen: boolean;
setLdapMgmtOpen: (v: boolean) => void;
ldapOUs: unknown[];
selectedOUDN: string;
setSelectedOUDN: (v: string) => void;
ldapUsers: unknown[];
selectedLDAPUsers: string[];
setSelectedLDAPUsers: (v: string[]) => void;
loadLdapOUs: () => void;
loadLdapOUUsers: (ouDn?: string) => void;
handleLdapImport: (opts?: { importAll?: boolean }) => void | Promise<void>;
importing: boolean;
importFeedback: string | null;
importResultOpen: boolean;
setImportResultOpen: (v: boolean) => void;
createOUOpen: boolean;
setCreateOUOpen: (v: boolean) => void;
ouForm: { ou_name: string; parent_dn: string; description: string };
setOuForm: React.Dispatch<React.SetStateAction<{ ou_name: string; parent_dn: string; description: string }>>;
handleCreateOU: () => void;
createUserOpen: boolean;
setCreateUserOpen: (v: boolean) => void;
newUser: { username: string; password: string; display_name: string; email: string; ou_dn: string };
setNewUser: React.Dispatch<React.SetStateAction<{ username: string; password: string; display_name: string; email: string; ou_dn: string }>>;
handleCreateLDAPUser: () => void;
ldapUserTestOpen: boolean;
setLdapUserTestOpen: (v: boolean) => void;
ldapTestUser: string;
setLdapTestUser: (v: string) => void;
ldapTestPass: string;
setLdapTestPass: (v: string) => void;
ldapUserTesting: boolean;
ldapUserTestResult: unknown;
handleLdapUserTest: () => void;
editLdapUser: JsonData | null;
setEditLdapUser: (u: JsonData | null) => void;
editLdapForm: { cn: string; mail: string; phone: string; title: string };
setEditLdapForm: React.Dispatch<React.SetStateAction<{ cn: string; mail: string; phone: string; title: string }>>;
handleUpdateLdapUser: () => void;
handleDeleteLDAPEntry: (dn: string, silent?: boolean) => void;
openEditLdapUser: (user: JsonData) => void;
}
export function SystemSettingsTab(props: SystemSettingsTabProps) {
const { t } = useTranslation();
const {
auditRetentionDays,
setAuditRetentionDays,
auditRecordAll,
setAuditRecordAll,
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>(
SETTINGS_TABS.some(t => t.id === tabFromUrl) ? tabFromUrl! : 'general',
);
useEffect(() => {
if (tabFromUrl && SETTINGS_TABS.some(t => t.id === tabFromUrl) && tabFromUrl !== subTab) {
setSubTab(tabFromUrl);
}
}, [tabFromUrl, subTab]);
const changeSubTab = (id: string) => {
const next = id as SettingsSubTab;
setSubTab(next);
const params = new URLSearchParams(searchParams);
if (next === 'general') params.delete('tab');
else params.set('tab', next);
setSearchParams(params, { replace: true });
};
return (
<div className="w-full space-y-4">
<PageTabs items={[...SETTINGS_TABS]} value={subTab} 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">
<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>
</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} />}
</>
)}
</div>
);
}