mirror of
https://github.com/PlayEdu/PlayEdu
synced 2026-02-25 08:21:47 +08:00
!13 LDAP优化增强
* cursorrules * fixed: ldap同步的部门记录name记录错误 * 主题色一致 * changelog * admin接入ldap同步数据统计 * ldap同步数据记录接口合并 * fixed: 已同步被禁用用户的DN更新 * 已经同步的LDAP用户被禁止可以继续更新 * 优化代码 * 新增LDAP同步的详细记录 * 新增LDAP禁止用户的数据量统计 * 优化LDAP拉取数据的重复使用 * 优化LDAP同步 * ldap同步记录 * cursor rules
This commit is contained in:
@@ -15,3 +15,4 @@ export * as user from "./user";
|
||||
export * as appConfig from "./app-config";
|
||||
export * as dashboard from "./dashboard";
|
||||
export * as adminLog from "./admin-log";
|
||||
export * as ldap from "./ldap";
|
||||
|
||||
26
playedu-admin/src/api/ldap.ts
Normal file
26
playedu-admin/src/api/ldap.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
// 获取同步记录列表
|
||||
export function getSyncRecords(params: { page?: number; size?: number }) {
|
||||
return client.get("/backend/v1/ldap/sync-records", params);
|
||||
}
|
||||
|
||||
// 获取单条同步记录详情
|
||||
export function getSyncRecordDetail(id: number) {
|
||||
return client.get(`/backend/v1/ldap/sync-records/${id}`, {});
|
||||
}
|
||||
|
||||
// 获取同步记录的详细项目
|
||||
export function getSyncRecordDetails(id: number, params: {
|
||||
type: 'department' | 'user';
|
||||
action?: number;
|
||||
page?: number;
|
||||
size?: number
|
||||
}) {
|
||||
return client.get(`/backend/v1/ldap/sync-records/${id}/details`, params);
|
||||
}
|
||||
|
||||
// 下载同步记录数据
|
||||
export function downloadSyncRecord(id: number) {
|
||||
return client.get(`/backend/v1/ldap/sync-records/${id}/download`, {});
|
||||
}
|
||||
@@ -722,3 +722,10 @@ textarea.ant-input {
|
||||
.select-range-modal .ant-tabs-tab + .ant-tabs-tab {
|
||||
margin: 0 0 0 55px;
|
||||
}
|
||||
|
||||
.clickable-stat {
|
||||
cursor: pointer;
|
||||
}
|
||||
.clickable-stat:hover .ant-statistic-content {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Modal, Card, Row, Col, Statistic, Divider } from "antd";
|
||||
import { ldap } from "../../../api";
|
||||
import { LdapSyncItemsModal } from ".";
|
||||
import { dateFormat } from "../../../utils/index";
|
||||
|
||||
interface LdapSyncDetailModalProps {
|
||||
record: any;
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const LdapSyncDetailModal: React.FC<LdapSyncDetailModalProps> = ({
|
||||
record,
|
||||
open,
|
||||
onCancel
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [detail, setDetail] = useState<any>(null);
|
||||
const [itemsVisible, setItemsVisible] = useState(false);
|
||||
const [itemsType, setItemsType] = useState<"department" | "user">("department");
|
||||
const [itemsAction, setItemsAction] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && record) {
|
||||
loadDetail();
|
||||
}
|
||||
}, [open, record]);
|
||||
|
||||
const loadDetail = () => {
|
||||
setLoading(true);
|
||||
ldap.getSyncRecordDetail(record.id).then((res: any) => {
|
||||
setDetail(res.data);
|
||||
setLoading(false);
|
||||
}).catch(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const showItems = (type: "department" | "user", action: number) => {
|
||||
setItemsType(type);
|
||||
setItemsAction(action);
|
||||
setItemsVisible(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title="同步详情"
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
width={888}
|
||||
footer={null}
|
||||
>
|
||||
{detail && (
|
||||
<>
|
||||
<Card title="基本信息" loading={loading}>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Statistic title="同步ID" value={detail.id} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="同步状态"
|
||||
value={
|
||||
detail.status === 0 ? "进行中" :
|
||||
detail.status === 1 ? "成功" : "失败"
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic title="同步时间" value={dateFormat(detail.created_at)} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Card title="部门同步统计" loading={loading}>
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<div
|
||||
onClick={() => showItems("department", 0)}
|
||||
className="clickable-stat"
|
||||
>
|
||||
<Statistic
|
||||
title="总部门数"
|
||||
value={detail.total_department_count}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div
|
||||
onClick={() => showItems("department", 1)}
|
||||
className="clickable-stat"
|
||||
>
|
||||
<Statistic
|
||||
title="新增部门"
|
||||
value={detail.created_department_count}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div
|
||||
onClick={() => showItems("department", 2)}
|
||||
className="clickable-stat"
|
||||
>
|
||||
<Statistic
|
||||
title="更新部门"
|
||||
value={detail.updated_department_count}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div
|
||||
onClick={() => showItems("department", 3)}
|
||||
className="clickable-stat"
|
||||
>
|
||||
<Statistic
|
||||
title="删除部门"
|
||||
value={detail.deleted_department_count}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Card title="用户同步统计" loading={loading}>
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<div
|
||||
onClick={() => showItems("user", 0)}
|
||||
className="clickable-stat"
|
||||
>
|
||||
<Statistic
|
||||
title="总用户数"
|
||||
value={detail.total_user_count}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<div
|
||||
onClick={() => showItems("user", 1)}
|
||||
className="clickable-stat"
|
||||
>
|
||||
<Statistic
|
||||
title="新增用户"
|
||||
value={detail.created_user_count}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<div
|
||||
onClick={() => showItems("user", 2)}
|
||||
className="clickable-stat"
|
||||
>
|
||||
<Statistic
|
||||
title="更新用户"
|
||||
value={detail.updated_user_count}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<div
|
||||
onClick={() => showItems("user", 3)}
|
||||
className="clickable-stat"
|
||||
>
|
||||
<Statistic
|
||||
title="删除用户"
|
||||
value={detail.deleted_user_count}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div
|
||||
onClick={() => showItems("user", 5)}
|
||||
className="clickable-stat"
|
||||
>
|
||||
<Statistic
|
||||
title="禁止用户"
|
||||
value={detail.banned_user_count}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{detail.error_message && (
|
||||
<>
|
||||
<Divider />
|
||||
<Card title="错误信息" loading={loading}>
|
||||
<pre>{detail.error_message}</pre>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{detail && (
|
||||
<LdapSyncItemsModal
|
||||
recordId={detail.id}
|
||||
type={itemsType}
|
||||
action={itemsAction}
|
||||
open={itemsVisible}
|
||||
onCancel={() => setItemsVisible(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Modal, Table, Tag } from "antd";
|
||||
import { ldap } from "../../../api";
|
||||
|
||||
interface LdapSyncItemsModalProps {
|
||||
recordId: number;
|
||||
type: "department" | "user";
|
||||
action: number;
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const LdapSyncItemsModal: React.FC<LdapSyncItemsModalProps> = ({
|
||||
recordId,
|
||||
type,
|
||||
action,
|
||||
open,
|
||||
onCancel
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [list, setList] = useState<any[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [size, setSize] = useState(10);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
loadData();
|
||||
}
|
||||
}, [open, page, size, type, action]);
|
||||
|
||||
const loadData = () => {
|
||||
setLoading(true);
|
||||
ldap.getSyncRecordDetails(recordId, {
|
||||
type,
|
||||
action,
|
||||
page,
|
||||
size,
|
||||
}).then((res: any) => {
|
||||
setList(res.data.records);
|
||||
setTotal(res.data.total);
|
||||
setLoading(false);
|
||||
}).catch(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const getActionText = (action: number) => {
|
||||
const actions: any = {
|
||||
department: {
|
||||
1: "新增",
|
||||
2: "更新",
|
||||
3: "删除",
|
||||
4: "无变化"
|
||||
},
|
||||
user: {
|
||||
1: "新增",
|
||||
2: "更新",
|
||||
3: "删除",
|
||||
4: "无变化",
|
||||
5: "禁止"
|
||||
}
|
||||
};
|
||||
|
||||
return actions[type][action] || "未知";
|
||||
};
|
||||
|
||||
const getActionColor = (action: number) => {
|
||||
const colors: {[key: number]: string} = {
|
||||
1: "green", // 新增
|
||||
2: "blue", // 更新
|
||||
3: "red", // 删除
|
||||
4: "gray", // 无变化
|
||||
5: "orange" // 禁止
|
||||
};
|
||||
|
||||
return colors[action] || "default";
|
||||
};
|
||||
|
||||
const departmentColumns = [
|
||||
{ title: "ID", dataIndex: "id", key: "id", width: 60 },
|
||||
{ title: "部门名称", dataIndex: "name", key: "name" },
|
||||
{ title: "DN", dataIndex: "dn", key: "dn", ellipsis: true },
|
||||
{ title: "UUID", dataIndex: "uuid", key: "uuid", width: 280 },
|
||||
{
|
||||
title: "操作类型",
|
||||
dataIndex: "action",
|
||||
key: "action",
|
||||
width: 100,
|
||||
render: (actionType: number) => (
|
||||
<Tag color={getActionColor(actionType)}>
|
||||
{getActionText(actionType)}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
const userColumns = [
|
||||
{ title: "ID", dataIndex: "id", key: "id", width: 60 },
|
||||
{ title: "用户名", dataIndex: "cn", key: "cn" },
|
||||
{ title: "登录名", dataIndex: "uid", key: "uid" },
|
||||
{ title: "邮箱", dataIndex: "email", key: "email" },
|
||||
{ title: "部门", dataIndex: "ou", key: "ou", ellipsis: true },
|
||||
{
|
||||
title: "操作类型",
|
||||
dataIndex: "action",
|
||||
key: "action",
|
||||
width: 100,
|
||||
render: (actionType: number) => (
|
||||
<Tag color={getActionColor(actionType)}>
|
||||
{getActionText(actionType)}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
const columns = type === "department" ? departmentColumns : userColumns;
|
||||
const title = type === "department" ? "部门同步详情" : "用户同步详情";
|
||||
const actionText = action > 0 ? ` - ${getActionText(action)}` : "";
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${title}${actionText}`}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
width={900}
|
||||
footer={null}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
total,
|
||||
current: page,
|
||||
pageSize: size,
|
||||
onChange: (page, pageSize) => {
|
||||
setPage(page);
|
||||
setSize(pageSize || 10);
|
||||
},
|
||||
showSizeChanger: true,
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
183
playedu-admin/src/pages/department/components/LdapSyncModal.tsx
Normal file
183
playedu-admin/src/pages/department/components/LdapSyncModal.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Modal, Table, Button, message } from "antd";
|
||||
import { EyeOutlined, DownloadOutlined, SyncOutlined } from "@ant-design/icons";
|
||||
import { department, ldap } from "../../../api";
|
||||
import { LdapSyncDetailModal } from ".";
|
||||
import { dateFormat } from "../../../utils/index";
|
||||
|
||||
interface LdapSyncModalProps {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const LdapSyncModal: React.FC<LdapSyncModalProps> = ({
|
||||
open,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [syncLoading, setSyncLoading] = useState(false);
|
||||
const [list, setList] = useState<any[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [size, setSize] = useState(10);
|
||||
const [detailVisible, setDetailVisible] = useState(false);
|
||||
const [currentRecord, setCurrentRecord] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
loadData();
|
||||
}
|
||||
}, [open, page, size]);
|
||||
|
||||
const loadData = () => {
|
||||
setLoading(true);
|
||||
ldap
|
||||
.getSyncRecords({ page, size })
|
||||
.then((res: any) => {
|
||||
setList(res.data.data);
|
||||
setTotal(res.data.total);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSync = () => {
|
||||
if (syncLoading) {
|
||||
message.warning("正在同步,请稍后...");
|
||||
return;
|
||||
}
|
||||
setSyncLoading(true);
|
||||
department
|
||||
.ldapSync()
|
||||
.then(() => {
|
||||
message.success("同步触发成功");
|
||||
setSyncLoading(false);
|
||||
// 刷新数据列表
|
||||
loadData();
|
||||
})
|
||||
.catch(() => {
|
||||
setSyncLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownload = (id: number) => {
|
||||
ldap
|
||||
.downloadSyncRecord(id)
|
||||
.then((res: any) => {
|
||||
window.open(res.data.url, "_blank");
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error("下载失败");
|
||||
});
|
||||
};
|
||||
|
||||
const handleDetail = (record: any) => {
|
||||
setCurrentRecord(record);
|
||||
setDetailVisible(true);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: "ID", dataIndex: "id", key: "id" },
|
||||
{
|
||||
title: "同步状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
render: (status: number) => {
|
||||
if (status === 0)
|
||||
return <span style={{ color: "#faad14" }}>进行中</span>;
|
||||
if (status === 1) return <span style={{ color: "#52c41a" }}>成功</span>;
|
||||
return <span style={{ color: "#f5222d" }}>失败</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "部门总数",
|
||||
dataIndex: "total_department_count",
|
||||
key: "total_department_count",
|
||||
},
|
||||
{
|
||||
title: "用户总数",
|
||||
dataIndex: "total_user_count",
|
||||
key: "total_user_count",
|
||||
},
|
||||
{
|
||||
title: "同步时间",
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
render: (text: string) => <span>{dateFormat(text)}</span>,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
render: (_: any, record: any) => (
|
||||
<div className="d-flex">
|
||||
<Button
|
||||
type="link"
|
||||
className="b-link c-red mr-8"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleDetail(record)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
className="b-link c-red"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => handleDownload(record.id)}
|
||||
disabled={!record.s3_file_path}
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title="LDAP同步记录"
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
width={900}
|
||||
footer={null}
|
||||
>
|
||||
<div className="mb-24">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SyncOutlined />}
|
||||
loading={syncLoading}
|
||||
onClick={handleSync}
|
||||
>
|
||||
创建同步任务
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
total,
|
||||
current: page,
|
||||
pageSize: size,
|
||||
onChange: (page, pageSize) => {
|
||||
setPage(page);
|
||||
setSize(pageSize || 10);
|
||||
},
|
||||
showSizeChanger: true,
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{currentRecord && (
|
||||
<LdapSyncDetailModal
|
||||
record={currentRecord}
|
||||
open={detailVisible}
|
||||
onCancel={() => setDetailVisible(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
3
playedu-admin/src/pages/department/components/index.ts
Normal file
3
playedu-admin/src/pages/department/components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { LdapSyncModal } from './LdapSyncModal';
|
||||
export { LdapSyncDetailModal } from './LdapSyncDetailModal';
|
||||
export { LdapSyncItemsModal } from './LdapSyncItemsModal';
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
|
||||
import { Spin, Button, Tree, Modal, message, Tooltip } from "antd";
|
||||
// import styles from "./index.module.less";
|
||||
import { PlusOutlined, ExclamationCircleFilled } from "@ant-design/icons";
|
||||
import { department } from "../../api/index";
|
||||
import { department, ldap } from "../../api/index";
|
||||
import { PerButton } from "../../compenents";
|
||||
import type { DataNode, TreeProps } from "antd/es/tree";
|
||||
import { DepartmentCreate } from "./compenents/create";
|
||||
@@ -10,6 +10,7 @@ import { DepartmentUpdate } from "./compenents/update";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { saveDepartmentsAction } from "../../store/system/systemConfigSlice";
|
||||
import { LdapSyncModal } from "./components";
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
@@ -34,6 +35,7 @@ const DepartmentPage = () => {
|
||||
const [updateVisible, setUpdateVisible] = useState(false);
|
||||
const [did, setDid] = useState<number>(0);
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const [syncModalVisible, setSyncModalVisible] = useState(false);
|
||||
|
||||
// 是否启用LDAP
|
||||
const ldapEnabled = useSelector(
|
||||
@@ -395,16 +397,7 @@ const DepartmentPage = () => {
|
||||
};
|
||||
|
||||
const ldapSync = () => {
|
||||
if (loading) {
|
||||
message.warning("正在同步,请稍后...");
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
department.ldapSync().then(() => {
|
||||
message.success("操作成功");
|
||||
setLoading(false);
|
||||
resetData();
|
||||
});
|
||||
setSyncModalVisible(true);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -474,6 +467,10 @@ const DepartmentPage = () => {
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
<LdapSyncModal
|
||||
open={syncModalVisible}
|
||||
onCancel={() => setSyncModalVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user