Files
aiops/frontend/src/components/system/SystemManager.tsx
T
attacker c6e0d6ca57 refactor(i18n): U42 残余硬编码中文彻底清零 — 33文件54处→0, zh/en 7847→7898对齐
全量 JSX children + toast/confirm + sr-only 描述硬编码中文清零:
- Kubernetes模块: UpdateStrategyCard maxSurge/maxUnavailable说明 + IngressCreateForm host域名/Service端口 + ServiceCreateForm 端口配置/selector + DeploymentEditService 无selector/个关联/暂无Service/Ingress + ClusterOverviewPage 核 + KubernetesClusterDialog 已配置 + KubectlTerminal/ResourceVisualEditor/CreateHPADialog/DeploymentEditContainers/Storage + PodInstancesTable/UpdateStrategySelectorCards
- AI模块: PredictiveCapacityTab AI深度解读/紧急/警告/7天预测/预计N天 + PredictiveCostTab AI深度解读 + event-handler.ts 高危操作确认toast
- Deploy模块: DeployCredentials 启用/禁用 + DeployRBAC 删除角色confirm + WorkspaceTermDialog + CredFormDialog Windows用户
- Monitor模块: AlertRulesTab 删除confirm/分钟 + PrometheusDatasourceManager/LogDatasourcePage 删除confirm + QueryHistoryPanel 条
- System模块: ServerFileBrowser/TerminalDialog/RdpAuditViewer sr-only + SystemManager/CredentialManager/TerminalSessionsTab 删除confirm + SecuritySettingsTab 30分钟/2小时
- Task模块: CronJobViewDialog 次

新增 61 翻译键 (k8s.editAdvanced 6 + k8s.editService 11 + k8s.cluster 2 + k8s.misc 9 + aiops.capacity 9 + aiops.cost 1 + aiops.cli 2 + deploy 4 + monitor 5 + system 10 + task 1 + common 2)
tsc -b=0, npm build✓ (17.71s), ESLint=0/43/any=0, go build=0, 17/17 测试通过
2026-07-14 00:51:36 +08:00

425 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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";
// 后端用户数据转换为前端兼容格式
function mapUser(u: any) {
return {
...u,
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",
};
}
const emptyForm: UserFormData = {
username: "",
nickname: "",
email: "",
phone: "",
role: "",
password: "",
status: 1 as number,
};
export function SystemManager() {
const { t } = useTranslation();
const confirm = useConfirm();
const location = useLocation();
// 侧边栏菜单路径映射到 Tab // Logan.Li
// 侧边栏已有独立菜单项,按路由直接渲染对应页面,不再重复 Tab // Logan.Li
const pathToTab: Record<string, "users" | "roles" | "settings"> = {
"/users": "users",
"/roles": "roles",
"/settings": "settings",
};
const activeTab = pathToTab[location.pathname] || "users";
const pageMeta: Record<
string,
{ title: string; subtitle: string; icon: React.ReactNode }
> = {
users: {
title: t("system.users"),
subtitle: t("system.localUserDesc"),
icon: <Users className="h-5 w-5" />,
},
roles: {
title: t("system.roles"),
subtitle: t("system.roleBindingHint"),
icon: <Shield className="h-5 w-5" />,
},
settings: {
title: t("system.settings"),
subtitle: t("system.generalSubtitle"),
icon: <Settings className="h-5 w-5" />,
},
};
const meta = pageMeta[activeTab];
const [searchQuery, setSearchQuery] = useState("");
const [users, setUsers] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [selectedUser, setSelectedUser] = useState<any | null>(null);
const [resetPasswordUser, setResetPasswordUser] = useState<any | null>(null);
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
// 创建/编辑用户弹窗
const [dialogOpen, setDialogOpen] = useState(false);
const [editingUser, setEditingUser] = useState<any | null>(null);
const [saving, setSaving] = useState(false);
const [form, setForm] = useState<UserFormData>({ ...emptyForm });
// CSV 批量导入弹窗
const [importDialogOpen, setImportDialogOpen] = useState<
"csv" | "ldap" | null
>(null);
const [importPreview, setImportPreview] = useState<any[]>([]);
const [importResult, setImportResult] = useState<any>(null);
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
const loadUsers = async () => {
setLoading(true);
try {
const res = await userApi.getUsers({ page: 1, page_size: 100 });
const rawUsers = res?.list ?? [];
setUsers(rawUsers.map(mapUser));
} catch (error) {
console.error("Failed to fetch users:", error);
setUsers([]);
} finally {
setLoading(false);
}
};
// LDAP 状态全部由 hook 管理(在 loadUsers 之后调用避免 TDZ) // Logan.Li
const ldap = useLdapManagement(loadUsers);
const audit = useAuditSettings();
useEffect(() => {
loadUsers();
ldap.loadLdapConfig();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// LDAP 导入 Dialog 打开时加载 OU 和用户
useEffect(() => {
if (importDialogOpen === "ldap") {
ldap.loadImportDialogData();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [importDialogOpen]);
const filteredUsers = users.filter(
(u) =>
(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"
>
<CheckCircle className="h-3 w-3 mr-1" />
{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"
>
<XCircle className="h-3 w-3 mr-1" />
{t("system.userInactive")}
</Badge>
);
};
const openCreate = () => {
setEditingUser(null);
setForm({ ...emptyForm });
setDialogOpen(true);
};
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,
});
setDialogOpen(true);
};
const handleSubmit = async () => {
setSaving(true);
try {
const payload: Record<string, unknown> = {
username: form.username,
nickname: form.nickname,
email: form.email,
phone: form.phone,
role: form.role,
status: form.status,
};
if (form.password) {
payload.password = form.password;
}
if (editingUser) {
await userApi.updateUser(editingUser.id, payload);
} else {
await userApi.createUser(payload);
}
setDialogOpen(false);
await loadUsers();
} catch (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: t('system.deleteUserConfirmDesc', { name: 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"));
} catch (error: unknown) {
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"),
description: t("system.batchDeleteUsersConfirmDesc", { count: selectedUserIds.length }),
confirmText: t("system.terminalBatchDelete"),
variant: "destructive",
});
if (!ok) return;
try {
const res = (await userApi.batchDelete(selectedUserIds)) as {
message?: string;
};
toast.success(res?.message || t('system.batchDeleteUsers', { count: selectedUserIds.length }));
setSelectedUserIds([]);
await loadUsers();
} catch (error: unknown) {
toast.error(
error instanceof Error ? error.message : t("cloud.batchDeleteFailed"),
);
}
};
// CSV 文件解析
const handleCsvUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
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"));
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 parsed: Record<string, unknown>[] = [];
for (let i = 1; i < lines.length; i++) {
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] : "",
});
}
setImportPreview(parsed);
setImportResult(null);
};
reader.readAsText(file, "UTF-8");
};
// 执行 CSV 批量导入
const handleBatchImport = async () => {
ldap.setImporting(true);
try {
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"),
});
} finally {
ldap.setImporting(false);
}
};
return (
<PageShell
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 === "roles" && (
<SystemRolesTab
selectedRole={selectedRole}
setSelectedRole={setSelectedRole}
/>
)}
{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}
auditLastCleanupText={audit.lastCleanupText}
auditRetentionOptions={audit.retentionOptions}
/>
)}
</div>
<ResetPasswordDialog
user={resetPasswordUser}
onClose={() => setResetPasswordUser(null)}
/>
</PageShell>
);
}