feat: 完善堡垒机与 Agent 智能运维能力
- 支持 Windows Agent 构建、安装脚本和统一上报协议 - 增强 RDP 剪贴板、虚拟盘和文件传输能力 - 新增 AIOps 上下文工具、告警 RCA trace 落库和回放接口 - 重构 docs 模块文档并补充终端、堡垒机、监控、AI Agent 设计
This commit is contained in:
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';
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user