fix(i18n): 回退到稳定的 32% 覆盖率状态 (tsc + Vite 构建通过)
批量转换脚本引入的语法错误已回退,保留稳定的 i18n 转换: - 翻译词典: 22 namespaces, 3600+ keys - 已接入: 145+ 文件 (Dashboard/Login/K8s 主页面/Monitor/System/Deploy) - 语言切换器在 Header 中可用 (🇨🇳/🇺🇸) - tsc 零错误, Vite 构建成功
This commit is contained in:
1 parent
ecec73989b
commit
72bfb0e12d
191 files changed
+5057
-7592
No files matched your search
@@ -24,14 +24,16 @@ import { ContentPanel } from '@/components/layout/ContentPanel';
|
||||
import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
import { StatStrip } from '@/components/ui/stat-strip';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
type CredentialType = 'ssh_key' | 'api_token' | 'password' | 'access_key';
|
||||
|
||||
const TYPE_CONFIG: Record<CredentialType, { label: string; icon: typeof Key; color: string }> = {
|
||||
ssh_key: { label: 'SSH 密钥', icon: Key, color: 'dg-text-yellow' },
|
||||
ssh_key: { label: i18n.t('system.sshKey'), icon: Key, color: 'dg-text-yellow' },
|
||||
api_token: { label: 'API Token', icon: Globe, color: 'dg-text-blue' },
|
||||
password: { label: '密码', icon: Shield, color: 'dg-text-green' },
|
||||
access_key: { label: 'AK密钥对', icon: Key, color: 'dg-text-purple' },
|
||||
password: { label: i18n.t('system.userPassword'), icon: Shield, color: 'dg-text-green' },
|
||||
access_key: { label: i18n.t('system.akPair'), icon: Key, color: 'dg-text-purple' },
|
||||
};
|
||||
|
||||
interface CredentialItem {
|
||||
@@ -47,6 +49,7 @@ interface CredentialItem {
|
||||
const EMPTY_FORM = { name: '', type: 'password' as CredentialType, content: '', description: '' };
|
||||
|
||||
export function CredentialManager() {
|
||||
const { t } = useTranslation();
|
||||
const confirm = useConfirm();
|
||||
const [credentials, setCredentials] = useState<CredentialItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -64,7 +67,7 @@ export function CredentialManager() {
|
||||
const res: JsonData = await credentialApi.list();
|
||||
setCredentials(res?.list || []);
|
||||
} catch (e) {
|
||||
console.error('加载凭据列表失败:', e);
|
||||
console.error(t('system.loadCredentialsFailed'), e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -102,15 +105,15 @@ export function CredentialManager() {
|
||||
...(formData.content ? { content: formData.content } : {}),
|
||||
description: formData.description,
|
||||
});
|
||||
toast.success('凭据已更新');
|
||||
toast.success(t('system.credentialUpdated'));
|
||||
} else {
|
||||
await credentialApi.create(formData);
|
||||
toast.success('凭据已创建');
|
||||
toast.success(t('system.credentialCreated'));
|
||||
}
|
||||
setFormOpen(false);
|
||||
fetchList();
|
||||
} catch (e) {
|
||||
toast.error(e?.message || '操作失败');
|
||||
toast.error(e?.message || t('common.operationFailed'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -119,18 +122,18 @@ export function CredentialManager() {
|
||||
const handleDelete = async (id: number) => {
|
||||
const item = credentials.find(c => c.id === id);
|
||||
const ok = await confirm({
|
||||
title: '删除凭据',
|
||||
title: t('pipeline.deleteCred'),
|
||||
description: `确定删除「${item?.name || id}」吗?删除后无法恢复,关联的服务器将无法使用此凭据连接。`,
|
||||
confirmText: '删除',
|
||||
confirmText: t('common.delete'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
await credentialApi.delete(id);
|
||||
toast.success('凭据已删除');
|
||||
toast.success(t('system.credentialDeleted'));
|
||||
fetchList();
|
||||
} catch {
|
||||
toast.error('删除失败');
|
||||
toast.error(t('common.deleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -158,20 +161,20 @@ export function CredentialManager() {
|
||||
return (
|
||||
<PageShell
|
||||
variant="list"
|
||||
title="凭证配置"
|
||||
subtitle="SSH 密钥、API Token、密码、AK密钥对等敏感信息统一加密管理"
|
||||
title={t('system.credentials')}
|
||||
subtitle={t('system.credentialMgmtDesc')}
|
||||
icon={<Key className="h-5 w-5" />}
|
||||
loading={loading}
|
||||
metrics={(
|
||||
<StatStrip items={[{ label: '凭据总数', value: credentials.length, color: 'blue' }]} />
|
||||
<StatStrip items={[{ label: t('system.credentialTotal'), value: credentials.length, color: 'blue' }]} />
|
||||
)}
|
||||
actions={(
|
||||
<>
|
||||
<Button variant="outline" className="dg-btn-secondary" onClick={fetchList} disabled={loading}>
|
||||
<RefreshCw className={cn('h-4 w-4 mr-2', loading && 'animate-spin')} />刷新
|
||||
<RefreshCw className={cn('h-4 w-4 mr-2', loading && 'animate-spin')} />{t('common.refresh')}
|
||||
</Button>
|
||||
<Button className="dg-btn-primary" onClick={openCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />添加凭据
|
||||
<Plus className="h-4 w-4 mr-2" />{t('system.addCredentialBtn')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -179,21 +182,21 @@ export function CredentialManager() {
|
||||
{!loading && credentials.length === 0 ? (
|
||||
<PageEmpty
|
||||
icon={<Key className="h-10 w-10" />}
|
||||
title="暂无凭据"
|
||||
description="点击「添加凭据」创建"
|
||||
action={<Button onClick={openCreate}><Plus className="h-4 w-4 mr-2" />添加凭据</Button>}
|
||||
title={t('pipeline.noCred')}
|
||||
description={t('system.noCredentialsHint')}
|
||||
action={<Button onClick={openCreate}><Plus className="h-4 w-4 mr-2" />{t('system.addCredentialBtn')}</Button>}
|
||||
/>
|
||||
) : (
|
||||
<ContentPanel noPadding>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">名称</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">类型</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">内容预览</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">描述</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">更新时间</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)] text-right">操作</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">{t('common.name')}</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">{t('system.permissionType')}</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">{t('system.contentPreview')}</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">{t('system.roleDesc')}</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">{t('common.updatedAt')}</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)] text-right">{t('system.auditAction')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -201,7 +204,7 @@ export function CredentialManager() {
|
||||
<TableRow className="border-[var(--dg-border)]">
|
||||
<TableCell colSpan={6} className="text-center text-[var(--dg-text-muted)] py-12">
|
||||
<RefreshCw className="h-5 w-5 animate-spin mx-auto mb-2" />
|
||||
加载中...
|
||||
{t('common.loading')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : credentials.length === 0 ? (
|
||||
@@ -263,9 +266,9 @@ export function CredentialManager() {
|
||||
<Dialog open={formOpen} onOpenChange={setFormOpen}>
|
||||
<DialogContent className="max-w-lg bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingId ? '编辑凭据' : '添加凭据'}</DialogTitle>
|
||||
<DialogTitle>{editingId ? t('pipeline.editCred') : t('system.addCredentialBtn')}</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
{editingId ? '编辑凭据信息,留空内容不更新' : '添加新的凭据'}
|
||||
{editingId ? t('system.editCredentialInfo') : t('system.addNewCredential')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -273,10 +276,10 @@ export function CredentialManager() {
|
||||
{/* 名称 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-1">
|
||||
名称 <span className="dg-text-red">*</span>
|
||||
{t('common.name')} <span className="dg-text-red">*</span>
|
||||
</label>
|
||||
<Input
|
||||
placeholder="如: 生产环境SSH密钥"
|
||||
placeholder={t('system.credentialNamePlaceholder')}
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]"
|
||||
@@ -285,7 +288,7 @@ export function CredentialManager() {
|
||||
|
||||
{/* 类型 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-1">类型</label>
|
||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-1">{t('system.permissionType')}</label>
|
||||
<Select
|
||||
value={formData.type}
|
||||
onValueChange={(v) => setFormData({ ...formData, type: v as CredentialType })}
|
||||
@@ -296,7 +299,7 @@ export function CredentialManager() {
|
||||
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
<SelectItem value="ssh_key">
|
||||
<span className="flex items-center gap-2">
|
||||
<Key className="h-4 w-4 dg-text-yellow" /> SSH 密钥
|
||||
<Key className="h-4 w-4 dg-text-yellow" /> {t('system.sshKey')}
|
||||
</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="api_token">
|
||||
@@ -306,12 +309,12 @@ export function CredentialManager() {
|
||||
</SelectItem>
|
||||
<SelectItem value="password">
|
||||
<span className="flex items-center gap-2">
|
||||
<Shield className="h-4 w-4 dg-text-green" /> 密码
|
||||
<Shield className="h-4 w-4 dg-text-green" /> {t('system.userPassword')}
|
||||
</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="access_key">
|
||||
<span className="flex items-center gap-2">
|
||||
<Key className="h-4 w-4 dg-text-purple" /> AK密钥对
|
||||
<Key className="h-4 w-4 dg-text-purple" /> {t('system.akPair')}
|
||||
</span>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -323,7 +326,7 @@ export function CredentialManager() {
|
||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-1">
|
||||
{formData.type === 'ssh_key' ? 'SSH 私钥' : formData.type === 'api_token' ? 'Token' : formData.type === 'access_key' ? 'AK/SK (每行一对,用空格或逗号分隔)' : '密码'}
|
||||
{!editingId && <span className="dg-text-red"> *</span>}
|
||||
{editingId && <span className="text-xs text-[var(--dg-text-muted)] ml-1">(留空不更新)</span>}
|
||||
{editingId && <span className="text-xs text-[var(--dg-text-muted)] ml-1">{t('system.leaveBlankNoUpdate')}</span>}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
@@ -334,7 +337,7 @@ export function CredentialManager() {
|
||||
? '***'
|
||||
: formData.type === 'access_key'
|
||||
? 'AKID*** SK***\nAKID*** SK***'
|
||||
: '输入密码'
|
||||
: t('system.enterPassword')
|
||||
}
|
||||
value={formData.content}
|
||||
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
|
||||
@@ -345,7 +348,7 @@ export function CredentialManager() {
|
||||
size="icon"
|
||||
className="absolute top-2 right-2 h-7 w-7 text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]"
|
||||
onClick={() => setShowContent(!showContent)}
|
||||
title={showContent ? '隐藏' : '显示'}
|
||||
title={showContent ? t('common.hidden') : '显示'}
|
||||
>
|
||||
{showContent ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</Button>
|
||||
@@ -354,9 +357,9 @@ export function CredentialManager() {
|
||||
|
||||
{/* 描述 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-1">描述</label>
|
||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-1">{t('system.roleDesc')}</label>
|
||||
<Input
|
||||
placeholder="可选备注"
|
||||
placeholder={t('monitor.silenceDialog.descPlaceholder')}
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]"
|
||||
@@ -374,14 +377,14 @@ export function CredentialManager() {
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-[var(--dg-border)]" onClick={() => setFormOpen(false)}>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
|
||||
onClick={handleSave}
|
||||
disabled={saving || !formData.name}
|
||||
>
|
||||
{saving ? '保存中...' : editingId ? '保存' : '创建'}
|
||||
{saving ? t('k8s.topology.saving') : editingId ? t('common.save') : t('common.create')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { Upload, Download, CheckCircle, XCircle } from 'lucide-react';
|
||||
import { userApi } from '@/api';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface CsvImportDialogProps {
|
||||
importDialogOpen: 'csv' | 'ldap' | null;
|
||||
@@ -44,6 +45,7 @@ export function CsvImportDialog({
|
||||
handleCsvUpload,
|
||||
handleBatchImport,
|
||||
}: CsvImportDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Dialog open={importDialogOpen === 'csv'} onOpenChange={(v) => !v && setImportDialogOpen(null)}>
|
||||
<DialogContent className="dg-card max-w-2xl">
|
||||
@@ -63,7 +65,7 @@ export function CsvImportDialog({
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => userApi.downloadTemplate()}>
|
||||
<Download className="h-4 w-4 mr-1.5" />
|
||||
下载模板
|
||||
{t('system.downloadTemplate')}
|
||||
</Button>
|
||||
<label className="inline-flex items-center px-3 py-1.5 rounded-md text-sm bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] text-[var(--dg-text-primary)] cursor-pointer hover:bg-[var(--dg-bg-hover)] transition-colors">
|
||||
<Upload className="h-4 w-4 mr-1.5" />
|
||||
@@ -80,10 +82,10 @@ export function CsvImportDialog({
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-[var(--dg-border)]">
|
||||
<TableHead className="dg-text-secondary text-xs">用户名</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs">昵称</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs">邮箱</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs">手机</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs">{t('system.userName')}</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs">{t('system.userNickname')}</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs">{t('system.userEmail')}</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs">{t('monitor.contactPhone')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -110,10 +112,10 @@ export function CsvImportDialog({
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => { setImportDialogOpen(null); setImportPreview([]); }}>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleBatchImport} disabled={importing || importPreview.length === 0}>
|
||||
{importing ? '导入中...' : `确认导入 (${importPreview.length})`}
|
||||
{importing ? t('system.importingDot') : `确认导入 (${importPreview.length})`}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
@@ -136,7 +138,7 @@ export function CsvImportDialog({
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={() => { setImportDialogOpen(null); setImportPreview([]); setImportResult(null); }}>
|
||||
完成
|
||||
{t('common.finish')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { Network, Loader2, RefreshCw, CheckCircle, XCircle } from 'lucide-react';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface LdapImportDialogProps {
|
||||
importDialogOpen: 'csv' | 'ldap' | null;
|
||||
@@ -55,6 +56,7 @@ export function LdapImportDialog({
|
||||
importResult,
|
||||
setImportResult,
|
||||
}: LdapImportDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Dialog open={importDialogOpen === 'ldap'} onOpenChange={(v) => !v && setImportDialogOpen(null)}>
|
||||
<DialogContent className="dg-card max-w-6xl w-[calc(100vw-2rem)] max-h-[90vh] overflow-hidden flex flex-col">
|
||||
@@ -73,7 +75,7 @@ export function LdapImportDialog({
|
||||
<div className="space-y-3 flex-1 min-h-0 overflow-hidden flex flex-col">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs dg-text-muted whitespace-nowrap">选择 OU</span>
|
||||
<span className="text-xs dg-text-muted whitespace-nowrap">{t('system.selectOuLabel')}</span>
|
||||
<select
|
||||
className="bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] text-[var(--dg-text-primary)] text-xs rounded-md px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-[var(--dg-blue)]"
|
||||
value={ldapImportOUDN}
|
||||
@@ -82,7 +84,7 @@ export function LdapImportDialog({
|
||||
loadLdapUsers(e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="">根 (全部)</option>
|
||||
<option value="">{t('system.rootAll')}</option>
|
||||
{ldapImportOUs.map((ou) => (
|
||||
<option key={ou.dn} value={ou.dn}>{ou.name}</option>
|
||||
))}
|
||||
@@ -95,7 +97,7 @@ export function LdapImportDialog({
|
||||
</div>
|
||||
|
||||
<p className="text-xs dg-text-muted">
|
||||
{ldapUsers.length > 0 ? `找到 ${ldapUsers.length} 个用户` : '选择 OU 或点击刷新获取用户列表'}
|
||||
{ldapUsers.length > 0 ? `找到 ${ldapUsers.length} 个用户` : t('system.selectOuHint')}
|
||||
</p>
|
||||
|
||||
{ldapUsers.length > 0 && (
|
||||
@@ -117,9 +119,9 @@ export function LdapImportDialog({
|
||||
}}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs w-[100px]">用户名</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs w-[120px]">显示名</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs w-[160px]">邮箱</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs w-[100px]">{t('system.userName')}</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs w-[120px]">{t('system.userDisplayName')}</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs w-[160px]">{t('system.userEmail')}</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs">DN</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -153,10 +155,10 @@ export function LdapImportDialog({
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => { setImportDialogOpen(null); setLdapUsers([]); }}>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleLdapImport} disabled={importing || ldapSelected.size === 0}>
|
||||
{importing ? '导入中...' : `导入选中 (${ldapSelected.size})`}
|
||||
{importing ? t('system.importingDot') : `导入选中 (${ldapSelected.size})`}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
@@ -172,7 +174,7 @@ export function LdapImportDialog({
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={() => { setImportDialogOpen(null); setLdapUsers([]); setImportResult(null); }}>
|
||||
完成
|
||||
{t('common.finish')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface LdapManagementSectionProps {
|
||||
ldapConfig?: JsonData;
|
||||
@@ -89,6 +90,7 @@ interface LdapManagementSectionProps {
|
||||
}
|
||||
|
||||
export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
ldapConfig,
|
||||
ldapMgmtOpen,
|
||||
@@ -148,7 +150,7 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
>
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${ldapMgmtOpen ? '' : '-rotate-90'}`} />
|
||||
<Users className="h-4 w-4 dg-text-yellow" />
|
||||
<span className="text-[var(--dg-text-secondary)]">LDAP 账号管理</span>
|
||||
<span className="text-[var(--dg-text-secondary)]">{t('system.ldapAccountMgmt')}</span>
|
||||
</button>
|
||||
|
||||
{ldapMgmtOpen && (
|
||||
@@ -159,17 +161,17 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="dg-text-secondary hover:text-[var(--dg-text-primary)]">
|
||||
<Key className="h-3.5 w-3.5 mr-1" />
|
||||
账号验证
|
||||
{t('system.ldapAccountVerify')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md dg-card">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">LDAP 账号验证测试</DialogTitle>
|
||||
<DialogDescription className="dg-text-muted">输入 LDAP 用户凭据进行认证测试</DialogDescription>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">{t('system.ldapAccountTestTitle')}</DialogTitle>
|
||||
<DialogDescription className="dg-text-muted">{t('system.ldapAccountTestDesc')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 py-2">
|
||||
<div>
|
||||
<label className="text-xs dg-text-muted mb-1 block">用户名</label>
|
||||
<label className="text-xs dg-text-muted mb-1 block">{t('system.userName')}</label>
|
||||
<Input
|
||||
value={ldapTestUser}
|
||||
onChange={(e) => setLdapTestUser(e.target.value)}
|
||||
@@ -178,7 +180,7 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs dg-text-muted mb-1 block">密码</label>
|
||||
<label className="text-xs dg-text-muted mb-1 block">{t('system.userPassword')}</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={ldapTestPass}
|
||||
@@ -210,13 +212,13 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
|
||||
<Button variant="ghost" size="sm" className="dg-text-secondary hover:text-[var(--dg-text-primary)]" onClick={loadLdapOUs}>
|
||||
<RefreshCw className="h-3.5 w-3.5 mr-1" />
|
||||
刷新
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
|
||||
{/* OU 选择 */}
|
||||
<Select value={selectedOUDN} onValueChange={(v) => { setSelectedOUDN(v); loadLdapOUUsers(v); }}>
|
||||
<SelectTrigger className="w-60 h-7 dg-input text-xs">
|
||||
<SelectValue placeholder="选择组织单元" />
|
||||
<SelectValue placeholder={t('system.selectOu')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input">
|
||||
<SelectItem value={ldapConfig.base_dn}>根 ({ldapConfig.base_dn})</SelectItem>
|
||||
@@ -233,26 +235,26 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="dg-text-secondary hover:text-[var(--dg-text-primary)]">
|
||||
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||
新建 OU
|
||||
{t('system.createOu')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md dg-card">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">新建组织单元</DialogTitle>
|
||||
<DialogDescription className="dg-text-muted">在 LDAP 目录中创建新的组织单元 (OU)</DialogDescription>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">{t('system.createOuTitle')}</DialogTitle>
|
||||
<DialogDescription className="dg-text-muted">{t('system.createOuDesc')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 py-2">
|
||||
<div>
|
||||
<label className="text-xs dg-text-muted mb-1 block">OU 名称 *</label>
|
||||
<label className="text-xs dg-text-muted mb-1 block">{t('system.ouNameRequired')}</label>
|
||||
<Input value={ouForm.ou_name} onChange={(e) => setOuForm({ ...ouForm, ou_name: e.target.value })} placeholder="dev" className="dg-input" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs dg-text-muted mb-1 block">父级 DN</label>
|
||||
<label className="text-xs dg-text-muted mb-1 block">{t('system.parentDn')}</label>
|
||||
<Input value={ouForm.parent_dn} onChange={(e) => setOuForm({ ...ouForm, parent_dn: e.target.value })} placeholder={ldapConfig.base_dn} className="dg-input" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs dg-text-muted mb-1 block">描述</label>
|
||||
<Input value={ouForm.description} onChange={(e) => setOuForm({ ...ouForm, description: e.target.value })} placeholder="开发团队" className="dg-input" />
|
||||
<label className="text-xs dg-text-muted mb-1 block">{t('system.roleDesc')}</label>
|
||||
<Input value={ouForm.description} onChange={(e) => setOuForm({ ...ouForm, description: e.target.value })} placeholder={t('system.devTeamPlaceholder')} className="dg-input" />
|
||||
</div>
|
||||
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={handleCreateOU} disabled={!ouForm.ou_name || ldapSaving}>
|
||||
{ldapSaving ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : null}
|
||||
@@ -267,33 +269,33 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="dg-text-secondary hover:text-[var(--dg-text-primary)]">
|
||||
<UserPlus className="h-3.5 w-3.5 mr-1" />
|
||||
添加用户
|
||||
{t('system.addUser')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md dg-card">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">添加 LDAP 用户</DialogTitle>
|
||||
<DialogDescription className="dg-text-muted">在 LDAP 目录中创建新的用户账号</DialogDescription>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">{t('system.addLdapUser')}</DialogTitle>
|
||||
<DialogDescription className="dg-text-muted">{t('system.addLdapUserDesc')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 py-2">
|
||||
<div>
|
||||
<label className="text-xs dg-text-muted mb-1 block">用户名 *</label>
|
||||
<label className="text-xs dg-text-muted mb-1 block">{t('system.userNameRequired')}</label>
|
||||
<Input value={newUser.username} onChange={(e) => setNewUser({ ...newUser, username: e.target.value })} placeholder="zhangsan" className="dg-input" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs dg-text-muted mb-1 block">密码 *</label>
|
||||
<label className="text-xs dg-text-muted mb-1 block">{t('system.passwordRequired')}</label>
|
||||
<Input type="password" value={newUser.password} onChange={(e) => setNewUser({ ...newUser, password: e.target.value })} placeholder="••••••" className="dg-input" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs dg-text-muted mb-1 block">显示名称</label>
|
||||
<label className="text-xs dg-text-muted mb-1 block">{t('system.userDisplayName')}</label>
|
||||
<Input value={newUser.display_name} onChange={(e) => setNewUser({ ...newUser, display_name: e.target.value })} placeholder="张三" className="dg-input" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs dg-text-muted mb-1 block">邮箱</label>
|
||||
<label className="text-xs dg-text-muted mb-1 block">{t('system.userEmail')}</label>
|
||||
<Input value={newUser.email} onChange={(e) => setNewUser({ ...newUser, email: e.target.value })} placeholder="zs@example.com" className="dg-input" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs dg-text-muted mb-1 block">目标 OU</label>
|
||||
<label className="text-xs dg-text-muted mb-1 block">{t('system.targetOu')}</label>
|
||||
<Input value={newUser.ou_dn || selectedOUDN || ldapConfig.base_dn} onChange={(e) => setNewUser({ ...newUser, ou_dn: e.target.value })} className="dg-input" />
|
||||
</div>
|
||||
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={handleCreateLDAPUser} disabled={!newUser.username || !newUser.password || ldapSaving}>
|
||||
@@ -321,14 +323,14 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
<Dialog open={importResultOpen} onOpenChange={setImportResultOpen}>
|
||||
<DialogContent className="max-w-sm dg-card">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">导入结果</DialogTitle>
|
||||
<DialogDescription className="dg-text-muted">LDAP 用户导入到本地用户库的执行结果</DialogDescription>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">{t('system.importResult')}</DialogTitle>
|
||||
<DialogDescription className="dg-text-muted">{t('system.ldapImportResultDesc')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-3 text-sm text-[var(--dg-text-secondary)]">
|
||||
{importFeedback}
|
||||
</div>
|
||||
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={() => setImportResultOpen(false)}>
|
||||
确定
|
||||
{t('common.ok')}
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -347,18 +349,18 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
onChange={(e) => setSelectedLDAPUsers(e.target.checked ? ldapUsers.map((u: JsonData) => u.dn) : [])}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="dg-text-secondary">用户名</TableHead>
|
||||
<TableHead className="dg-text-secondary">显示名称</TableHead>
|
||||
<TableHead className="dg-text-secondary">邮箱</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('system.userName')}</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('system.userDisplayName')}</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('system.userEmail')}</TableHead>
|
||||
<TableHead className="dg-text-secondary">DN</TableHead>
|
||||
<TableHead className="dg-text-secondary w-12">操作</TableHead>
|
||||
<TableHead className="dg-text-secondary w-12">{t('system.auditAction')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ldapUsers.length === 0 ? (
|
||||
<TableRow className="border-[var(--dg-border)]">
|
||||
<TableCell colSpan={6} className="text-center dg-text-muted py-8">
|
||||
点击「刷新」加载 LDAP 用户
|
||||
{t('system.ldapClickRefresh')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
@@ -422,7 +424,7 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Edit className="h-5 w-5 dg-text-blue" />
|
||||
编辑 LDAP 用户
|
||||
{t('system.editLdapUser')}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="dg-text-muted">
|
||||
修改 {editLdapUser?.uid} 的属性
|
||||
@@ -431,30 +433,30 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
{editLdapUser && (
|
||||
<div className="space-y-3 py-2">
|
||||
<div>
|
||||
<label className="text-xs dg-text-muted mb-1 block">用户名(只读)</label>
|
||||
<label className="text-xs dg-text-muted mb-1 block">{t('system.userNameReadonly')}</label>
|
||||
<Input value={editLdapUser.uid || ''} disabled className="dg-input dg-text-muted" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs dg-text-muted mb-1 block">显示名称 (cn)</label>
|
||||
<label className="text-xs dg-text-muted mb-1 block">{t('system.displayNameCn')}</label>
|
||||
<Input value={editLdapForm.cn} onChange={(e) => setEditLdapForm({ ...editLdapForm, cn: e.target.value })} className="dg-input" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs dg-text-muted mb-1 block">邮箱</label>
|
||||
<label className="text-xs dg-text-muted mb-1 block">{t('system.userEmail')}</label>
|
||||
<Input value={editLdapForm.mail} onChange={(e) => setEditLdapForm({ ...editLdapForm, mail: e.target.value })} placeholder="user@example.com" className="dg-input" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs dg-text-muted mb-1 block">电话</label>
|
||||
<label className="text-xs dg-text-muted mb-1 block">{t('system.phoneLabel')}</label>
|
||||
<Input value={editLdapForm.phone} onChange={(e) => setEditLdapForm({ ...editLdapForm, phone: e.target.value })} placeholder="13800138000" className="dg-input" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs dg-text-muted mb-1 block">职位/头衔</label>
|
||||
<label className="text-xs dg-text-muted mb-1 block">{t('system.jobTitle')}</label>
|
||||
<Input value={editLdapForm.title} onChange={(e) => setEditLdapForm({ ...editLdapForm, title: e.target.value })} placeholder="运维工程师" className="dg-input" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => setEditLdapUser(null)}>取消</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleUpdateLdapUser}>保存</Button>
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => setEditLdapUser(null)}>{t('common.cancel')}</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleUpdateLdapUser}>{t('common.save')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Loader2, Maximize2, Minimize2, Monitor, X } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { attachTerminalResize, mountXterm } from '@/lib/xterm-utils';
|
||||
import { useUserStore } from '@/store';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface SessionInfo {
|
||||
session_id: number;
|
||||
@@ -32,6 +33,7 @@ interface LiveMonitorProps {
|
||||
}
|
||||
|
||||
export function LiveMonitorDialog({ open, onOpenChange, sessionId, sessionInfo }: LiveMonitorProps) {
|
||||
const { t } = useTranslation();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const termRef = useRef<XTerm | null>(null);
|
||||
const fitRef = useRef<FitAddon | null>(null);
|
||||
@@ -124,11 +126,11 @@ export function LiveMonitorDialog({ open, onOpenChange, sessionId, sessionInfo }
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = () => setConnectError('WebSocket 连接失败');
|
||||
ws.onerror = () => setConnectError(t('aiops.wsConnectFailed'));
|
||||
|
||||
ws.onclose = (e) => {
|
||||
if (!didOpen) {
|
||||
setConnectError(`连接失败 (code=${e.code}): ${e.reason || '认证失败或网络问题'}`);
|
||||
setConnectError(`连接失败 (code=${e.code}): ${e.reason || t('aiops.authOrNetwork')}`);
|
||||
}
|
||||
setConnected(false);
|
||||
term.writeln('\r\n\x1b[33m[审计监控] 连接已断开\x1b[0m');
|
||||
@@ -136,7 +138,7 @@ export function LiveMonitorDialog({ open, onOpenChange, sessionId, sessionInfo }
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
if (!didOpen) {
|
||||
setConnectError('连接超时 — 请确认已登录且有活跃会话');
|
||||
setConnectError(t('aiops.connectTimeoutActive'));
|
||||
ws.close();
|
||||
}
|
||||
}, 5000);
|
||||
@@ -194,14 +196,14 @@ export function LiveMonitorDialog({ open, onOpenChange, sessionId, sessionInfo }
|
||||
: { maxWidth: '960px', width: '92vw' }
|
||||
}
|
||||
>
|
||||
<DialogTitle className="sr-only">终端实时监控</DialogTitle>
|
||||
<DialogDescription className="sr-only">审计在线 WebSSH 会话</DialogDescription>
|
||||
<DialogTitle className="sr-only">{t('system.terminalLiveMonitor')}</DialogTitle>
|
||||
<DialogDescription className="sr-only">{t('system.auditWebssh')}</DialogDescription>
|
||||
|
||||
{/* 工具栏 — 与 TerminalDialog 一致 // Logan.Li */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-[var(--dg-border)]">
|
||||
<div className="flex items-center gap-2 text-sm min-w-0 flex-wrap">
|
||||
<Monitor className="h-4 w-4 dg-text-green shrink-0" />
|
||||
<span className="text-[var(--dg-text-primary)] font-medium">终端实时监控</span>
|
||||
<span className="text-[var(--dg-text-primary)] font-medium">{t('system.terminalLiveMonitor')}</span>
|
||||
{meta && (
|
||||
<span className="text-[var(--dg-text-muted)] truncate">
|
||||
{meta.username} · {meta.server_name} ({meta.server_ip}) · {formatTime(meta.start_time)}
|
||||
@@ -215,12 +217,12 @@ export function LiveMonitorDialog({ open, onOpenChange, sessionId, sessionInfo }
|
||||
{!connected && !connectError && (
|
||||
<Badge variant="outline" className="border-yellow-500/30 dg-text-yellow bg-yellow-500/10 text-xs">
|
||||
<Loader2 className="h-3 w-3 animate-spin mr-1" />
|
||||
连接中
|
||||
{t('common.connecting')}
|
||||
</Badge>
|
||||
)}
|
||||
{connectError && (
|
||||
<Badge variant="outline" className="border-red-500/30 dg-text-red bg-red-500/10 text-xs">
|
||||
连接失败
|
||||
{t('system.ldapTestFailed')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -229,7 +231,7 @@ export function LiveMonitorDialog({ open, onOpenChange, sessionId, sessionInfo }
|
||||
type="button"
|
||||
onClick={() => setFullscreen(!fullscreen)}
|
||||
className="p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)]"
|
||||
title={fullscreen ? '退出全屏' : '全屏'}
|
||||
title={fullscreen ? t('common.exitFullscreen') : t('common.fullscreen')}
|
||||
>
|
||||
{fullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
|
||||
</button>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { Key, Loader2 } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { userApi } from '@/api';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ResetPasswordDialogProps {
|
||||
user: { id: number | string; username?: string; nickname?: string; authSource?: string } | null;
|
||||
@@ -16,6 +17,7 @@ interface ResetPasswordDialogProps {
|
||||
}
|
||||
|
||||
export function ResetPasswordDialog({ user, onClose }: ResetPasswordDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirm, setConfirm] = useState('');
|
||||
@@ -29,12 +31,12 @@ export function ResetPasswordDialog({ user, onClose }: ResetPasswordDialogProps)
|
||||
setSaving(true);
|
||||
try {
|
||||
await userApi.resetUserPassword(user.id, password);
|
||||
toast({ title: '密码已重置', description: `用户 ${user.nickname || user.username} 的新密码已生效` });
|
||||
toast({ title: t('system.passwordReset'), description: `用户 ${user.nickname || user.username} 的新密码已生效` });
|
||||
setPassword('');
|
||||
setConfirm('');
|
||||
onClose();
|
||||
} catch (e: any) {
|
||||
toast({ title: '重置失败', description: e?.message || String(e), variant: 'destructive' });
|
||||
toast({ title: t('system.resetFailed'), description: e?.message || String(e), variant: 'destructive' });
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -46,7 +48,7 @@ export function ResetPasswordDialog({ user, onClose }: ResetPasswordDialogProps)
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Key className="h-5 w-5 text-[var(--dg-blue)]" />
|
||||
重置密码
|
||||
{t('system.resetPassword')}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-muted)]">
|
||||
{user ? `为「${user.nickname || user.username}」设置新密码` : ''}
|
||||
@@ -54,26 +56,26 @@ export function ResetPasswordDialog({ user, onClose }: ResetPasswordDialogProps)
|
||||
</DialogHeader>
|
||||
|
||||
{isLdap ? (
|
||||
<p className="text-sm dg-text-yellow/90 py-2">LDAP 用户请在 LDAP 目录中管理密码,不支持本地重置。</p>
|
||||
<p className="text-sm dg-text-yellow/90 py-2">{t('system.ldapResetHint')}</p>
|
||||
) : (
|
||||
<div className="space-y-3 py-1">
|
||||
<div>
|
||||
<Label className="text-xs text-[var(--dg-text-muted)]">新密码</Label>
|
||||
<Label className="text-xs text-[var(--dg-text-muted)]">{t('system.newPassword')}</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
placeholder="至少 6 位"
|
||||
placeholder={t('system.passwordMinLen')}
|
||||
className="mt-1 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-[var(--dg-text-muted)]">确认密码</Label>
|
||||
<Label className="text-xs text-[var(--dg-text-muted)]">{t('system.userConfirmPassword')}</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={confirm}
|
||||
onChange={e => setConfirm(e.target.value)}
|
||||
placeholder="再次输入新密码"
|
||||
placeholder={t('system.confirmNewPassword')}
|
||||
className="mt-1 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]"
|
||||
/>
|
||||
</div>
|
||||
@@ -81,7 +83,7 @@ export function ResetPasswordDialog({ user, onClose }: ResetPasswordDialogProps)
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-[var(--dg-border)]" onClick={onClose}>取消</Button>
|
||||
<Button variant="outline" className="border-[var(--dg-border)]" onClick={onClose}>{t('common.cancel')}</Button>
|
||||
{!isLdap && (
|
||||
<Button onClick={handleReset} disabled={!canSubmit || saving} className="bg-[var(--dg-blue)] hover:opacity-90">
|
||||
{saving ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : null}
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { Shield } from 'lucide-react';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export interface RoleFormData {
|
||||
name: string;
|
||||
@@ -36,17 +38,17 @@ interface RoleFormDialogProps {
|
||||
|
||||
// 权限按模块分组
|
||||
const MODULE_GROUPS: { key: string; label: string; prefixes: string[] }[] = [
|
||||
{ key: 'dashboard', label: '📊 仪表盘', prefixes: ['dashboard:'] },
|
||||
{ key: 'server', label: '🖥️ 资产管理', prefixes: ['server:'] },
|
||||
{ key: 'cloud', label: '☁️ 云资源', prefixes: ['cloud:'] },
|
||||
{ key: 'dashboard', label: i18n.t('common.menuDashboard'), prefixes: ['dashboard:'] },
|
||||
{ key: 'server', label: i18n.t('common.menuAsset'), prefixes: ['server:'] },
|
||||
{ key: 'cloud', label: i18n.t('common.menuCloud'), prefixes: ['cloud:'] },
|
||||
{ key: 'k8s', label: '☸️ Kubernetes', prefixes: ['k8s:'] },
|
||||
{ key: 'deploy', label: '🚀 应用发布', prefixes: ['deploy:'] },
|
||||
{ key: 'monitor', label: '📡 监控告警', prefixes: ['monitor:', 'agent:'] },
|
||||
{ key: 'auto', label: '⚙️ 自动化运维', prefixes: ['auto:'] },
|
||||
{ key: 'deploy', label: i18n.t('common.menuApp'), prefixes: ['deploy:'] },
|
||||
{ key: 'monitor', label: i18n.t('common.menuMonitoring'), prefixes: ['monitor:', 'agent:'] },
|
||||
{ key: 'auto', label: i18n.t('common.menuAutomation'), prefixes: ['auto:'] },
|
||||
{ key: 'aiops', label: '🤖 AIOps智能', prefixes: ['aiops:'] },
|
||||
{ key: 'approval', label: '✅ 资源申请', prefixes: ['approval:'] },
|
||||
{ key: 'audit', label: '🔍 安全审计', prefixes: ['audit:'] },
|
||||
{ key: 'system', label: '🔧 系统管理', prefixes: ['system:'] },
|
||||
{ key: 'approval', label: i18n.t('common.menuApproval'), prefixes: ['approval:'] },
|
||||
{ key: 'audit', label: i18n.t('common.menuAudit'), prefixes: ['audit:'] },
|
||||
{ key: 'system', label: i18n.t('common.menuSystem'), prefixes: ['system:'] },
|
||||
];
|
||||
|
||||
function groupPermissions(perms: PermissionOption[]): { key: string; label: string; items: PermissionOption[] }[] {
|
||||
@@ -60,7 +62,7 @@ function groupPermissions(perms: PermissionOption[]): { key: string; label: stri
|
||||
const classified = new Set(grouped.flatMap(g => g.items.map(p => p.id)));
|
||||
const uncategorized = perms.filter(p => !classified.has(p.id));
|
||||
if (uncategorized.length > 0) {
|
||||
grouped.push({ key: 'other', label: '📌 其他', items: uncategorized });
|
||||
grouped.push({ key: 'other', label: i18n.t('common.menuOther'), items: uncategorized });
|
||||
}
|
||||
|
||||
return grouped;
|
||||
@@ -69,6 +71,7 @@ function groupPermissions(perms: PermissionOption[]): { key: string; label: stri
|
||||
export function RoleFormDialog({
|
||||
open, onOpenChange, editingRole, form, setForm, saving, onSubmit, permissions,
|
||||
}: RoleFormDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const togglePermission = (id: number, checked: boolean) => {
|
||||
setForm((prev) => {
|
||||
const set = new Set(prev.permission_ids);
|
||||
@@ -94,42 +97,42 @@ export function RoleFormDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Shield className="h-5 w-5 dg-text-purple" />
|
||||
{editingRole ? '编辑角色' : '新建角色'}
|
||||
{editingRole ? t('system.editRole') : t('pipeline.newRole')}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-secondary)]">
|
||||
{editingRole ? '修改角色信息与权限' : '创建新的系统角色并分配权限'}
|
||||
{editingRole ? t('system.roleManageDesc') : t('system.createRoleDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-[var(--dg-text-secondary)]">角色名称</Label>
|
||||
<Label className="text-xs text-[var(--dg-text-secondary)]">{t('system.roleName')}</Label>
|
||||
<Input
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||
placeholder="如:运维工程师"
|
||||
placeholder={t('system.roleNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-[var(--dg-text-secondary)]">角色编码</Label>
|
||||
<Label className="text-xs text-[var(--dg-text-secondary)]">{t('system.roleCode')}</Label>
|
||||
<Input
|
||||
value={form.code}
|
||||
onChange={(e) => setForm({ ...form, code: e.target.value })}
|
||||
disabled={!!editingRole}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] disabled:opacity-60"
|
||||
placeholder="如:ops_engineer"
|
||||
placeholder={t('system.roleCodePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-[var(--dg-text-secondary)]">描述</Label>
|
||||
<Label className="text-xs text-[var(--dg-text-secondary)]">{t('system.roleDesc')}</Label>
|
||||
<Textarea
|
||||
value={form.description}
|
||||
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] resize-none"
|
||||
placeholder="角色职责描述(可选)"
|
||||
placeholder={t('system.roleDutyDesc')}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
@@ -144,7 +147,7 @@ export function RoleFormDialog({
|
||||
</Label>
|
||||
<div className="max-h-[40vh] overflow-y-auto rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)] p-3 space-y-3">
|
||||
{grouped.length === 0 ? (
|
||||
<p className="text-xs text-[var(--dg-text-muted)] text-center py-4">暂无可分配权限</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] text-center py-4">{t('system.noPermsAvailable')}</p>
|
||||
) : (
|
||||
grouped.map(group => {
|
||||
const selectedInGroup = group.items.filter(p => form.permission_ids.includes(p.id)).length;
|
||||
@@ -165,7 +168,7 @@ export function RoleFormDialog({
|
||||
onClick={() => toggleGroup(group.items, !allSelected)}
|
||||
className="text-[10px] dg-text-blue hover:text-blue-300 transition-colors"
|
||||
>
|
||||
{allSelected ? '取消全选' : '全选'}
|
||||
{allSelected ? t('common.deselectAll') : t('system.selectAll')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-x-3 gap-y-1 pl-1">
|
||||
@@ -193,10 +196,10 @@ export function RoleFormDialog({
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={onSubmit} disabled={saving}>
|
||||
{saving ? '保存中...' : '保存'}
|
||||
{saving ? t('k8s.topology.saving') : t('common.save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { auditApi } from '@/api';
|
||||
import type { OperationLog } from '@/types';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface SystemLogsTabProps {
|
||||
searchQuery: string;
|
||||
@@ -39,6 +40,7 @@ function normalizeLog(raw: JsonData): OperationLog {
|
||||
}
|
||||
|
||||
export function SystemLogsTab({ searchQuery, setSearchQuery }: SystemLogsTabProps) {
|
||||
const { t } = useTranslation();
|
||||
const [logs, setLogs] = useState<OperationLog[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -69,11 +71,11 @@ export function SystemLogsTab({ searchQuery, setSearchQuery }: SystemLogsTabProp
|
||||
const getLogResultBadge = (result: OperationLog['result']) => {
|
||||
return result === 'success' ? (
|
||||
<Badge variant="outline" className="border-emerald-500/30 dg-text-green bg-emerald-500/10">
|
||||
成功
|
||||
{t('system.auditSuccess')}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="border-red-500/30 dg-text-red bg-red-500/10">
|
||||
失败
|
||||
{t('system.auditFailed')}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
@@ -85,7 +87,7 @@ export function SystemLogsTab({ searchQuery, setSearchQuery }: SystemLogsTabProp
|
||||
<div className="relative w-64">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 dg-text-muted" />
|
||||
<Input
|
||||
placeholder="搜索操作日志..."
|
||||
placeholder={t('audit.operations.searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 h-9 dg-input"
|
||||
@@ -97,7 +99,7 @@ export function SystemLogsTab({ searchQuery, setSearchQuery }: SystemLogsTabProp
|
||||
onClick={fetchLogs}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
刷新
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -112,19 +114,19 @@ export function SystemLogsTab({ searchQuery, setSearchQuery }: SystemLogsTabProp
|
||||
) : filteredLogs.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 dg-text-muted">
|
||||
<Search className="h-8 w-8 mb-3 opacity-40" />
|
||||
<p className="text-sm">暂无操作日志</p>
|
||||
<p className="text-sm">{t('audit.operations.noData')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
<TableHead className="dg-text-secondary">时间</TableHead>
|
||||
<TableHead className="dg-text-secondary">用户</TableHead>
|
||||
<TableHead className="dg-text-secondary">模块</TableHead>
|
||||
<TableHead className="dg-text-secondary">操作</TableHead>
|
||||
<TableHead className="dg-text-secondary">资源</TableHead>
|
||||
<TableHead className="dg-text-secondary">IP地址</TableHead>
|
||||
<TableHead className="dg-text-secondary">结果</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('system.auditTime')}</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('system.roleUsers')}</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('system.auditModule')}</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('system.auditAction')}</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('system.auditResource')}</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('server.csv.ip')}</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('system.auditResult')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
||||
@@ -15,6 +15,7 @@ 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) {
|
||||
@@ -39,6 +40,7 @@ const emptyForm: UserFormData = {
|
||||
};
|
||||
|
||||
export function SystemManager() {
|
||||
const { t } = useTranslation();
|
||||
const confirm = useConfirm();
|
||||
const location = useLocation();
|
||||
// 侧边栏菜单路径映射到 Tab // Logan.Li
|
||||
@@ -52,18 +54,18 @@ export function SystemManager() {
|
||||
|
||||
const pageMeta: Record<string, { title: string; subtitle: string; icon: React.ReactNode }> = {
|
||||
users: {
|
||||
title: '用户管理',
|
||||
subtitle: '本地与 LDAP 用户账号、状态与角色分配',
|
||||
title: t('system.users'),
|
||||
subtitle: t('system.localUserDesc'),
|
||||
icon: <Users className="h-5 w-5" />,
|
||||
},
|
||||
roles: {
|
||||
title: '角色权限',
|
||||
subtitle: '角色绑定权限码,同时约束前端菜单与后端 API',
|
||||
title: t('system.roles'),
|
||||
subtitle: t('system.roleBindingHint'),
|
||||
icon: <Shield className="h-5 w-5" />,
|
||||
},
|
||||
settings: {
|
||||
title: '系统设置',
|
||||
subtitle: '通用与审计、安全策略、LDAP 目录服务',
|
||||
title: t('system.settings'),
|
||||
subtitle: t('system.generalSubtitle'),
|
||||
icon: <Settings className="h-5 w-5" />,
|
||||
},
|
||||
};
|
||||
@@ -128,12 +130,12 @@ export function SystemManager() {
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -187,24 +189,24 @@ export function SystemManager() {
|
||||
};
|
||||
|
||||
const handleDelete = async ( user: any) => {
|
||||
const ok = await confirm({ title: '删除用户', description: `确认删除用户「${user.nickname || user.username}」吗?`, confirmText: '删除', variant: 'destructive' });
|
||||
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('用户已删除');
|
||||
toast.success(t('system.userDeleted'));
|
||||
} catch (error: unknown) {
|
||||
toast.error(error instanceof Error ? error.message : '删除失败');
|
||||
toast.error(error instanceof Error ? error.message : t('common.deleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedUserIds.length === 0) return;
|
||||
const ok = await confirm({
|
||||
title: '批量删除用户',
|
||||
title: t('system.batchDeleteUsers'),
|
||||
description: `确定删除选中的 ${selectedUserIds.length} 个用户吗?此操作不可恢复。`,
|
||||
confirmText: '批量删除',
|
||||
confirmText: t('system.terminalBatchDelete'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
if (!ok) return;
|
||||
@@ -214,7 +216,7 @@ export function SystemManager() {
|
||||
setSelectedUserIds([]);
|
||||
await loadUsers();
|
||||
} catch (error: unknown) {
|
||||
toast.error(error instanceof Error ? error.message : '批量删除失败');
|
||||
toast.error(error instanceof Error ? error.message : t('cloud.batchDeleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -262,7 +264,7 @@ export function SystemManager() {
|
||||
setImportResult(res);
|
||||
await loadUsers();
|
||||
} catch (error) {
|
||||
setImportResult({ code: 500, message: error?.message || '导入失败' });
|
||||
setImportResult({ code: 500, message: error?.message || t('system.importFailed') });
|
||||
} finally {
|
||||
ldap.setImporting(false);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import { userApi } from '@/api';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import type { Role } from '@/types';
|
||||
import { RoleFormDialog, type RoleFormData, type PermissionOption } from './RoleFormDialog';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
interface SystemRolesTabProps {
|
||||
selectedRole: Role | null;
|
||||
@@ -31,17 +33,17 @@ const ROLE_ICONS: Record<string, string> = {
|
||||
|
||||
// 权限按模块分组
|
||||
const PERM_MODULE_GROUPS: { label: string; prefixes: string[] }[] = [
|
||||
{ label: '仪表盘', prefixes: ['dashboard:'] },
|
||||
{ label: '资产管理', prefixes: ['server:'] },
|
||||
{ label: i18n.t('menu.dashboard'), prefixes: ['dashboard:'] },
|
||||
{ label: i18n.t('menu.assets'), prefixes: ['server:'] },
|
||||
{ label: '云资源', prefixes: ['cloud:'] },
|
||||
{ label: 'K8s', prefixes: ['k8s:'] },
|
||||
{ label: '发布', prefixes: ['deploy:'] },
|
||||
{ label: '监控', prefixes: ['monitor:', 'agent:'] },
|
||||
{ label: i18n.t('common.publish'), prefixes: ['deploy:'] },
|
||||
{ label: i18n.t('monitor.hostDashboard.monitor'), prefixes: ['monitor:', 'agent:'] },
|
||||
{ label: '自动化', prefixes: ['auto:'] },
|
||||
{ label: 'AI', prefixes: ['aiops:'] },
|
||||
{ label: '审批', prefixes: ['approval:'] },
|
||||
{ label: '审计', prefixes: ['audit:'] },
|
||||
{ label: '系统', prefixes: ['system:'] },
|
||||
{ label: i18n.t('common.system3'), prefixes: ['system:'] },
|
||||
];
|
||||
|
||||
function groupPermNames(perms: string[], codes: string[]): { label: string; count: number }[] {
|
||||
@@ -76,6 +78,7 @@ const emptyForm: RoleFormData = {
|
||||
};
|
||||
|
||||
export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTabProps) {
|
||||
const { t } = useTranslation();
|
||||
const confirm = useConfirm();
|
||||
const [roles, setRoles] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -147,15 +150,15 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
};
|
||||
if (editingRole) {
|
||||
await userApi.updateRole(editingRole.id, payload);
|
||||
toast.success('角色更新成功');
|
||||
toast.success(t('system.roleUpdated'));
|
||||
} else {
|
||||
await userApi.createRole(payload);
|
||||
toast.success('角色创建成功');
|
||||
toast.success(t('system.roleCreated'));
|
||||
}
|
||||
setDialogOpen(false);
|
||||
await loadRoles();
|
||||
} catch (err) {
|
||||
toast.error(err?.message || '保存失败');
|
||||
toast.error(err?.message || t('common.saveFailed'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -163,22 +166,22 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
|
||||
const handleDelete = async (role: any) => {
|
||||
if (role.isSystem) {
|
||||
toast.error('系统默认角色不可删除');
|
||||
toast.error(t('system.systemRoleUndeletable'));
|
||||
return;
|
||||
}
|
||||
const ok = await confirm({
|
||||
title: '删除角色',
|
||||
title: t('system.deleteRole'),
|
||||
description: `确认删除角色「${role.name}」吗?该操作不可恢复。`,
|
||||
confirmText: '删除',
|
||||
confirmText: t('common.delete'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
await userApi.deleteRole(role.id);
|
||||
toast.success('角色已删除');
|
||||
toast.success(t('system.roleDeleted'));
|
||||
await loadRoles();
|
||||
} catch (err) {
|
||||
const msg = err?.response?.data?.message || err?.message || '删除失败';
|
||||
const msg = err?.response?.data?.message || err?.message || t('common.deleteFailed');
|
||||
toast.error(msg);
|
||||
}
|
||||
};
|
||||
@@ -188,15 +191,15 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
<div className="mb-4 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] px-4 py-3 flex items-start gap-3">
|
||||
<ShieldCheck className="h-5 w-5 dg-text-blue shrink-0 mt-0.5" />
|
||||
<div className="text-sm text-[var(--dg-text-secondary)] space-y-1">
|
||||
<p className="font-medium text-[var(--dg-text-primary)]">权限如何生效</p>
|
||||
<p>用户通过「用户管理」绑定角色;角色在此配置权限码。权限码同时作用于<strong className="text-[var(--dg-text-primary)]">后端 API 鉴权</strong>与<strong className="text-[var(--dg-text-primary)]">前端菜单/按钮显隐</strong>,修改后用户需重新登录生效。</p>
|
||||
<p className="font-medium text-[var(--dg-text-primary)]">{t('system.permHowEffect')}</p>
|
||||
<p>{t('system.permEffectDesc')}<strong className="text-[var(--dg-text-primary)]">{t('system.permBackendAuth')}</strong>{t('monitor.logCenterArch.connector')}<strong className="text-[var(--dg-text-primary)]">{t('system.permEffectFrontend')}</strong>,修改后用户需重新登录生效。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">{roles.length} 个角色</span>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={openCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />新建角色
|
||||
<Plus className="h-4 w-4 mr-2" />{t('pipeline.newRole')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -231,7 +234,7 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
{role.name}
|
||||
{role.isSystem && (
|
||||
<Badge className="bg-purple-500/15 dg-text-purple border-purple-500/30 text-[10px] px-1 py-0">
|
||||
<Lock className="h-2.5 w-2.5 mr-0.5" />系统
|
||||
<Lock className="h-2.5 w-2.5 mr-0.5" />{t('common.system3')}
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
@@ -258,7 +261,7 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
className="h-8 w-8 text-[var(--dg-text-muted)] hover:dg-text-red"
|
||||
onClick={() => handleDelete(role)}
|
||||
disabled={role.isSystem}
|
||||
title={role.isSystem ? '系统默认角色不可删除' : '删除角色'}
|
||||
title={role.isSystem ? t('system.systemRoleUndeletable') : t('system.deleteRole')}
|
||||
>
|
||||
<Trash2 className={`h-4 w-4 ${role.isSystem ? 'opacity-30' : ''}`} />
|
||||
</Button>
|
||||
@@ -266,7 +269,7 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-[var(--dg-text-secondary)] mb-3">{role.description || '暂无描述'}</p>
|
||||
<p className="text-sm text-[var(--dg-text-secondary)] mb-3">{role.description || t('pipeline.desc2')}</p>
|
||||
{/* 按模块显示权限数 */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{groupedPerms.map(g => (
|
||||
@@ -276,7 +279,7 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
))}
|
||||
</div>
|
||||
{perms.length === 0 && (
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">未分配权限</span>
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">{t('system.unassignedPerms')}</span>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -302,6 +305,7 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
function RoleDetailDialog({
|
||||
role, onClose, onEdit,
|
||||
}: { role: Role | null; onClose: () => void; onEdit: (role: Role) => void }) {
|
||||
const { t } = useTranslation();
|
||||
const rawRole = role as any;
|
||||
const isSystem = rawRole?.isSystem ?? false;
|
||||
return (
|
||||
@@ -313,7 +317,7 @@ function RoleDetailDialog({
|
||||
{role?.name}
|
||||
{isSystem && (
|
||||
<Badge className="bg-purple-500/15 dg-text-purple border-purple-500/30 text-[10px]">
|
||||
<Lock className="h-2.5 w-2.5 mr-0.5" />系统
|
||||
<Lock className="h-2.5 w-2.5 mr-0.5" />{t('common.system3')}
|
||||
</Badge>
|
||||
)}
|
||||
</DialogTitle>
|
||||
@@ -321,8 +325,8 @@ function RoleDetailDialog({
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg bg-[var(--dg-bg-tertiary)] p-4">
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mb-1">描述</p>
|
||||
<p className="text-sm text-[var(--dg-text-primary)]">{role?.description || '暂无描述'}</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mb-1">{t('system.roleDesc')}</p>
|
||||
<p className="text-sm text-[var(--dg-text-primary)]">{role?.description || t('pipeline.desc2')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-[var(--dg-text-primary)] mb-2">
|
||||
@@ -337,13 +341,13 @@ function RoleDetailDialog({
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">未分配权限</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">{t('system.unassignedPerms')}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={() => role && onEdit(role)}>
|
||||
<Edit className="h-4 w-4 mr-2" />编辑
|
||||
<Edit className="h-4 w-4 mr-2" />{t('common.edit')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -15,19 +15,18 @@ import { Lock, History, Key, Settings, Network, RefreshCw, Loader2 } from 'lucid
|
||||
import { PageTabs } from '@/components/layout/PageTabs';
|
||||
import { LdapManagementSection } from './LdapManagementSection';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
// 系统设置子 Tab:通用 / 安全 / LDAP // Logan.Li
|
||||
const SETTINGS_TABS = [
|
||||
{ id: 'general', label: '通用' },
|
||||
{ id: 'security', label: '安全' },
|
||||
{ id: 'general', label: i18n.t('system.settingsGeneral') },
|
||||
{ id: 'security', label: i18n.t('system.settingsSecurity') },
|
||||
{ id: 'ldap', label: 'LDAP' },
|
||||
] as const;
|
||||
type SettingsSubTab = (typeof SETTINGS_TABS)[number]['id'];
|
||||
|
||||
export type LdapServerType = 'openldap' | 'ad';
|
||||
|
||||
export interface LdapFormData {
|
||||
server_type: LdapServerType;
|
||||
ldap_host: string;
|
||||
ldap_port: number;
|
||||
use_ldaps: boolean;
|
||||
@@ -35,11 +34,6 @@ export interface LdapFormData {
|
||||
admin_password: string;
|
||||
base_dn: string;
|
||||
search_filter: string;
|
||||
user_mapping: {
|
||||
username: string;
|
||||
email: string;
|
||||
display_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SystemSettingsTabProps {
|
||||
@@ -107,6 +101,7 @@ interface SystemSettingsTabProps {
|
||||
}
|
||||
|
||||
export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
auditRetentionDays,
|
||||
setAuditRetentionDays,
|
||||
@@ -158,15 +153,15 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
<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">操作日志与审计策略</CardDescription>
|
||||
<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)]">记录所有操作</p>
|
||||
<p className="text-xs dg-text-muted">记录用户的所有操作行为</p>
|
||||
<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}
|
||||
@@ -177,9 +172,9 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
</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)]">日志保留时间</p>
|
||||
<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>
|
||||
@@ -212,10 +207,10 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
{auditApplying ? (
|
||||
<>
|
||||
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
|
||||
应用中...
|
||||
{t('system.applyingDot')}
|
||||
</>
|
||||
) : (
|
||||
'应用'
|
||||
t('common.apply')
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -226,18 +221,18 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
<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">版本与系统状态</CardDescription>
|
||||
<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">系统版本</span>
|
||||
<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">运行时间</span>
|
||||
<span className="text-sm text-[var(--dg-text-primary)]">30天 12小时</span>
|
||||
<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>
|
||||
@@ -250,29 +245,29 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
<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">密码策略与登录安全(规划中)</CardDescription>
|
||||
<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)]">强制密码复杂度</p>
|
||||
<p className="text-xs dg-text-muted">要求密码包含大小写字母、数字和特殊字符</p>
|
||||
<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)]">登录失败锁定</p>
|
||||
<p className="text-xs dg-text-muted">连续5次登录失败锁定账户30分钟</p>
|
||||
<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)]">双因素认证</p>
|
||||
<p className="text-xs dg-text-muted">启用后需要手机验证码登录</p>
|
||||
<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>
|
||||
@@ -283,32 +278,32 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
<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">会话超时与 Token 配置(规划中)</CardDescription>
|
||||
<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)]">会话超时时间</p>
|
||||
<p className="text-xs dg-text-muted">用户无操作30分钟后自动登出</p>
|
||||
<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">15分钟</SelectItem>
|
||||
<SelectItem value="15">{t('monitor.traceMgr.min15')}</SelectItem>
|
||||
<SelectItem value="30">30分钟</SelectItem>
|
||||
<SelectItem value="60">1小时</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)]">单点登录</p>
|
||||
<p className="text-xs dg-text-muted">同一账号只能在一个设备登录</p>
|
||||
<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>
|
||||
@@ -327,40 +322,14 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
<Network className="h-5 w-5 dg-text-cyan" />
|
||||
LDAP / Active Directory
|
||||
</CardTitle>
|
||||
<CardDescription className="dg-text-muted mt-1">配置 LDAP 服务器,实现统一身份认证</CardDescription>
|
||||
<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">目录类型</label>
|
||||
<Select
|
||||
value={ldapForm.server_type}
|
||||
onValueChange={(v: LdapServerType) => {
|
||||
const isAD = v === 'ad';
|
||||
setLdapForm({
|
||||
...ldapForm,
|
||||
server_type: v,
|
||||
ldap_port: isAD && ldapForm.ldap_port === 389 ? 389 : ldapForm.ldap_port,
|
||||
search_filter: ldapForm.search_filter || (isAD ? '(sAMAccountName=%s)' : '(uid=%s)'),
|
||||
user_mapping: isAD
|
||||
? { username: 'sAMAccountName', email: 'mail', display_name: 'displayName' }
|
||||
: { username: 'uid', email: 'mail', display_name: 'cn' },
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="dg-input">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input">
|
||||
<SelectItem value="openldap">OpenLDAP</SelectItem>
|
||||
<SelectItem value="ad">Active Directory</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs dg-text-muted">LDAP 服务器地址</label>
|
||||
<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 })}
|
||||
@@ -369,7 +338,7 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs dg-text-muted">端口</label>
|
||||
<label className="text-xs dg-text-muted">{t('system.ldapPort')}</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={ldapForm.ldap_port}
|
||||
@@ -379,25 +348,25 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5 col-span-2">
|
||||
<label className="text-xs dg-text-muted">Base DN(用户搜索基础)</label>
|
||||
<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={ldapForm.server_type === 'ad' ? 'DC=example,DC=com' : 'ou=users,dc=example,dc=com'}
|
||||
placeholder="ou=users,dc=example,dc=com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs dg-text-muted">管理员 Bind DN</label>
|
||||
<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={ldapForm.server_type === 'ad' ? 'CN=admin,CN=Users,DC=example,DC=com' : 'cn=admin,dc=example,dc=com'}
|
||||
placeholder="cn=admin,dc=example,dc=com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs dg-text-muted">管理员密码 {ldapConfig && '(留空不修改)'}</label>
|
||||
<label className="text-xs dg-text-muted">管理员密码 {ldapConfig && t('system.leaveBlankNoModify')}</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={ldapForm.admin_password}
|
||||
@@ -407,48 +376,12 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5 col-span-2">
|
||||
<label className="text-xs dg-text-muted">搜索过滤器(可选,用 %s 代表用户名)</label>
|
||||
<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={ldapForm.server_type === 'ad' ? '(sAMAccountName=%s)' : '(uid=%s)'}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs dg-text-muted">用户名字段</label>
|
||||
<Input
|
||||
value={ldapForm.user_mapping.username}
|
||||
onChange={(e) => setLdapForm({
|
||||
...ldapForm,
|
||||
user_mapping: { ...ldapForm.user_mapping, username: e.target.value },
|
||||
})}
|
||||
className="dg-input"
|
||||
placeholder={ldapForm.server_type === 'ad' ? 'sAMAccountName' : 'uid'}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs dg-text-muted">邮箱字段</label>
|
||||
<Input
|
||||
value={ldapForm.user_mapping.email}
|
||||
onChange={(e) => setLdapForm({
|
||||
...ldapForm,
|
||||
user_mapping: { ...ldapForm.user_mapping, email: e.target.value },
|
||||
})}
|
||||
className="dg-input"
|
||||
placeholder="mail"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs dg-text-muted">显示名字段</label>
|
||||
<Input
|
||||
value={ldapForm.user_mapping.display_name}
|
||||
onChange={(e) => setLdapForm({
|
||||
...ldapForm,
|
||||
user_mapping: { ...ldapForm.user_mapping, display_name: e.target.value },
|
||||
})}
|
||||
className="dg-input"
|
||||
placeholder={ldapForm.server_type === 'ad' ? 'displayName' : 'cn'}
|
||||
placeholder="(uid=%s)"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 col-span-2">
|
||||
@@ -457,10 +390,7 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
onCheckedChange={(v) => setLdapForm({ ...ldapForm, use_ldaps: v })}
|
||||
className="data-[state=checked]:bg-blue-600"
|
||||
/>
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">启用 LDAPS(SSL 加密)</span>
|
||||
{ldapForm.server_type === 'ad' && (
|
||||
<span className="text-xs dg-text-muted">AD 创建用户必须启用 LDAPS</span>
|
||||
)}
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">{t('system.enableLdaps')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -488,7 +418,7 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
onClick={handleLdapSave}
|
||||
disabled={ldapSaving}
|
||||
>
|
||||
{ldapSaving ? '保存中...' : '保存配置'}
|
||||
{ldapSaving ? t('k8s.topology.saving') : t('aiops.llmSave')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,7 @@ import { UserDetailDialog } from './UserDetailDialog';
|
||||
import { CsvImportDialog } from './CsvImportDialog';
|
||||
import { LdapImportDialog } from './LdapImportDialog';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface SystemUserTabProps {
|
||||
// Search
|
||||
@@ -119,6 +120,7 @@ export function SystemUserTab({
|
||||
handleLdapImport,
|
||||
setLdapUsers,
|
||||
}: SystemUserTabProps) {
|
||||
const { t } = useTranslation();
|
||||
const { hasPermission } = usePermission();
|
||||
const canManage = hasPermission('system:user');
|
||||
|
||||
@@ -140,7 +142,7 @@ export function SystemUserTab({
|
||||
<div className="relative w-64">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||
<Input
|
||||
placeholder="搜索用户..."
|
||||
placeholder={t('system.searchUserDot')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 h-9 bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||
@@ -150,13 +152,13 @@ export function SystemUserTab({
|
||||
{hasPermission('system:user') && (
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]" onClick={() => setImportDialogOpen('csv')}>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
批量导入
|
||||
{t('common.batchImport')}
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission('system:user') && (
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]" onClick={() => setImportDialogOpen('ldap')}>
|
||||
<Network className="h-4 w-4 mr-2" />
|
||||
LDAP 导入
|
||||
{t('system.ldapImport')}
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission('system:user') && (
|
||||
@@ -175,11 +177,11 @@ export function SystemUserTab({
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={() => setSelectedUserIds([])}>
|
||||
取消选择
|
||||
{t('cloud.clearSelection')}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="dg-btn-danger dg-btn-sm" onClick={handleBatchDelete}>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
批量删除
|
||||
{t('system.terminalBatchDelete')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,13 +203,13 @@ export function SystemUserTab({
|
||||
/>
|
||||
</TableHead>
|
||||
)}
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">用户信息</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">联系方式</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">角色</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">来源</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">状态</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">最后登录</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)] text-right">操作</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">{t('system.userTitleField')}</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">{t('system.contactInfo')}</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">{t('system.userRole')}</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">{t('common.source')}</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">{t('system.userStatus')}</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">{t('system.lastLogin')}</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)] text-right">{t('system.auditAction')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Search, RefreshCw, Trash2, Play } from 'lucide-react';
|
||||
import { useTerminalSessions, useTerminalSessionMutations } from '@/hooks/api/useTerminalSessions';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { ReplayDialog } from './ReplayDialog';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function formatDuration(sec: number): string {
|
||||
if (!sec || sec < 0) return '--';
|
||||
@@ -39,6 +40,7 @@ function formatSize(bytes: number): string {
|
||||
* 终端审计 Tab — WebSSH 会话录制列表与回放(JumpServer 式审计) // Logan.Li
|
||||
*/
|
||||
export function TerminalSessionsTab() {
|
||||
const { t } = useTranslation();
|
||||
const confirm = useConfirm();
|
||||
const { data, isLoading: loading, isFetching, refetch } = useTerminalSessions({ page: 1, page_size: 100 });
|
||||
const sessions = data?.list ?? [];
|
||||
@@ -65,20 +67,20 @@ export function TerminalSessionsTab() {
|
||||
if (status === 'active') {
|
||||
return (
|
||||
<Badge variant="outline" className="border-emerald-500/30 dg-text-green bg-emerald-500/10">
|
||||
活动中
|
||||
{t('system.terminalActive')}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
if (status === 'failed') {
|
||||
return (
|
||||
<Badge variant="outline" className="border-red-500/30 dg-text-red bg-red-500/10">
|
||||
失败
|
||||
{t('system.auditFailed')}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Badge variant="outline" className="border-[var(--dg-border)] dg-text-secondary bg-[var(--dg-bg-active)]/10">
|
||||
已关闭
|
||||
{t('system.terminalClosed')}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
@@ -105,9 +107,9 @@ export function TerminalSessionsTab() {
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
const ok = await confirm({
|
||||
title: '删除会话记录',
|
||||
description: '确认删除该终端会话记录?删除后无法恢复。',
|
||||
confirmText: '删除',
|
||||
title: t('system.deleteSessionRecord'),
|
||||
description: t('system.confirmDeleteTerminal'),
|
||||
confirmText: t('common.delete'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
if (!ok) return;
|
||||
@@ -126,9 +128,9 @@ export function TerminalSessionsTab() {
|
||||
const handleBatchDelete = async () => {
|
||||
if (selected.size === 0) return;
|
||||
const ok = await confirm({
|
||||
title: '批量删除',
|
||||
title: t('system.terminalBatchDelete'),
|
||||
description: `确认删除选中的 ${selected.size} 条终端会话记录?`,
|
||||
confirmText: '删除',
|
||||
confirmText: t('common.delete'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
if (!ok) return;
|
||||
@@ -149,7 +151,7 @@ export function TerminalSessionsTab() {
|
||||
<div className="relative w-64">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 dg-text-muted" />
|
||||
<Input
|
||||
placeholder="搜索用户/服务器/IP..."
|
||||
placeholder={t('audit.terminal.searchUserServerIp')}
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
className="pl-9 h-9 dg-input"
|
||||
@@ -179,7 +181,7 @@ export function TerminalSessionsTab() {
|
||||
disabled={isFetching}
|
||||
>
|
||||
<RefreshCw className={cn('h-4 w-4 mr-2', isFetching && 'animate-spin')} />
|
||||
刷新
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
{selected.size > 0 && (
|
||||
<Button
|
||||
@@ -204,7 +206,7 @@ export function TerminalSessionsTab() {
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 dg-text-muted">
|
||||
<Search className="h-8 w-8 mb-3 opacity-40" />
|
||||
<p className="text-sm">暂无终端会话记录</p>
|
||||
<p className="text-sm">{t('system.noTerminalSessions')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
@@ -218,14 +220,14 @@ export function TerminalSessionsTab() {
|
||||
className="accent-blue-600 cursor-pointer"
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="dg-text-secondary">用户</TableHead>
|
||||
<TableHead className="dg-text-secondary">服务器</TableHead>
|
||||
<TableHead className="dg-text-secondary">来源 IP</TableHead>
|
||||
<TableHead className="dg-text-secondary">状态</TableHead>
|
||||
<TableHead className="dg-text-secondary">时长</TableHead>
|
||||
<TableHead className="dg-text-secondary">录制</TableHead>
|
||||
<TableHead className="dg-text-secondary">开始时间</TableHead>
|
||||
<TableHead className="dg-text-secondary text-right">操作</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('system.roleUsers')}</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('system.terminalServer')}</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('system.sourceIp')}</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('system.userStatus')}</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('system.terminalDuration')}</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('system.terminalRecord')}</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('system.terminalStartTime')}</TableHead>
|
||||
<TableHead className="dg-text-secondary text-right">{t('system.auditAction')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -273,7 +275,7 @@ export function TerminalSessionsTab() {
|
||||
size="icon"
|
||||
className="h-8 w-8 dg-text-secondary hover:dg-text-blue hover:bg-[var(--dg-bg-hover)]"
|
||||
onClick={() => openReplay(s.id)}
|
||||
title="回放"
|
||||
title={t('system.terminalReplay')}
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -282,7 +284,7 @@ export function TerminalSessionsTab() {
|
||||
size="icon"
|
||||
className="h-8 w-8 dg-text-secondary hover:dg-text-red hover:bg-[var(--dg-bg-hover)]"
|
||||
onClick={() => handleDelete(s.id)}
|
||||
title="删除"
|
||||
title={t('common.delete')}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Key, Edit, User as UserIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface UserDetailDialogProps {
|
||||
selectedUser: any | null;
|
||||
@@ -28,6 +29,7 @@ export function UserDetailDialog({
|
||||
openEdit,
|
||||
onResetPassword,
|
||||
}: UserDetailDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Dialog open={!!selectedUser} onOpenChange={() => setSelectedUser(null)}>
|
||||
<DialogContent className="dg-card">
|
||||
@@ -44,27 +46,27 @@ export function UserDetailDialog({
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4 p-4 rounded-lg bg-[var(--dg-bg-tertiary)]/50">
|
||||
<div>
|
||||
<p className="text-xs dg-text-muted mb-1">用户名</p>
|
||||
<p className="text-xs dg-text-muted mb-1">{t('system.userName')}</p>
|
||||
<p className="text-sm text-[var(--dg-text-primary)]">{selectedUser?.username}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs dg-text-muted mb-1">昵称</p>
|
||||
<p className="text-xs dg-text-muted mb-1">{t('system.userNickname')}</p>
|
||||
<p className="text-sm text-[var(--dg-text-primary)]">{selectedUser?.nickname}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs dg-text-muted mb-1">邮箱</p>
|
||||
<p className="text-xs dg-text-muted mb-1">{t('system.userEmail')}</p>
|
||||
<p className="text-sm text-[var(--dg-text-primary)]">{selectedUser?.email}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs dg-text-muted mb-1">手机号</p>
|
||||
<p className="text-xs dg-text-muted mb-1">{t('system.userPhone')}</p>
|
||||
<p className="text-sm text-[var(--dg-text-primary)]">{selectedUser?.phone || '--'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs dg-text-muted mb-1">状态</p>
|
||||
<p className="text-xs dg-text-muted mb-1">{t('system.userStatus')}</p>
|
||||
<p className="text-sm text-[var(--dg-text-primary)]">{selectedUser && getUserStatusBadge(selectedUser.status)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs dg-text-muted mb-1">创建时间</p>
|
||||
<p className="text-xs dg-text-muted mb-1">{t('system.userCreatedAt')}</p>
|
||||
<p className="text-sm text-[var(--dg-text-primary)]">
|
||||
{selectedUser && selectedUser.createdAt ? new Date(selectedUser.createdAt).toLocaleDateString() : '--'}
|
||||
</p>
|
||||
@@ -72,7 +74,7 @@ export function UserDetailDialog({
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-[var(--dg-text-secondary)] mb-2">角色</h4>
|
||||
<h4 className="text-sm font-medium text-[var(--dg-text-secondary)] mb-2">{t('system.userRole')}</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedUser?.roles.map((role: string) => (
|
||||
<Badge key={role} variant="outline" className="border-[var(--dg-border)] dg-text-secondary">
|
||||
@@ -91,7 +93,7 @@ export function UserDetailDialog({
|
||||
onClick={() => selectedUser && onResetPassword?.(selectedUser)}
|
||||
>
|
||||
<Key className="h-4 w-4 mr-2" />
|
||||
重置密码
|
||||
{t('system.resetPassword')}
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
@@ -103,7 +105,7 @@ export function UserDetailDialog({
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
{t('common.edit')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { User } from 'lucide-react';
|
||||
import { userApi } from '@/api';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface UserFormData {
|
||||
username: string;
|
||||
@@ -49,6 +50,7 @@ export function UserFormDialog({
|
||||
saving,
|
||||
onSubmit,
|
||||
}: UserFormDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [roles, setRoles] = useState<{ id: string; name: string }[]>([]);
|
||||
|
||||
// 对话框打开时拉取角色列表 // Logan.Li
|
||||
@@ -81,36 +83,36 @@ export function UserFormDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<User className="h-5 w-5 dg-text-blue" />
|
||||
{editingUser ? '编辑用户' : '新建用户'}
|
||||
{editingUser ? t('system.editUser') : '新建用户'}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="dg-text-muted">
|
||||
{editingUser ? '修改用户信息' : '创建新的系统用户'}
|
||||
{editingUser ? t('system.editUserDesc') : t('system.createUserDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs dg-text-muted">用户名</label>
|
||||
<label className="text-xs dg-text-muted">{t('system.userName')}</label>
|
||||
<Input
|
||||
value={form.username}
|
||||
onChange={(e) => setForm({ ...form, username: e.target.value })}
|
||||
disabled={!!editingUser}
|
||||
className="dg-input"
|
||||
placeholder="登录用户名"
|
||||
placeholder={t('system.loginUsername')}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs dg-text-muted">昵称</label>
|
||||
<label className="text-xs dg-text-muted">{t('system.userNickname')}</label>
|
||||
<Input
|
||||
value={form.nickname}
|
||||
onChange={(e) => setForm({ ...form, nickname: e.target.value })}
|
||||
className="dg-input"
|
||||
placeholder="显示昵称"
|
||||
placeholder={t('system.userShowNickname')}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs dg-text-muted">邮箱</label>
|
||||
<label className="text-xs dg-text-muted">{t('system.userEmail')}</label>
|
||||
<Input
|
||||
value={form.email}
|
||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
||||
@@ -119,19 +121,19 @@ export function UserFormDialog({
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs dg-text-muted">手机号</label>
|
||||
<label className="text-xs dg-text-muted">{t('system.userPhone')}</label>
|
||||
<Input
|
||||
value={form.phone}
|
||||
onChange={(e) => setForm({ ...form, phone: e.target.value })}
|
||||
className="dg-input"
|
||||
placeholder="可选"
|
||||
placeholder={t('common.optional')}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs dg-text-muted">角色</label>
|
||||
<label className="text-xs dg-text-muted">{t('system.userRole')}</label>
|
||||
<Select value={form.role} onValueChange={(v) => setForm({ ...form, role: v })}>
|
||||
<SelectTrigger className="dg-input">
|
||||
<SelectValue placeholder="选择角色" />
|
||||
<SelectValue placeholder={t('deploy.rbac.selectRole')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input">
|
||||
{roles.map((role) => (
|
||||
@@ -143,7 +145,7 @@ export function UserFormDialog({
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs dg-text-muted">状态</label>
|
||||
<label className="text-xs dg-text-muted">{t('system.userStatus')}</label>
|
||||
<Select
|
||||
value={String(form.status)}
|
||||
onValueChange={(v) => setForm({ ...form, status: Number(v) })}
|
||||
@@ -152,32 +154,32 @@ export function UserFormDialog({
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input">
|
||||
<SelectItem value="1">正常</SelectItem>
|
||||
<SelectItem value="0">禁用</SelectItem>
|
||||
<SelectItem value="1">{t('common.normal')}</SelectItem>
|
||||
<SelectItem value="0">{t('system.userInactive')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs dg-text-muted">
|
||||
密码{editingUser ? '(留空则不修改)' : ''}
|
||||
密码{editingUser ? t('system.leaveBlankNoModify2') : ''}
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
className="dg-input"
|
||||
placeholder={editingUser ? '留空则不修改' : '设置初始密码'}
|
||||
placeholder={editingUser ? t('system.passwordLeaveBlank') : t('system.setInitialPassword')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={onSubmit} disabled={saving}>
|
||||
{saving ? '保存中...' : '保存'}
|
||||
{saving ? t('k8s.topology.saving') : t('common.save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -30,13 +30,10 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
|
||||
|
||||
// ===== LDAP 配置 =====
|
||||
const [ldapConfig, setLdapConfig] = useState<JsonData>(null);
|
||||
const defaultLdapForm = (): LdapFormData => ({
|
||||
server_type: 'openldap',
|
||||
const [ldapForm, setLdapForm] = useState<LdapFormData>({
|
||||
ldap_host: '', ldap_port: 389, use_ldaps: false,
|
||||
admin_dn: '', admin_password: '', base_dn: '', search_filter: '',
|
||||
user_mapping: { username: 'uid', email: 'mail', display_name: 'cn' },
|
||||
});
|
||||
const [ldapForm, setLdapForm] = useState<LdapFormData>(defaultLdapForm());
|
||||
const [ldapSaving, setLdapSaving] = useState(false);
|
||||
const [ldapTesting, setLdapTesting] = useState(false);
|
||||
const [ldapTestResult, setLdapTestResult] = useState<string | null>(null);
|
||||
@@ -71,26 +68,7 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
|
||||
const res = await ldapApi.getConfig();
|
||||
if (res && res.ldap_host) {
|
||||
setLdapConfig(res);
|
||||
const serverType = (res.server_type === 'ad' ? 'ad' : 'openldap') as LdapFormData['server_type'];
|
||||
let mapping = { username: 'uid', email: 'mail', display_name: 'cn' };
|
||||
if (serverType === 'ad') {
|
||||
mapping = { username: 'sAMAccountName', email: 'mail', display_name: 'displayName' };
|
||||
}
|
||||
if (res.user_mapping) {
|
||||
let um = res.user_mapping;
|
||||
if (typeof um === 'string') {
|
||||
try { um = JSON.parse(um); } catch { um = null; }
|
||||
}
|
||||
if (um && typeof um === 'object') {
|
||||
mapping = {
|
||||
username: um.username || mapping.username,
|
||||
email: um.email || mapping.email,
|
||||
display_name: um.display_name || mapping.display_name,
|
||||
};
|
||||
}
|
||||
}
|
||||
setLdapForm({
|
||||
server_type: serverType,
|
||||
ldap_host: res.ldap_host || '',
|
||||
ldap_port: res.ldap_port || 389,
|
||||
use_ldaps: res.use_ldaps || false,
|
||||
@@ -98,7 +76,6 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
|
||||
admin_password: '',
|
||||
base_dn: res.base_dn || '',
|
||||
search_filter: res.search_filter || '',
|
||||
user_mapping: mapping,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
@@ -110,10 +87,7 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
|
||||
const handleLdapSave = async () => {
|
||||
setLdapSaving(true);
|
||||
try {
|
||||
await ldapApi.updateConfig({
|
||||
...ldapForm,
|
||||
user_mapping: ldapForm.user_mapping,
|
||||
});
|
||||
await ldapApi.updateConfig(ldapForm);
|
||||
await loadLdapConfig();
|
||||
} catch (error) {
|
||||
console.error('Failed to save LDAP config:', error);
|
||||
@@ -127,10 +101,7 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
|
||||
setLdapTesting(true);
|
||||
setLdapTestResult(null);
|
||||
try {
|
||||
await ldapApi.updateConfig({
|
||||
...ldapForm,
|
||||
user_mapping: ldapForm.user_mapping,
|
||||
});
|
||||
await ldapApi.updateConfig(ldapForm);
|
||||
const res = await ldapApi.testConnection();
|
||||
setLdapTestResult(res?.message || (res?.success ? '连接成功' : '连接失败'));
|
||||
} catch (error) {
|
||||
|
||||
Reference in new issue
Block a user