Files
model-platform/frontend/app/features/admin/AdminPages.tsx
T

257 lines
12 KiB
TypeScript

import { FormEvent, useEffect, useState } from "react";
import {
ApiRequestError,
createEmployee,
deleteEmployee,
demoContext,
listEmployees,
updateEmployee,
type Employee,
} from "../../services/api";
import Icon from "../../components/Icon";
import "../../styles/admin.css";
import "../../styles/dashboard.css";
type Notice = {
tone: "success" | "error" | "info";
message: string;
};
export function DashboardPage({
scriptCount,
online,
onNavigate,
}: {
scriptCount: number;
online: boolean;
onNavigate: (page: "scripts" | "schedules" | "system") => void;
}) {
return (
<section className="dashboard-page">
<div className="dashboard-hero">
<div>
<span>MODEL DEVELOPMENT PLATFORM</span>
<h2>下午好,{demoContext.userName}</h2>
<p>当前位于 {demoContext.workspaceName},可以继续构建脚本或配置调度。</p>
</div>
<span className="dashboard-hero__badge">{online ? "服务正常" : "服务连接中"}</span>
</div>
<div className="dashboard-metrics">
<article><Icon name="script" /><span><b>{scriptCount}</b><small>工作副本</small></span></article>
<article><Icon name="workspace" /><span><b>2</b><small>Workspace</small></span></article>
<article><Icon name="settings" /><span><b>4</b><small>平台员工</small></span></article>
<article><Icon name="check" /><span><b>{online ? "正常" : "检查中"}</b><small>平台状态</small></span></article>
</div>
<div className="dashboard-actions">
<button type="button" onClick={() => onNavigate("scripts")}><Icon name="script" />构建脚本</button>
<button type="button" onClick={() => onNavigate("schedules")}><Icon name="schedule" />调度配置</button>
<button type="button" onClick={() => onNavigate("system")}><Icon name="settings" />员工管理</button>
</div>
<div className="dashboard-grid">
<section className="dashboard-panel dashboard-panel--trend">
<header><div><span>运行趋势</span><h3> 7 天调度执行</h3></div><b>成功率 92.6%</b></header>
<div className="trend-chart">
{[38, 55, 44, 73, 61, 86, 78].map((value, index) => (
<div className="trend-column" key={index}>
<span className="trend-column__value">{Math.round(value / 7)}</span>
<i style={{ height: `${value}%` }} />
<small>{["周一", "周二", "周三", "周四", "周五", "周六", "今天"][index]}</small>
</div>
))}
</div>
<footer><span><i className="legend-dot is-success" />成功 75</span><span><i className="legend-dot is-failed" />失败 6</span></footer>
</section>
<section className="dashboard-panel dashboard-panel--donut">
<header><div><span>脚本资产</span><h3>类型分布</h3></div></header>
<div className="donut-layout">
<div className="donut-chart"><span><b>{scriptCount}</b><small>全部脚本</small></span></div>
<div className="donut-legend">
<span><i className="legend-dot is-notebook" /><b>Notebook</b><small>{Math.max(1, Math.round(scriptCount * .67))} · 67%</small></span>
<span><i className="legend-dot is-python" /><b>Python</b><small>{Math.max(0, scriptCount - Math.round(scriptCount * .67))} · 33%</small></span>
<span><i className="legend-dot is-version" /><b>稳定版本</b><small>3 个已发布</small></span>
</div>
</div>
</section>
<section className="dashboard-panel dashboard-panel--activity">
<header><div><span>ACTIVITY</span><h3>最近平台活动</h3></div><button type="button">查看全部</button></header>
<div className="activity-table">
<div className="activity-table__head"><span>操作内容</span><span>执行人</span><span>状态</span><span>时间</span></div>
{[
["数据探索.ipynb 发布稳定版本 v3.0", "张三", "成功", "16:42"],
["每日模型训练流程完成调度运行", "Scheduler", "成功", "15:25"],
["批量预测.py 更新工作副本", "王五", "已同步", "14:18"],
["风险验证流程完成 DAG 校验", "李四", "成功", "11:06"],
].map((row) => (
<div className="activity-row" key={row[0]}>
<span><i className="activity-icon"><Icon name="check" size={13} /></i>{row[0]}</span>
<span>{row[1]}</span><span><b>{row[2]}</b></span><span>{row[3]}</span>
</div>
))}
</div>
</section>
</div>
</section>
);
}
const EMPTY_FORM = {
username: "",
display_name: "",
email: "",
role_code: "developer" as "admin" | "developer",
status: "active" as "active" | "disabled" | "locked",
};
export function SystemAdminPage({
onNotify,
onConnectionChange,
}: {
onNotify: (notice: Notice) => void;
onConnectionChange: (online: boolean) => void;
}) {
const [employees, setEmployees] = useState<Employee[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [editing, setEditing] = useState<Employee | null>(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [form, setForm] = useState(EMPTY_FORM);
const canManage = demoContext.roleCode === "admin";
const load = async (): Promise<void> => {
setLoading(true);
try {
setEmployees(await listEmployees());
onConnectionChange(true);
} catch (error) {
onConnectionChange(false);
onNotify({
tone: "error",
message: error instanceof Error ? error.message : "员工列表加载失败",
});
} finally {
setLoading(false);
}
};
useEffect(() => {
void load();
}, []);
const openCreate = (): void => {
setEditing(null);
setForm(EMPTY_FORM);
setDialogOpen(true);
};
const openEdit = (employee: Employee): void => {
setEditing(employee);
setForm({
username: employee.username,
display_name: employee.display_name,
email: employee.email ?? "",
role_code: employee.role_code,
status: employee.status,
});
setDialogOpen(true);
};
const submit = async (event: FormEvent): Promise<void> => {
event.preventDefault();
if (!form.display_name.trim() || (!editing && !form.username.trim())) return;
setSaving(true);
try {
if (editing) {
const updated = await updateEmployee(editing.user_id, {
display_name: form.display_name.trim(),
email: form.email.trim() || null,
role_code: form.role_code,
status: form.status,
});
setEmployees((current) => current.map(
(item) => item.user_id === updated.user_id ? updated : item,
));
} else {
const created = await createEmployee({
username: form.username.trim(),
display_name: form.display_name.trim(),
email: form.email.trim() || null,
role_code: form.role_code,
});
setEmployees((current) => [...current, created]);
}
setDialogOpen(false);
onNotify({ tone: "success", message: editing ? "员工信息已更新" : "员工已添加" });
} catch (error) {
onNotify({
tone: "error",
message: error instanceof ApiRequestError ? error.message : "保存员工失败",
});
} finally {
setSaving(false);
}
};
const remove = async (employee: Employee): Promise<void> => {
if (!window.confirm(`确定从当前 Workspace 删除员工“${employee.display_name}”吗?`)) return;
try {
await deleteEmployee(employee.user_id);
setEmployees((current) => current.filter((item) => item.user_id !== employee.user_id));
onNotify({ tone: "success", message: "员工已删除" });
} catch (error) {
onNotify({
tone: "error",
message: error instanceof Error ? error.message : "删除员工失败",
});
}
};
return (
<section className="admin-page">
<header className="admin-page__header">
<div><span>系统管理</span><h2>员工管理</h2><p>{demoContext.workspaceName} · {employees.length} 名员工</p></div>
<button className="primary-button" type="button" disabled={!canManage} onClick={openCreate}>
<Icon name="plus" size={15} />添加员工
</button>
</header>
{!canManage && <div className="admin-readonly">当前为开发人员,只能查看员工列表。</div>}
<div className="employee-table">
<div className="employee-table__head"><span>员工</span><span>账号</span><span>角色</span><span>状态</span><span>操作</span></div>
{loading ? <p className="admin-empty">正在加载员工…</p> : employees.map((employee) => {
const isProtectedAdmin = employee.role_code === "admin";
return (
<div className="employee-row" key={employee.user_id}>
<span className="employee-name"><b className="avatar">{employee.display_name.slice(0, 1)}</b><span><strong>{employee.display_name}</strong><small>{employee.email ?? "未设置邮箱"}</small></span></span>
<code>{employee.username}</code>
<span className={`role-pill is-${employee.role_code}`}>{employee.role_name}</span>
<span className={`status-pill is-${employee.status}`}>{employee.status === "active" ? "正常" : employee.status === "disabled" ? "已停用" : "已锁定"}</span>
<span className="employee-actions">
<button type="button" disabled={!canManage} onClick={() => openEdit(employee)}>编辑</button>
<button type="button" className="is-danger" disabled={!canManage || isProtectedAdmin} title={isProtectedAdmin ? "管理员账号不能删除" : "删除"} onClick={() => void remove(employee)}>删除</button>
</span>
</div>
);
})}
</div>
{dialogOpen && (
<div className="modal-backdrop">
<section className="modal modal--compact" role="dialog" aria-modal="true">
<div className="modal__header"><div><span className="modal__eyebrow">EMPLOYEE</span><h2>{editing ? "编辑员工" : "添加员工"}</h2></div><button className="icon-button" type="button" onClick={() => setDialogOpen(false)}><Icon name="close" /></button></div>
<form onSubmit={(event) => void submit(event)}>
<label className="form-field"><span>姓名</span><input autoFocus value={form.display_name} onChange={(event) => setForm({ ...form, display_name: event.target.value })} /></label>
<label className="form-field"><span>登录账号</span><input disabled={Boolean(editing)} value={form.username} onChange={(event) => setForm({ ...form, username: event.target.value })} /></label>
<label className="form-field"><span>邮箱</span><input type="email" value={form.email} onChange={(event) => setForm({ ...form, email: event.target.value })} /></label>
<label className="form-field"><span>角色</span><select value={form.role_code} onChange={(event) => setForm({ ...form, role_code: event.target.value as "admin" | "developer" })}><option value="developer">开发人员</option><option value="admin">管理员</option></select></label>
{editing && <label className="form-field"><span>状态</span><select value={form.status} onChange={(event) => setForm({ ...form, status: event.target.value as typeof form.status })}><option value="active">正常</option><option value="disabled">停用</option><option value="locked">锁定</option></select></label>}
<div className="modal__footer"><button className="secondary-button" type="button" onClick={() => setDialogOpen(false)}>取消</button><button className="primary-button" type="submit" disabled={saving}>{saving ? "保存中…" : "保存"}</button></div>
</form>
</section>
</div>
)}
</section>
);
}