feat: 完善堡垒机与 Agent 智能运维能力

- 支持 Windows Agent 构建、安装脚本和统一上报协议

- 增强 RDP 剪贴板、虚拟盘和文件传输能力

- 新增 AIOps 上下文工具、告警 RCA trace 落库和回放接口

- 重构 docs 模块文档并补充终端、堡垒机、监控、AI Agent 设计
This commit is contained in:
Logan.Li committed 2026-07-05 23:49:36 +08:00
1 parent a9513c2056
commit e0d652d211
169 files changed
+12192 -9116

No files matched your search

@@ -78,6 +78,7 @@ export function RdpAuditViewer({ open, onOpenChange, sessionId, mode }: RdpAudit
const canvasRef = useRef<HTMLCanvasElement>(null);
const wsRef = useRef<WebSocket | null>(null);
const timerRef = useRef<number | null>(null);
const replaySeqRef = useRef(0);
const [fullscreen, setFullscreen] = useState(false);
const [status, setStatus] = useState('');
const [loading, setLoading] = useState(false);
@@ -100,13 +101,26 @@ export function RdpAuditViewer({ open, onOpenChange, sessionId, mode }: RdpAudit
ctx.putImageData(new ImageData(pixels, frame.width, frame.height), frame.x, frame.y);
};
const drawReplay = async (nextIndex: number) => {
const frame = playableFrames[nextIndex];
const clearReplayCanvas = () => {
const canvas = canvasRef.current;
const ctx = canvas?.getContext('2d');
if (!frame || !canvas || !ctx) return;
if (!canvas || !ctx) return null;
ctx.imageSmoothingEnabled = false;
await drawImageFrame(ctx, frame);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
return ctx;
};
const drawReplay = async (nextIndex: number, rebuild = false) => {
const ctx = rebuild ? clearReplayCanvas() : canvasRef.current?.getContext('2d');
if (!ctx) return;
ctx.imageSmoothingEnabled = false;
const start = rebuild ? 0 : nextIndex;
for (let i = start; i <= nextIndex; i += 1) {
const frame = playableFrames[i];
if (frame) await drawImageFrame(ctx, frame);
}
};
useEffect(() => {
@@ -161,6 +175,11 @@ export function RdpAuditViewer({ open, onOpenChange, sessionId, mode }: RdpAudit
const text = typeof raw === 'string' ? raw : String(raw || '');
const record = JSON.parse(text || '{}') as RdpRecord;
const list = (record.frames || []).filter((f) => f.type === 'bitmap' && f.image);
const sessionFrame = (record.frames || []).find((f) => f.type === 'session' && f.width && f.height);
const sizeFrame = sessionFrame || list.find((f) => f.width && f.height);
if (sizeFrame?.width && sizeFrame?.height) {
setCanvasSize({ width: sizeFrame.width, height: sizeFrame.height });
}
setFrames(list);
setStatus(list.length ? '' : '该 RDP 会话没有可播放画面帧');
setPlaying(list.length > 0);
@@ -196,7 +215,12 @@ export function RdpAuditViewer({ open, onOpenChange, sessionId, mode }: RdpAudit
}, [playing, mode, playableFrames.length]);
useEffect(() => {
if (mode === 'replay' && playableFrames.length > 0) void drawReplay(index);
if (mode !== 'replay' || playableFrames.length === 0) return;
const seq = replaySeqRef.current + 1;
replaySeqRef.current = seq;
void drawReplay(index, true).then(() => {
if (replaySeqRef.current !== seq) return;
});
}, [index, playableFrames.length, mode]);
const displayHeight = fullscreen ? 'calc(100vh - 88px)' : '560px';
+204 -137
View File
@@ -1,41 +1,44 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { Badge } from '@/components/ui/badge';
import { toast } from 'sonner';
import { CheckCircle, XCircle, Settings, Users, Shield } from 'lucide-react';
import { PageShell } from '@/components/layout/PageShell';
import { userApi } from '@/api';
import type { Role } from '@/types';
import { SystemUserTab } from './SystemUserTab';
import { SystemRolesTab } from './SystemRolesTab';
import { SystemSettingsTab } from './SystemSettingsTab';
import { ResetPasswordDialog } from './ResetPasswordDialog';
import { useLdapManagement } from './useLdapManagement';
import { useAuditSettings } from './useAuditSettings';
import type { UserFormData } from './UserFormDialog';
import { useTranslation } from 'react-i18next';
import { useState, useEffect } from "react";
import { useLocation } from "react-router-dom";
import { useConfirm } from "@/components/ui/confirm-dialog";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import { CheckCircle, XCircle, Settings, Users, Shield } from "lucide-react";
import { PageShell } from "@/components/layout/PageShell";
import { userApi } from "@/api";
import type { Role } from "@/types";
import { SystemUserTab } from "./SystemUserTab";
import { SystemRolesTab } from "./SystemRolesTab";
import { SystemSettingsTab } from "./SystemSettingsTab";
import { ResetPasswordDialog } from "./ResetPasswordDialog";
import { useLdapManagement } from "./useLdapManagement";
import { useAuditSettings } from "./useAuditSettings";
import type { UserFormData } from "./UserFormDialog";
import { useTranslation } from "react-i18next";
// 后端用户数据转换为前端兼容格式
function mapUser( u: any) {
function mapUser(u: any) {
return {
...u,
status: u.status === 1 || u.status === 'active' ? ('active' as const) : ('inactive' as const),
status:
u.status === 1 || u.status === "active"
? ("active" as const)
: ("inactive" as const),
roles: Array.isArray(u.roles) ? u.roles : u.role ? [u.role] : [],
lastLoginAt: u.last_login ?? u.lastLoginAt,
createdAt: u.created_at ?? u.createdAt,
authSource: u.auth_source ?? u.authSource ?? 'local',
authSource: u.auth_source ?? u.authSource ?? "local",
};
}
const emptyForm: UserFormData = {
username: '',
nickname: '',
email: '',
phone: '',
role: '',
password: '',
username: "",
nickname: "",
email: "",
phone: "",
role: "",
password: "",
status: 1 as number,
};
@@ -45,32 +48,35 @@ export function SystemManager() {
const location = useLocation();
// 侧边栏菜单路径映射到 Tab // Logan.Li
// 侧边栏已有独立菜单项,按路由直接渲染对应页面,不再重复 Tab // Logan.Li
const pathToTab: Record<string, 'users' | 'roles' | 'settings'> = {
'/users': 'users',
'/roles': 'roles',
'/settings': 'settings',
const pathToTab: Record<string, "users" | "roles" | "settings"> = {
"/users": "users",
"/roles": "roles",
"/settings": "settings",
};
const activeTab = pathToTab[location.pathname] || 'users';
const activeTab = pathToTab[location.pathname] || "users";
const pageMeta: Record<string, { title: string; subtitle: string; icon: React.ReactNode }> = {
const pageMeta: Record<
string,
{ title: string; subtitle: string; icon: React.ReactNode }
> = {
users: {
title: t('system.users'),
subtitle: t('system.localUserDesc'),
title: t("system.users"),
subtitle: t("system.localUserDesc"),
icon: <Users className="h-5 w-5" />,
},
roles: {
title: t('system.roles'),
subtitle: t('system.roleBindingHint'),
title: t("system.roles"),
subtitle: t("system.roleBindingHint"),
icon: <Shield className="h-5 w-5" />,
},
settings: {
title: t('system.settings'),
subtitle: t('system.generalSubtitle'),
title: t("system.settings"),
subtitle: t("system.generalSubtitle"),
icon: <Settings className="h-5 w-5" />,
},
};
const meta = pageMeta[activeTab];
const [searchQuery, setSearchQuery] = useState('');
const [searchQuery, setSearchQuery] = useState("");
const [users, setUsers] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [selectedUser, setSelectedUser] = useState<any | null>(null);
@@ -84,7 +90,9 @@ export function SystemManager() {
const [form, setForm] = useState<UserFormData>({ ...emptyForm });
// CSV 批量导入弹窗
const [importDialogOpen, setImportDialogOpen] = useState<'csv' | 'ldap' | null>(null);
const [importDialogOpen, setImportDialogOpen] = useState<
"csv" | "ldap" | null
>(null);
const [importPreview, setImportPreview] = useState<any[]>([]);
const [importResult, setImportResult] = useState<any>(null);
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
@@ -96,7 +104,7 @@ export function SystemManager() {
const rawUsers = res?.list ?? [];
setUsers(rawUsers.map(mapUser));
} catch (error) {
console.error('Failed to fetch users:', error);
console.error("Failed to fetch users:", error);
setUsers([]);
} finally {
setLoading(false);
@@ -114,28 +122,34 @@ export function SystemManager() {
// LDAP 导入 Dialog 打开时加载 OU 和用户
useEffect(() => {
if (importDialogOpen === 'ldap') {
if (importDialogOpen === "ldap") {
ldap.loadImportDialogData();
}
}, [importDialogOpen]);
const filteredUsers = users.filter(
(u) =>
(u.username || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
(u.nickname || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
(u.email || '').toLowerCase().includes(searchQuery.toLowerCase())
(u.username || "").toLowerCase().includes(searchQuery.toLowerCase()) ||
(u.nickname || "").toLowerCase().includes(searchQuery.toLowerCase()) ||
(u.email || "").toLowerCase().includes(searchQuery.toLowerCase()),
);
const getUserStatusBadge = (status: 'active' | 'inactive') => {
return status === 'active' ? (
<Badge variant="outline" className="border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10">
const getUserStatusBadge = (status: "active" | "inactive") => {
return status === "active" ? (
<Badge
variant="outline"
className="border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10"
>
<CheckCircle className="h-3 w-3 mr-1" />
{t('common.normal')}
{t("common.normal")}
</Badge>
) : (
<Badge variant="outline" className="border-[var(--dg-text-muted)]/30 text-[var(--dg-text-muted)] bg-[var(--dg-text-muted)]/10">
<Badge
variant="outline"
className="border-[var(--dg-text-muted)]/30 text-[var(--dg-text-muted)] bg-[var(--dg-text-muted)]/10"
>
<XCircle className="h-3 w-3 mr-1" />
{t('system.userInactive')}
{t("system.userInactive")}
</Badge>
);
};
@@ -146,16 +160,16 @@ export function SystemManager() {
setDialogOpen(true);
};
const openEdit = ( user: any) => {
const openEdit = (user: any) => {
setEditingUser(user);
setForm({
username: user.username || '',
nickname: user.nickname || '',
email: user.email || '',
phone: user.phone || '',
role: Array.isArray(user.roles) ? user.roles[0] || '' : user.role || '',
password: '',
status: user.status === 'active' ? 1 : 0,
username: user.username || "",
nickname: user.nickname || "",
email: user.email || "",
phone: user.phone || "",
role: Array.isArray(user.roles) ? user.roles[0] || "" : user.role || "",
password: "",
status: user.status === "active" ? 1 : 0,
});
setDialogOpen(true);
};
@@ -182,41 +196,52 @@ export function SystemManager() {
setDialogOpen(false);
await loadUsers();
} catch (error) {
console.error('Failed to save user:', error);
console.error("Failed to save user:", error);
} finally {
setSaving(false);
}
};
const handleDelete = async ( user: any) => {
const ok = await confirm({ title: t('system.deleteUser'), description: `确认删除用户「${user.nickname || user.username}」吗?`, confirmText: t('common.delete'), variant: 'destructive' });
const handleDelete = async (user: any) => {
const ok = await confirm({
title: t("system.deleteUser"),
description: `确认删除用户「${user.nickname || user.username}」吗?`,
confirmText: t("common.delete"),
variant: "destructive",
});
if (!ok) return;
try {
await userApi.deleteUser(user.id);
setSelectedUserIds((prev) => prev.filter((id) => id !== user.id));
await loadUsers();
toast.success(t('system.userDeleted'));
toast.success(t("system.userDeleted"));
} catch (error: unknown) {
toast.error(error instanceof Error ? error.message : t('common.deleteFailed'));
toast.error(
error instanceof Error ? error.message : t("common.deleteFailed"),
);
}
};
const handleBatchDelete = async () => {
if (selectedUserIds.length === 0) return;
const ok = await confirm({
title: t('system.batchDeleteUsers'),
title: t("system.batchDeleteUsers"),
description: `确定删除选中的 ${selectedUserIds.length} 个用户吗?此操作不可恢复。`,
confirmText: t('system.terminalBatchDelete'),
variant: 'destructive',
confirmText: t("system.terminalBatchDelete"),
variant: "destructive",
});
if (!ok) return;
try {
const res = await userApi.batchDelete(selectedUserIds) as { message?: string };
const res = (await userApi.batchDelete(selectedUserIds)) as {
message?: string;
};
toast.success(res?.message || `已删除 ${selectedUserIds.length} 个用户`);
setSelectedUserIds([]);
await loadUsers();
} catch (error: unknown) {
toast.error(error instanceof Error ? error.message : t('cloud.batchDeleteFailed'));
toast.error(
error instanceof Error ? error.message : t("cloud.batchDeleteFailed"),
);
}
};
@@ -227,44 +252,64 @@ export function SystemManager() {
const reader = new FileReader();
reader.onload = (ev) => {
const text = ev.target?.result as string;
const lines = text.split('\n').filter((l) => l.trim() && !l.startsWith('\xEF\xBB\xBF'));
const lines = text
.split("\n")
.filter((l) => l.trim() && !l.startsWith("\xEF\xBB\xBF"));
if (lines.length < 2) return;
// Find column indices from header
const headers = lines[0].split(',').map((h) => h.trim().toLowerCase());
const idxUsername = headers.findIndex((h) => h.includes('用户名') || h.includes('username'));
const idxNickname = headers.findIndex((h) => h.includes('昵称') || h.includes('name') || h.includes('显示名'));
const idxEmail = headers.findIndex((h) => h.includes('邮箱') || h.includes('email') || h.includes('mail'));
const idxPhone = headers.findIndex((h) => h.includes('手机') || h.includes('phone'));
const idxRole = headers.findIndex((h) => h.includes('角色') || h.includes('role'));
const idxPassword = headers.findIndex((h) => h.includes('密码') || h.includes('password'));
const headers = lines[0].split(",").map((h) => h.trim().toLowerCase());
const idxUsername = headers.findIndex(
(h) => h.includes("用户名") || h.includes("username"),
);
const idxNickname = headers.findIndex(
(h) => h.includes("昵称") || h.includes("name") || h.includes("显示名"),
);
const idxEmail = headers.findIndex(
(h) => h.includes("邮箱") || h.includes("email") || h.includes("mail"),
);
const idxPhone = headers.findIndex(
(h) => h.includes("手机") || h.includes("phone"),
);
const idxRole = headers.findIndex(
(h) => h.includes("角色") || h.includes("role"),
);
const idxPassword = headers.findIndex(
(h) => h.includes("密码") || h.includes("password"),
);
const parsed: Record<string, unknown>[] = [];
for (let i = 1; i < lines.length; i++) {
const cols = lines[i].split(',').map((c) => c.trim());
const cols = lines[i].split(",").map((c) => c.trim());
parsed.push({
username: idxUsername >= 0 ? cols[idxUsername] : '',
nickname: idxNickname >= 0 ? cols[idxNickname] : '',
email: idxEmail >= 0 ? cols[idxEmail] : '',
phone: idxPhone >= 0 ? cols[idxPhone] : '',
role: idxRole >= 0 ? cols[idxRole] : '',
password: idxPassword >= 0 ? cols[idxPassword] : '',
username: idxUsername >= 0 ? cols[idxUsername] : "",
nickname: idxNickname >= 0 ? cols[idxNickname] : "",
email: idxEmail >= 0 ? cols[idxEmail] : "",
phone: idxPhone >= 0 ? cols[idxPhone] : "",
role: idxRole >= 0 ? cols[idxRole] : "",
password: idxPassword >= 0 ? cols[idxPassword] : "",
});
}
setImportPreview(parsed);
setImportResult(null);
};
reader.readAsText(file, 'UTF-8');
reader.readAsText(file, "UTF-8");
};
// 执行 CSV 批量导入
const handleBatchImport = async () => {
ldap.setImporting(true);
try {
const res = await userApi.batchImport({ users: importPreview, skip_existing: true });
const res = await userApi.batchImport({
users: importPreview,
skip_existing: true,
});
setImportResult(res);
await loadUsers();
} catch (error) {
setImportResult({ code: 500, message: error?.message || t('system.importFailed') });
setImportResult({
code: 500,
message: error?.message || t("system.importFailed"),
});
} finally {
ldap.setImporting(false);
}
@@ -272,74 +317,93 @@ export function SystemManager() {
return (
<PageShell
variant={activeTab === 'settings' ? 'config' : 'list'}
maxWidth={activeTab === 'settings' ? 'xl' : 'full'}
variant={activeTab === "settings" ? "config" : "list"}
maxWidth={activeTab === "settings" ? "xl" : "full"}
title={meta.title}
subtitle={meta.subtitle}
icon={meta.icon}
>
<div className="w-full">
{activeTab === 'users' && (
<div className="space-y-4">
<SystemUserTab
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
filteredUsers={filteredUsers}
loading={loading}
selectedUser={selectedUser}
setSelectedUser={setSelectedUser}
getUserStatusBadge={getUserStatusBadge}
dialogOpen={dialogOpen}
setDialogOpen={setDialogOpen}
editingUser={editingUser}
form={form}
setForm={setForm}
saving={saving}
handleSubmit={handleSubmit}
openCreate={openCreate}
openEdit={openEdit}
handleDelete={handleDelete}
handleBatchDelete={handleBatchDelete}
selectedUserIds={selectedUserIds}
setSelectedUserIds={setSelectedUserIds}
onResetPassword={(u) => { setSelectedUser(null); setResetPasswordUser(u); }}
importDialogOpen={importDialogOpen}
setImportDialogOpen={setImportDialogOpen}
importPreview={importPreview}
setImportPreview={setImportPreview}
importing={ldap.importing}
importResult={importResult}
setImportResult={setImportResult}
handleCsvUpload={handleCsvUpload}
handleBatchImport={handleBatchImport}
ldapUsers={ldap.ldapUsers}
ldapSelected={ldap.ldapSelected}
setLdapSelected={ldap.setLdapSelected}
ldapLoading={ldap.ldapLoading}
ldapImportOUs={ldap.ldapImportOUs}
ldapImportOUDN={ldap.ldapImportOUDN}
setLdapImportOUDN={ldap.setLdapImportOUDN}
loadLdapUsers={ldap.loadLdapUsers}
handleLdapImport={() => ldap.handleLdapImport({ setResult: setImportResult })}
setLdapUsers={ldap.setLdapUsers}
/>
</div>
{activeTab === "users" && (
<div className="space-y-4">
<SystemUserTab
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
filteredUsers={filteredUsers}
loading={loading}
selectedUser={selectedUser}
setSelectedUser={setSelectedUser}
getUserStatusBadge={getUserStatusBadge}
dialogOpen={dialogOpen}
setDialogOpen={setDialogOpen}
editingUser={editingUser}
form={form}
setForm={setForm}
saving={saving}
handleSubmit={handleSubmit}
openCreate={openCreate}
openEdit={openEdit}
handleDelete={handleDelete}
handleBatchDelete={handleBatchDelete}
selectedUserIds={selectedUserIds}
setSelectedUserIds={setSelectedUserIds}
onResetPassword={(u) => {
setSelectedUser(null);
setResetPasswordUser(u);
}}
importDialogOpen={importDialogOpen}
setImportDialogOpen={setImportDialogOpen}
importPreview={importPreview}
setImportPreview={setImportPreview}
importing={ldap.importing}
importResult={importResult}
setImportResult={setImportResult}
handleCsvUpload={handleCsvUpload}
handleBatchImport={handleBatchImport}
ldapUsers={ldap.ldapUsers}
ldapSelected={ldap.ldapSelected}
setLdapSelected={ldap.setLdapSelected}
ldapLoading={ldap.ldapLoading}
ldapImportOUs={ldap.ldapImportOUs}
ldapImportOUDN={ldap.ldapImportOUDN}
setLdapImportOUDN={ldap.setLdapImportOUDN}
loadLdapUsers={ldap.loadLdapUsers}
handleLdapImport={() =>
ldap.handleLdapImport({ setResult: setImportResult })
}
setLdapUsers={ldap.setLdapUsers}
/>
</div>
)}
{activeTab === 'roles' && (
{activeTab === "roles" && (
<SystemRolesTab
selectedRole={selectedRole}
setSelectedRole={setSelectedRole}
/>
)}
{activeTab === 'settings' && (
{activeTab === "settings" && (
<SystemSettingsTab
{...ldap.settingsTabProps}
auditRetentionDays={audit.retentionDays}
setAuditRetentionDays={audit.setRetentionDays}
auditRecordAll={audit.recordAll}
setAuditRecordAll={audit.setRecordAll}
sessionWatermarkEnabled={audit.sessionWatermarkEnabled}
setSessionWatermarkEnabled={audit.setSessionWatermarkEnabled}
sessionRecordEnabled={audit.sessionRecordEnabled}
setSessionRecordEnabled={audit.setSessionRecordEnabled}
clipboardEnabled={audit.clipboardEnabled}
setClipboardEnabled={audit.setClipboardEnabled}
fileUploadEnabled={audit.fileUploadEnabled}
setFileUploadEnabled={audit.setFileUploadEnabled}
fileDownloadEnabled={audit.fileDownloadEnabled}
setFileDownloadEnabled={audit.setFileDownloadEnabled}
commandPolicyEnabled={audit.commandPolicyEnabled}
setCommandPolicyEnabled={audit.setCommandPolicyEnabled}
approvalRequired={audit.approvalRequired}
setApprovalRequired={audit.setApprovalRequired}
auditLoading={audit.loading}
auditApplying={audit.applying}
handleAuditApply={audit.handleApply}
@@ -349,7 +413,10 @@ export function SystemManager() {
)}
</div>
<ResetPasswordDialog user={resetPasswordUser} onClose={() => setResetPasswordUser(null)} />
<ResetPasswordDialog
user={resetPasswordUser}
onClose={() => setResetPasswordUser(null)}
/>
</PageShell>
);
}
@@ -1,30 +1,45 @@
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 { 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';
} 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";
// 系统设置子 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' },
{ 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'];
type SettingsSubTab = (typeof SETTINGS_TABS)[number]["id"];
export interface LdapFormData {
ldap_host: string;
@@ -42,6 +57,20 @@ interface SystemSettingsTabProps {
setAuditRetentionDays: (v: string) => void;
auditRecordAll: boolean;
setAuditRecordAll: (v: boolean) => void;
sessionWatermarkEnabled: boolean;
setSessionWatermarkEnabled: (v: boolean) => void;
sessionRecordEnabled: boolean;
setSessionRecordEnabled: (v: boolean) => void;
clipboardEnabled: boolean;
setClipboardEnabled: (v: boolean) => void;
fileUploadEnabled: boolean;
setFileUploadEnabled: (v: boolean) => void;
fileDownloadEnabled: boolean;
setFileDownloadEnabled: (v: boolean) => void;
commandPolicyEnabled: boolean;
setCommandPolicyEnabled: (v: boolean) => void;
approvalRequired: boolean;
setApprovalRequired: (v: boolean) => void;
auditLoading: boolean;
auditApplying: boolean;
handleAuditApply: () => void;
@@ -75,12 +104,32 @@ interface SystemSettingsTabProps {
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 }>>;
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 }>>;
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;
@@ -94,7 +143,14 @@ interface SystemSettingsTabProps {
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 }>>;
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;
@@ -102,11 +158,28 @@ 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,
@@ -123,13 +196,17 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
} = props;
const [searchParams, setSearchParams] = useSearchParams();
const tabFromUrl = searchParams.get('tab') as SettingsSubTab | null;
const tabFromUrl = searchParams.get("tab") as SettingsSubTab | null;
const [subTab, setSubTab] = useState<SettingsSubTab>(
SETTINGS_TABS.some(t => t.id === tabFromUrl) ? tabFromUrl! : 'general',
SETTINGS_TABS.some((t) => t.id === tabFromUrl) ? tabFromUrl! : "general",
);
useEffect(() => {
if (tabFromUrl && SETTINGS_TABS.some(t => t.id === tabFromUrl) && tabFromUrl !== subTab) {
if (
tabFromUrl &&
SETTINGS_TABS.some((t) => t.id === tabFromUrl) &&
tabFromUrl !== subTab
) {
setSubTab(tabFromUrl);
}
}, [tabFromUrl, subTab]);
@@ -138,295 +215,538 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
const next = id as SettingsSubTab;
setSubTab(next);
const params = new URLSearchParams(searchParams);
if (next === 'general') params.delete('tab');
else params.set('tab', next);
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} />
<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>
{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">
<Network className="h-5 w-5 dg-text-cyan" />
LDAP / Active Directory
<History className="h-5 w-5 dg-text-green" />
{t("system.audit")}
</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>
<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>
{/* 测试/保存按钮 */}
<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>
<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>
)}
{ldapConfig && <LdapManagementSection {...props} />}
</>
{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} />}
</>
)}
</div>
);
@@ -1,15 +1,23 @@
// 审计策略配置 — 保留天数与应用后立即清理 // Logan.Li
import { useState, useEffect, useCallback } from 'react';
import { toast } from 'sonner';
import { auditSettingsApi, type AuditSettings } from '@/api';
import i18n from '@/i18n';
import { useState, useEffect, useCallback } from "react";
import { toast } from "sonner";
import { auditSettingsApi, type AuditSettings } from "@/api";
import i18n from "@/i18n";
import { useSessionSecurityStore } from "@/store";
const RETENTION_OPTIONS = [7, 15, 30, 90, 180, 365] as const;
export function useAuditSettings() {
const [settings, setSettings] = useState<AuditSettings | null>(null);
const [retentionDays, setRetentionDays] = useState('180');
const [retentionDays, setRetentionDays] = useState("180");
const [recordAll, setRecordAll] = useState(true);
const [sessionWatermarkEnabled, setSessionWatermarkEnabled] = useState(true);
const [sessionRecordEnabled, setSessionRecordEnabled] = useState(true);
const [clipboardEnabled, setClipboardEnabled] = useState(true);
const [fileUploadEnabled, setFileUploadEnabled] = useState(true);
const [fileDownloadEnabled, setFileDownloadEnabled] = useState(true);
const [commandPolicyEnabled, setCommandPolicyEnabled] = useState(false);
const [approvalRequired, setApprovalRequired] = useState(false);
const [loading, setLoading] = useState(true);
const [applying, setApplying] = useState(false);
@@ -20,8 +28,25 @@ export function useAuditSettings() {
setSettings(data);
setRetentionDays(String(data.retention_days || 180));
setRecordAll(data.record_all !== false);
const policy = {
watermarkEnabled: data.session_watermark_enabled !== false,
recordEnabled: data.session_record_enabled !== false,
clipboardEnabled: data.clipboard_enabled !== false,
fileUploadEnabled: data.file_upload_enabled !== false,
fileDownloadEnabled: data.file_download_enabled !== false,
commandPolicyEnabled: data.command_policy_enabled === true,
approvalRequired: data.approval_required === true,
};
setSessionWatermarkEnabled(policy.watermarkEnabled);
setSessionRecordEnabled(policy.recordEnabled);
setClipboardEnabled(policy.clipboardEnabled);
setFileUploadEnabled(policy.fileUploadEnabled);
setFileDownloadEnabled(policy.fileDownloadEnabled);
setCommandPolicyEnabled(policy.commandPolicyEnabled);
setApprovalRequired(policy.approvalRequired);
useSessionSecurityStore.getState().setPolicy(policy);
} catch {
toast.error(i18n.t('system.useauditsetting.s32e8a76e'));
toast.error(i18n.t("system.useauditsetting.s32e8a76e"));
} finally {
setLoading(false);
}
@@ -33,19 +58,52 @@ export function useAuditSettings() {
const handleApply = async () => {
const days = Number(retentionDays);
if (!RETENTION_OPTIONS.includes(days as (typeof RETENTION_OPTIONS)[number])) {
toast.error(i18n.t('system.useauditsetting.s51680af0'));
if (
!RETENTION_OPTIONS.includes(days as (typeof RETENTION_OPTIONS)[number])
) {
toast.error(i18n.t("system.useauditsetting.s51680af0"));
return;
}
setApplying(true);
try {
const res = await auditSettingsApi.apply({ retention_days: days, record_all: recordAll });
const res = await auditSettingsApi.apply({
retention_days: days,
record_all: recordAll,
session_watermark_enabled: sessionWatermarkEnabled,
session_record_enabled: sessionRecordEnabled,
clipboard_enabled: clipboardEnabled,
file_upload_enabled: fileUploadEnabled,
file_download_enabled: fileDownloadEnabled,
command_policy_enabled: commandPolicyEnabled,
approval_required: approvalRequired,
});
setSettings({
retention_days: res.retention_days ?? days,
record_all: res.record_all ?? recordAll,
session_watermark_enabled:
res.session_watermark_enabled ?? sessionWatermarkEnabled,
session_record_enabled:
res.session_record_enabled ?? sessionRecordEnabled,
clipboard_enabled: res.clipboard_enabled ?? clipboardEnabled,
file_upload_enabled: res.file_upload_enabled ?? fileUploadEnabled,
file_download_enabled: res.file_download_enabled ?? fileDownloadEnabled,
command_policy_enabled:
res.command_policy_enabled ?? commandPolicyEnabled,
approval_required: res.approval_required ?? approvalRequired,
last_cleanup_at: res.last_cleanup_at,
last_cleanup_summary: res.last_cleanup_summary,
});
useSessionSecurityStore.getState().setPolicy({
watermarkEnabled:
res.session_watermark_enabled ?? sessionWatermarkEnabled,
recordEnabled: res.session_record_enabled ?? sessionRecordEnabled,
clipboardEnabled: res.clipboard_enabled ?? clipboardEnabled,
fileUploadEnabled: res.file_upload_enabled ?? fileUploadEnabled,
fileDownloadEnabled: res.file_download_enabled ?? fileDownloadEnabled,
commandPolicyEnabled:
res.command_policy_enabled ?? commandPolicyEnabled,
approvalRequired: res.approval_required ?? approvalRequired,
});
const total = res.cleanup?.total ?? 0;
toast.success(
total > 0
@@ -53,7 +111,9 @@ export function useAuditSettings() {
: `已应用 ${days} 天保留策略,暂无超期记录需清理`,
);
} catch (e: unknown) {
toast.error(e instanceof Error ? e.message : i18n.t('kubernetes.msg.applyFailed'));
toast.error(
e instanceof Error ? e.message : i18n.t("kubernetes.msg.applyFailed"),
);
} finally {
setApplying(false);
}
@@ -61,13 +121,20 @@ export function useAuditSettings() {
const lastCleanupText = (() => {
if (!settings?.last_cleanup_at) return null;
const t = new Date(settings.last_cleanup_at).toLocaleString('zh-CN', { hour12: false });
let detail = '';
const t = new Date(settings.last_cleanup_at).toLocaleString("zh-CN", {
hour12: false,
});
let detail = "";
if (settings.last_cleanup_summary) {
try {
const s = JSON.parse(settings.last_cleanup_summary) as { total?: number };
if (typeof s.total === 'number') {
detail = s.total > 0 ? `,清理 ${s.total} 条` : i18n.t('system.useauditsetting.s7d55bd57');
const s = JSON.parse(settings.last_cleanup_summary) as {
total?: number;
};
if (typeof s.total === "number") {
detail =
s.total > 0
? `,清理 ${s.total} 条`
: i18n.t("system.useauditsetting.s7d55bd57");
}
} catch {
// ignore
@@ -81,10 +148,26 @@ export function useAuditSettings() {
setRetentionDays,
recordAll,
setRecordAll,
sessionWatermarkEnabled,
setSessionWatermarkEnabled,
sessionRecordEnabled,
setSessionRecordEnabled,
clipboardEnabled,
setClipboardEnabled,
fileUploadEnabled,
setFileUploadEnabled,
fileDownloadEnabled,
setFileDownloadEnabled,
commandPolicyEnabled,
setCommandPolicyEnabled,
approvalRequired,
setApprovalRequired,
loading,
applying,
handleApply,
lastCleanupText,
retentionOptions: settings?.allowed_retention_days ?? [...RETENTION_OPTIONS],
retentionOptions: settings?.allowed_retention_days ?? [
...RETENTION_OPTIONS,
],
};
}
}