754 lines
28 KiB
TypeScript
754 lines
28 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { useSearchParams } from "react-router-dom";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
CardDescription,
|
|
} from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Lock,
|
|
History,
|
|
Key,
|
|
Settings,
|
|
Network,
|
|
RefreshCw,
|
|
Loader2,
|
|
} from "lucide-react";
|
|
import { PageTabs } from "@/components/layout/PageTabs";
|
|
import { LdapManagementSection } from "./LdapManagementSection";
|
|
import type { JsonData } from "@/types/common";
|
|
import { useTranslation } from "react-i18next";
|
|
import i18n from "@/i18n";
|
|
import { useUserStore } from "@/store";
|
|
|
|
// 系统设置子 Tab:通用 / 安全 / LDAP // Logan.Li
|
|
const SETTINGS_TABS = [
|
|
{ id: "general", label: i18n.t("system.settingsGeneral") },
|
|
{ id: "security", label: i18n.t("system.settingsSecurity") },
|
|
{ id: "ldap", label: "LDAP" },
|
|
] as const;
|
|
type SettingsSubTab = (typeof SETTINGS_TABS)[number]["id"];
|
|
|
|
export interface LdapFormData {
|
|
ldap_host: string;
|
|
ldap_port: number;
|
|
use_ldaps: boolean;
|
|
admin_dn: string;
|
|
admin_password: string;
|
|
base_dn: string;
|
|
search_filter: string;
|
|
}
|
|
|
|
export interface SystemSettingsTabProps {
|
|
// 审计策略 // Logan.Li
|
|
auditRetentionDays: string;
|
|
setAuditRetentionDays: (v: string) => void;
|
|
auditRecordAll: boolean;
|
|
setAuditRecordAll: (v: boolean) => void;
|
|
sessionWatermarkEnabled: boolean;
|
|
setSessionWatermarkEnabled: (v: boolean) => void;
|
|
sessionRecordEnabled: boolean;
|
|
setSessionRecordEnabled: (v: boolean) => void;
|
|
clipboardEnabled: boolean;
|
|
setClipboardEnabled: (v: boolean) => void;
|
|
fileUploadEnabled: boolean;
|
|
setFileUploadEnabled: (v: boolean) => void;
|
|
fileDownloadEnabled: boolean;
|
|
setFileDownloadEnabled: (v: boolean) => void;
|
|
commandPolicyEnabled: boolean;
|
|
setCommandPolicyEnabled: (v: boolean) => void;
|
|
approvalRequired: boolean;
|
|
setApprovalRequired: (v: boolean) => void;
|
|
auditLoading: boolean;
|
|
auditApplying: boolean;
|
|
handleAuditApply: () => void;
|
|
auditLastCleanupText: string | null;
|
|
auditRetentionOptions: number[];
|
|
// LDAP config
|
|
ldapConfig: unknown;
|
|
ldapForm: LdapFormData;
|
|
setLdapForm: React.Dispatch<React.SetStateAction<LdapFormData>>;
|
|
ldapSaving: boolean;
|
|
ldapTesting: boolean;
|
|
ldapTestResult: string | null;
|
|
handleLdapSave: () => void;
|
|
handleLdapTest: () => void;
|
|
// LDAP management - pass-through to LdapManagementSection
|
|
ldapMgmtOpen: boolean;
|
|
setLdapMgmtOpen: (v: boolean) => void;
|
|
ldapOUs: unknown[];
|
|
selectedOUDN: string;
|
|
setSelectedOUDN: (v: string) => void;
|
|
ldapUsers: unknown[];
|
|
selectedLDAPUsers: string[];
|
|
setSelectedLDAPUsers: (v: string[]) => void;
|
|
loadLdapOUs: () => void;
|
|
loadLdapOUUsers: (ouDn?: string) => void;
|
|
handleLdapImport: (opts?: { importAll?: boolean }) => void | Promise<void>;
|
|
importing: boolean;
|
|
importFeedback: string | null;
|
|
importResultOpen: boolean;
|
|
setImportResultOpen: (v: boolean) => void;
|
|
createOUOpen: boolean;
|
|
setCreateOUOpen: (v: boolean) => void;
|
|
ouForm: { ou_name: string; parent_dn: string; description: string };
|
|
setOuForm: React.Dispatch<
|
|
React.SetStateAction<{
|
|
ou_name: string;
|
|
parent_dn: string;
|
|
description: string;
|
|
}>
|
|
>;
|
|
handleCreateOU: () => void;
|
|
createUserOpen: boolean;
|
|
setCreateUserOpen: (v: boolean) => void;
|
|
newUser: {
|
|
username: string;
|
|
password: string;
|
|
display_name: string;
|
|
email: string;
|
|
ou_dn: string;
|
|
};
|
|
setNewUser: React.Dispatch<
|
|
React.SetStateAction<{
|
|
username: string;
|
|
password: string;
|
|
display_name: string;
|
|
email: string;
|
|
ou_dn: string;
|
|
}>
|
|
>;
|
|
handleCreateLDAPUser: () => void;
|
|
ldapUserTestOpen: boolean;
|
|
setLdapUserTestOpen: (v: boolean) => void;
|
|
ldapTestUser: string;
|
|
setLdapTestUser: (v: string) => void;
|
|
ldapTestPass: string;
|
|
setLdapTestPass: (v: string) => void;
|
|
ldapUserTesting: boolean;
|
|
ldapUserTestResult: unknown;
|
|
handleLdapUserTest: () => void;
|
|
editLdapUser: JsonData | null;
|
|
setEditLdapUser: (u: JsonData | null) => void;
|
|
editLdapForm: { cn: string; mail: string; phone: string; title: string };
|
|
setEditLdapForm: React.Dispatch<
|
|
React.SetStateAction<{
|
|
cn: string;
|
|
mail: string;
|
|
phone: string;
|
|
title: string;
|
|
}>
|
|
>;
|
|
handleUpdateLdapUser: () => void;
|
|
handleDeleteLDAPEntry: (dn: string, silent?: boolean) => void;
|
|
openEditLdapUser: (user: JsonData) => void;
|
|
}
|
|
|
|
export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
|
const { t } = useTranslation();
|
|
const currentUsername = useUserStore(
|
|
(state) => state.user?.username || "admin",
|
|
);
|
|
const {
|
|
auditRetentionDays,
|
|
setAuditRetentionDays,
|
|
auditRecordAll,
|
|
setAuditRecordAll,
|
|
sessionWatermarkEnabled,
|
|
setSessionWatermarkEnabled,
|
|
sessionRecordEnabled,
|
|
setSessionRecordEnabled,
|
|
clipboardEnabled,
|
|
setClipboardEnabled,
|
|
fileUploadEnabled,
|
|
setFileUploadEnabled,
|
|
fileDownloadEnabled,
|
|
setFileDownloadEnabled,
|
|
commandPolicyEnabled,
|
|
setCommandPolicyEnabled,
|
|
approvalRequired,
|
|
setApprovalRequired,
|
|
auditLoading,
|
|
auditApplying,
|
|
handleAuditApply,
|
|
auditLastCleanupText,
|
|
auditRetentionOptions,
|
|
ldapConfig,
|
|
ldapForm,
|
|
setLdapForm,
|
|
ldapSaving,
|
|
ldapTesting,
|
|
ldapTestResult,
|
|
handleLdapSave,
|
|
handleLdapTest,
|
|
} = props;
|
|
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const tabFromUrl = searchParams.get("tab") as SettingsSubTab | null;
|
|
const [subTab, setSubTab] = useState<SettingsSubTab>(
|
|
SETTINGS_TABS.some((t) => t.id === tabFromUrl) ? tabFromUrl! : "general",
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
tabFromUrl &&
|
|
SETTINGS_TABS.some((t) => t.id === tabFromUrl) &&
|
|
tabFromUrl !== subTab
|
|
) {
|
|
setSubTab(tabFromUrl);
|
|
}
|
|
}, [tabFromUrl, subTab]);
|
|
|
|
const changeSubTab = (id: string) => {
|
|
const next = id as SettingsSubTab;
|
|
setSubTab(next);
|
|
const params = new URLSearchParams(searchParams);
|
|
if (next === "general") params.delete("tab");
|
|
else params.set("tab", next);
|
|
setSearchParams(params, { replace: true });
|
|
};
|
|
|
|
return (
|
|
<div className="w-full space-y-4">
|
|
<PageTabs
|
|
items={[...SETTINGS_TABS]}
|
|
value={subTab}
|
|
onChange={changeSubTab}
|
|
/>
|
|
|
|
{subTab === "general" && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
|
|
<Card className="dg-card">
|
|
<CardHeader>
|
|
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
|
<History className="h-5 w-5 dg-text-green" />
|
|
{t("system.audit")}
|
|
</CardTitle>
|
|
<CardDescription className="dg-text-muted">
|
|
{t("system.auditSettingsDesc")}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
|
|
{t("system.recordAllOps")}
|
|
</p>
|
|
<p className="text-xs dg-text-muted">
|
|
{t("system.recordAllOpsDesc")}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={auditRecordAll}
|
|
onCheckedChange={setAuditRecordAll}
|
|
disabled={auditLoading || auditApplying}
|
|
className="data-[state=checked]:bg-blue-600"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
|
|
{t("system.logRetention")}
|
|
</p>
|
|
<p className="text-xs dg-text-muted">
|
|
{t("system.logRetentionHint")}
|
|
</p>
|
|
{auditLastCleanupText && (
|
|
<p className="text-xs dg-text-muted mt-1">
|
|
{auditLastCleanupText}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<Select
|
|
value={auditRetentionDays}
|
|
onValueChange={setAuditRetentionDays}
|
|
disabled={auditLoading || auditApplying}
|
|
>
|
|
<SelectTrigger className="w-28 h-8 dg-input shrink-0">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent className="dg-input">
|
|
{auditRetentionOptions.map((d) => (
|
|
<SelectItem key={d} value={String(d)}>
|
|
{d}天
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="flex justify-end pt-1">
|
|
<Button
|
|
size="sm"
|
|
className="dg-btn-primary"
|
|
onClick={handleAuditApply}
|
|
disabled={auditLoading || auditApplying}
|
|
>
|
|
{auditApplying ? (
|
|
<>
|
|
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
|
|
{t("system.applyingDot")}
|
|
</>
|
|
) : (
|
|
t("common.apply")
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="dg-card">
|
|
<CardHeader>
|
|
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
|
<Settings className="h-5 w-5 dg-text-purple" />
|
|
{t("system.systemInfo")}
|
|
</CardTitle>
|
|
<CardDescription className="dg-text-muted">
|
|
{t("system.systemInfoDesc")}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm dg-text-secondary">
|
|
{t("system.systemVersion")}
|
|
</span>
|
|
<span className="text-sm text-[var(--dg-text-primary)]">
|
|
v2.5.1
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm dg-text-secondary">
|
|
{t("system.uptime")}
|
|
</span>
|
|
<span className="text-sm text-[var(--dg-text-primary)]">
|
|
{t("system.uptimeValue")}
|
|
</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{subTab === "security" && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
|
|
<Card className="dg-card">
|
|
<CardHeader>
|
|
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
|
<Lock className="h-5 w-5 dg-text-blue" />
|
|
{t("system.security")}
|
|
</CardTitle>
|
|
<CardDescription className="dg-text-muted">
|
|
{t("system.securityDesc")}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
|
|
{t("system.forcePasswordComplexity")}
|
|
</p>
|
|
<p className="text-xs dg-text-muted">
|
|
{t("system.forcePasswordComplexityDesc")}
|
|
</p>
|
|
</div>
|
|
<Switch disabled className="data-[state=checked]:bg-blue-600" />
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
|
|
{t("system.loginLockout")}
|
|
</p>
|
|
<p className="text-xs dg-text-muted">
|
|
{t("system.loginLockoutDesc")}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
disabled
|
|
defaultChecked
|
|
className="data-[state=checked]:bg-blue-600"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
|
|
{t("system.twoFactor")}
|
|
</p>
|
|
<p className="text-xs dg-text-muted">
|
|
{t("system.twoFactorDesc")}
|
|
</p>
|
|
</div>
|
|
<Switch disabled className="data-[state=checked]:bg-blue-600" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="dg-card">
|
|
<CardHeader>
|
|
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
|
<Key className="h-5 w-5 dg-text-yellow" />
|
|
{t("system.sessionSettings")}
|
|
</CardTitle>
|
|
<CardDescription className="dg-text-muted">
|
|
{t("system.sessionSettingsDesc")}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
|
|
{t("system.sessionWatermark")}
|
|
</p>
|
|
<p className="text-xs dg-text-muted">
|
|
{t("system.sessionWatermarkDesc", {
|
|
user: currentUsername,
|
|
})}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={sessionWatermarkEnabled}
|
|
onCheckedChange={setSessionWatermarkEnabled}
|
|
className="data-[state=checked]:bg-blue-600"
|
|
/>
|
|
</div>
|
|
<div className="rounded border border-[var(--dg-border)] bg-[#101418] px-3 py-4 overflow-hidden">
|
|
<div className="relative h-24 rounded bg-black/70">
|
|
<div
|
|
className="absolute inset-0 opacity-80"
|
|
style={{
|
|
backgroundImage: `repeating-linear-gradient(-24deg, transparent 0 42px, rgba(255,255,255,0.12) 43px 44px, transparent 45px 120px)`,
|
|
}}
|
|
/>
|
|
<div className="absolute inset-0 grid grid-cols-2 place-items-center -rotate-[18deg] text-lg font-semibold tracking-normal text-white/30">
|
|
<span>{currentUsername}</span>
|
|
<span>{currentUsername}</span>
|
|
<span>{currentUsername}</span>
|
|
<span>{currentUsername}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-3">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
|
|
{t("system.sessionRecordEnabled")}
|
|
</p>
|
|
<p className="text-xs dg-text-muted">
|
|
{t("system.sessionRecordEnabledDesc")}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={sessionRecordEnabled}
|
|
onCheckedChange={setSessionRecordEnabled}
|
|
className="data-[state=checked]:bg-blue-600"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
|
|
{t("system.clipboardEnabled")}
|
|
</p>
|
|
<p className="text-xs dg-text-muted">
|
|
{t("system.clipboardEnabledDesc")}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={clipboardEnabled}
|
|
onCheckedChange={setClipboardEnabled}
|
|
className="data-[state=checked]:bg-blue-600"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
|
|
{t("system.fileUploadEnabled")}
|
|
</p>
|
|
<p className="text-xs dg-text-muted">
|
|
{t("system.fileUploadEnabledDesc")}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={fileUploadEnabled}
|
|
onCheckedChange={setFileUploadEnabled}
|
|
className="data-[state=checked]:bg-blue-600"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
|
|
{t("system.fileDownloadEnabled")}
|
|
</p>
|
|
<p className="text-xs dg-text-muted">
|
|
{t("system.fileDownloadEnabledDesc")}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={fileDownloadEnabled}
|
|
onCheckedChange={setFileDownloadEnabled}
|
|
className="data-[state=checked]:bg-blue-600"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
|
|
{t("system.commandPolicyEnabled")}
|
|
</p>
|
|
<p className="text-xs dg-text-muted">
|
|
{t("system.commandPolicyEnabledDesc")}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={commandPolicyEnabled}
|
|
onCheckedChange={setCommandPolicyEnabled}
|
|
className="data-[state=checked]:bg-blue-600"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
|
|
{t("system.approvalRequired")}
|
|
</p>
|
|
<p className="text-xs dg-text-muted">
|
|
{t("system.approvalRequiredDesc")}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={approvalRequired}
|
|
onCheckedChange={setApprovalRequired}
|
|
className="data-[state=checked]:bg-blue-600"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
|
|
{t("system.sessionTimeoutTime")}
|
|
</p>
|
|
<p className="text-xs dg-text-muted">
|
|
{t("system.sessionTimeoutDesc")}
|
|
</p>
|
|
</div>
|
|
<Select defaultValue="30" disabled>
|
|
<SelectTrigger className="w-32 h-8 dg-input">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent className="dg-input">
|
|
<SelectItem value="15">
|
|
{t("monitor.traceMgr.min15")}
|
|
</SelectItem>
|
|
<SelectItem value="30">30分钟</SelectItem>
|
|
<SelectItem value="60">
|
|
{t("monitor.traceMgr.hour1")}
|
|
</SelectItem>
|
|
<SelectItem value="120">2小时</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--dg-text-primary)]">
|
|
{t("system.sso")}
|
|
</p>
|
|
<p className="text-xs dg-text-muted">
|
|
{t("system.singleSignOnDesc")}
|
|
</p>
|
|
</div>
|
|
<Switch disabled className="data-[state=checked]:bg-blue-600" />
|
|
</div>
|
|
<div className="flex justify-end pt-1">
|
|
<Button
|
|
size="sm"
|
|
className="dg-btn-primary"
|
|
onClick={handleAuditApply}
|
|
disabled={auditLoading || auditApplying}
|
|
>
|
|
{auditApplying ? (
|
|
<>
|
|
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
|
|
{t("system.applyingDot")}
|
|
</>
|
|
) : (
|
|
t("common.apply")
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{subTab === "ldap" && (
|
|
<>
|
|
<Card className="dg-card pt-2">
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
|
<Network className="h-5 w-5 dg-text-cyan" />
|
|
LDAP / Active Directory
|
|
</CardTitle>
|
|
<CardDescription className="dg-text-muted mt-1">
|
|
{t("system.ldapConfigHint")}
|
|
</CardDescription>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs dg-text-muted">
|
|
{t("system.ldapServerAddr")}
|
|
</label>
|
|
<Input
|
|
value={ldapForm.ldap_host}
|
|
onChange={(e) =>
|
|
setLdapForm({ ...ldapForm, ldap_host: e.target.value })
|
|
}
|
|
className="dg-input"
|
|
placeholder="ldap.example.com"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs dg-text-muted">
|
|
{t("system.ldapPort")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
value={ldapForm.ldap_port}
|
|
onChange={(e) =>
|
|
setLdapForm({
|
|
...ldapForm,
|
|
ldap_port: Number(e.target.value),
|
|
})
|
|
}
|
|
className="dg-input"
|
|
placeholder="389"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5 col-span-2">
|
|
<label className="text-xs dg-text-muted">
|
|
{t("system.baseDnHint")}
|
|
</label>
|
|
<Input
|
|
value={ldapForm.base_dn}
|
|
onChange={(e) =>
|
|
setLdapForm({ ...ldapForm, base_dn: e.target.value })
|
|
}
|
|
className="dg-input"
|
|
placeholder="ou=users,dc=example,dc=com"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs dg-text-muted">
|
|
{t("system.adminBindDn")}
|
|
</label>
|
|
<Input
|
|
value={ldapForm.admin_dn}
|
|
onChange={(e) =>
|
|
setLdapForm({ ...ldapForm, admin_dn: e.target.value })
|
|
}
|
|
className="dg-input"
|
|
placeholder="cn=admin,dc=example,dc=com"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs dg-text-muted">
|
|
管理员密码 {ldapConfig && t("system.leaveBlankNoModify")}
|
|
</label>
|
|
<Input
|
|
type="password"
|
|
value={ldapForm.admin_password}
|
|
onChange={(e) =>
|
|
setLdapForm({
|
|
...ldapForm,
|
|
admin_password: e.target.value,
|
|
})
|
|
}
|
|
className="dg-input"
|
|
placeholder="******"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5 col-span-2">
|
|
<label className="text-xs dg-text-muted">
|
|
{t("system.searchFilterHint")}
|
|
</label>
|
|
<Input
|
|
value={ldapForm.search_filter}
|
|
onChange={(e) =>
|
|
setLdapForm({
|
|
...ldapForm,
|
|
search_filter: e.target.value,
|
|
})
|
|
}
|
|
className="dg-input"
|
|
placeholder="(uid=%s)"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2 col-span-2">
|
|
<Switch
|
|
checked={ldapForm.use_ldaps}
|
|
onCheckedChange={(v) =>
|
|
setLdapForm({ ...ldapForm, use_ldaps: v })
|
|
}
|
|
className="data-[state=checked]:bg-blue-600"
|
|
/>
|
|
<span className="text-sm text-[var(--dg-text-secondary)]">
|
|
{t("system.enableLdaps")}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 测试/保存按钮 */}
|
|
<div className="flex items-center gap-3 mt-4 pt-4 border-t border-[var(--dg-border)]">
|
|
{ldapTestResult && (
|
|
<span
|
|
className={`text-xs ${ldapTestResult.includes("成功") ? "dg-text-green" : "dg-text-red"}`}
|
|
>
|
|
{ldapTestResult}
|
|
</span>
|
|
)}
|
|
<div className="flex items-center gap-2 ml-auto">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="dg-btn-secondary"
|
|
onClick={handleLdapTest}
|
|
disabled={ldapTesting}
|
|
>
|
|
{ldapTesting ? (
|
|
<Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
|
|
) : (
|
|
<RefreshCw className="h-3.5 w-3.5 mr-1" />
|
|
)}
|
|
测试连接
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
className="dg-btn-primary"
|
|
onClick={handleLdapSave}
|
|
disabled={ldapSaving}
|
|
>
|
|
{ldapSaving ? t("k8s.topology.saving") : t("aiops.llmSave")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{ldapConfig && <LdapManagementSection {...props} />}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|