feat: auth
This commit is contained in:
@@ -1,14 +1,10 @@
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { useEffect, useState, type FormEvent } from "react";
|
||||
|
||||
import {
|
||||
ApiRequestError,
|
||||
createEmployee,
|
||||
deleteEmployee,
|
||||
demoContext,
|
||||
listEmployees,
|
||||
updateEmployee,
|
||||
type Employee,
|
||||
} from "../../services/api";
|
||||
import { useApi, useAuth } from "../../context/AuthContext";
|
||||
import Icon from "../../components/Icon";
|
||||
import "../../styles/admin.css";
|
||||
import "../../styles/dashboard.css";
|
||||
@@ -28,13 +24,17 @@ export function DashboardPage({
|
||||
online: boolean;
|
||||
onNavigate: (page: "scripts" | "schedules" | "system") => void;
|
||||
}) {
|
||||
const { user, currentWorkspace } = useAuth();
|
||||
return (
|
||||
<section className="dashboard-page">
|
||||
<div className="dashboard-hero">
|
||||
<div>
|
||||
<span>MODEL DEVELOPMENT PLATFORM</span>
|
||||
<h2>下午好,{demoContext.userName}</h2>
|
||||
<p>当前位于 {demoContext.workspaceName},可以继续构建脚本或配置调度。</p>
|
||||
<h2>下午好,{user?.display_name ?? "用户"}</h2>
|
||||
<p>
|
||||
当前位于 {currentWorkspace?.workspace_name ?? "(未选择 Workspace)"}
|
||||
,可以继续构建脚本或配置调度。
|
||||
</p>
|
||||
</div>
|
||||
<span className="dashboard-hero__badge">{online ? "服务正常" : "服务连接中"}</span>
|
||||
</div>
|
||||
@@ -112,18 +112,20 @@ export function SystemAdminPage({
|
||||
onNotify: (notice: Notice) => void;
|
||||
onConnectionChange: (online: boolean) => void;
|
||||
}) {
|
||||
const api = useApi();
|
||||
const { user, currentWorkspace } = useAuth();
|
||||
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 canManage = user?.role_code === "admin";
|
||||
|
||||
const load = async (): Promise<void> => {
|
||||
setLoading(true);
|
||||
try {
|
||||
setEmployees(await listEmployees());
|
||||
setEmployees(await api.listEmployees());
|
||||
onConnectionChange(true);
|
||||
} catch (error) {
|
||||
onConnectionChange(false);
|
||||
@@ -164,7 +166,7 @@ export function SystemAdminPage({
|
||||
setSaving(true);
|
||||
try {
|
||||
if (editing) {
|
||||
const updated = await updateEmployee(editing.user_id, {
|
||||
const updated = await api.updateEmployee(editing.user_id, {
|
||||
display_name: form.display_name.trim(),
|
||||
email: form.email.trim() || null,
|
||||
role_code: form.role_code,
|
||||
@@ -174,7 +176,7 @@ export function SystemAdminPage({
|
||||
(item) => item.user_id === updated.user_id ? updated : item,
|
||||
));
|
||||
} else {
|
||||
const created = await createEmployee({
|
||||
const created = await api.createEmployee({
|
||||
username: form.username.trim(),
|
||||
display_name: form.display_name.trim(),
|
||||
email: form.email.trim() || null,
|
||||
@@ -197,7 +199,7 @@ export function SystemAdminPage({
|
||||
const remove = async (employee: Employee): Promise<void> => {
|
||||
if (!window.confirm(`确定从当前 Workspace 删除员工“${employee.display_name}”吗?`)) return;
|
||||
try {
|
||||
await deleteEmployee(employee.user_id);
|
||||
await api.deleteEmployee(employee.user_id);
|
||||
setEmployees((current) => current.filter((item) => item.user_id !== employee.user_id));
|
||||
onNotify({ tone: "success", message: "员工已删除" });
|
||||
} catch (error) {
|
||||
@@ -211,7 +213,11 @@ export function SystemAdminPage({
|
||||
return (
|
||||
<section className="admin-page">
|
||||
<header className="admin-page__header">
|
||||
<div><span>系统管理</span><h2>员工管理</h2><p>{demoContext.workspaceName} · {employees.length} 名员工</p></div>
|
||||
<div>
|
||||
<span>系统管理</span>
|
||||
<h2>员工管理</h2>
|
||||
<p>{currentWorkspace?.workspace_name ?? "(未选择 Workspace)"} · {employees.length} 名员工</p>
|
||||
</div>
|
||||
<button className="primary-button" type="button" disabled={!canManage} onClick={openCreate}>
|
||||
<Icon name="plus" size={15} />添加员工
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
type ChangeEvent,
|
||||
FormEvent,
|
||||
type FormEvent,
|
||||
type MouseEvent as ReactMouseEvent,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -10,24 +10,6 @@ import {
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
|
||||
import {
|
||||
acquireFileLock,
|
||||
createWorkspaceDirectory,
|
||||
createScript,
|
||||
createJupyterAccessTicket,
|
||||
deleteScript,
|
||||
deleteWorkspaceDirectory,
|
||||
demoContext,
|
||||
demoUsers,
|
||||
demoWorkspaces,
|
||||
heartbeatFileLock,
|
||||
listScripts,
|
||||
listScriptVersions,
|
||||
listWorkspaceDirectories,
|
||||
publishScriptVersion,
|
||||
releaseFileLock,
|
||||
releaseFileLockOnUnload,
|
||||
setDemoContext,
|
||||
uploadScript,
|
||||
type ActiveEditSession,
|
||||
type ScriptItem,
|
||||
type ScriptType,
|
||||
@@ -35,6 +17,7 @@ import {
|
||||
type Visibility,
|
||||
type WorkspaceDirectory,
|
||||
} from "../../services/api";
|
||||
import { useApi, useAuth } from "~/context/AuthContext";
|
||||
import Icon from "../../components/Icon";
|
||||
import SchedulePage from "../schedules/SchedulePage";
|
||||
import { DashboardPage, SystemAdminPage } from "../admin/AdminPages";
|
||||
@@ -186,6 +169,19 @@ export default function ModelPlatformApp() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const activePage = pageFromPath(location.pathname);
|
||||
const { user, workspaces, currentWorkspace, setCurrentWorkspace, logout } = useAuth();
|
||||
const api = useApi();
|
||||
|
||||
// Loading state: wait for auth and workspace selection
|
||||
if (!currentWorkspace) {
|
||||
return (
|
||||
<div className="app-shell" style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<span style={{ fontSize: 18 }}>加载中…</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const [scripts, setScripts] = useState<ScriptItem[]>([]);
|
||||
const [directories, setDirectories] = useState<WorkspaceDirectory[]>([]);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
@@ -204,7 +200,6 @@ export default function ModelPlatformApp() {
|
||||
}>({ open: false, parentPath: "", name: "", busy: false });
|
||||
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
||||
const [workspaceMenuOpen, setWorkspaceMenuOpen] = useState(false);
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
const [uploadParentPath, setUploadParentPath] = useState("");
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const uploadInputRef = useRef<HTMLInputElement | null>(null);
|
||||
@@ -236,8 +231,8 @@ export default function ModelPlatformApp() {
|
||||
setRefreshing(silent);
|
||||
try {
|
||||
const [items, folderItems] = await Promise.all([
|
||||
listScripts(),
|
||||
listWorkspaceDirectories(),
|
||||
api.listScripts(),
|
||||
api.listWorkspaceDirectories(),
|
||||
]);
|
||||
setScripts(items);
|
||||
setDirectories(folderItems);
|
||||
@@ -308,7 +303,7 @@ export default function ModelPlatformApp() {
|
||||
}
|
||||
let ignore = false;
|
||||
setVersionsLoading(true);
|
||||
void listScriptVersions(selectedId)
|
||||
void api.listScriptVersions(selectedId)
|
||||
.then((items) => {
|
||||
if (!ignore) setVersions(items);
|
||||
})
|
||||
@@ -342,7 +337,7 @@ export default function ModelPlatformApp() {
|
||||
return;
|
||||
}
|
||||
heartbeatRunning = true;
|
||||
void heartbeatFileLock(current)
|
||||
void api.heartbeatFileLock(current)
|
||||
.then((updated) => {
|
||||
setEditSession((active) => active
|
||||
&& active.edit_session_id === updated.edit_session_id
|
||||
@@ -379,7 +374,7 @@ export default function ModelPlatformApp() {
|
||||
if (!current || current.edit_session_id !== editSession.edit_session_id) {
|
||||
return;
|
||||
}
|
||||
void createJupyterAccessTicket(current)
|
||||
void api.createJupyterAccessTicket(current)
|
||||
.then((ticket) => {
|
||||
setEditSession((active) => active
|
||||
&& active.edit_session_id === ticket.edit_session_id
|
||||
@@ -402,7 +397,7 @@ export default function ModelPlatformApp() {
|
||||
if (!editSession) return;
|
||||
const handleUnload = () => {
|
||||
const current = editSessionRef.current;
|
||||
if (current) releaseFileLockOnUnload(current);
|
||||
if (current) api.releaseFileLockOnUnload(current);
|
||||
};
|
||||
window.addEventListener("beforeunload", handleUnload);
|
||||
return () => window.removeEventListener("beforeunload", handleUnload);
|
||||
@@ -416,24 +411,17 @@ export default function ModelPlatformApp() {
|
||||
);
|
||||
}, [keyword, scripts]);
|
||||
|
||||
const memberScriptGroups = [...demoUsers]
|
||||
.sort((left, right) => (
|
||||
Number(right.userId === demoContext.userId)
|
||||
- Number(left.userId === demoContext.userId)
|
||||
))
|
||||
.map((user) => {
|
||||
const memberScripts = filteredScripts.filter(
|
||||
(item) => item.owner_user_id === user.userId,
|
||||
const memberScriptGroups = (() => {
|
||||
const currentUserScripts = filteredScripts.filter(
|
||||
(item) => item.owner_user_id === user?.user_id,
|
||||
);
|
||||
const inferred = inferredDirectories(memberScripts);
|
||||
return {
|
||||
user,
|
||||
scripts: memberScripts,
|
||||
directories: user.userId === demoContext.userId
|
||||
? mergeDirectories(directories, inferred)
|
||||
: inferred,
|
||||
};
|
||||
});
|
||||
const inferred = inferredDirectories(currentUserScripts);
|
||||
return [{
|
||||
user: user,
|
||||
scripts: currentUserScripts,
|
||||
directories: mergeDirectories(directories, inferred),
|
||||
}];
|
||||
})();
|
||||
const selected = scripts.find((item) => item.script_id === selectedId) ?? null;
|
||||
|
||||
const selectScript = (scriptId: string | null) => {
|
||||
@@ -473,7 +461,7 @@ export default function ModelPlatformApp() {
|
||||
let newlyAcquired = false;
|
||||
try {
|
||||
if (active && active.script_id !== script.script_id) {
|
||||
await releaseFileLock(active);
|
||||
await api.releaseFileLock(active);
|
||||
setEmbeddedJupyterUrl(null);
|
||||
setEditSession(null);
|
||||
editSessionRef.current = null;
|
||||
@@ -482,20 +470,20 @@ export default function ModelPlatformApp() {
|
||||
if (!requestIsCurrent()) return;
|
||||
|
||||
if (!active) {
|
||||
active = await acquireFileLock(script);
|
||||
active = await api.acquireFileLock(script);
|
||||
newlyAcquired = true;
|
||||
}
|
||||
if (!requestIsCurrent()) {
|
||||
if (active) {
|
||||
await releaseFileLock(active);
|
||||
await api.releaseFileLock(active);
|
||||
clearSessionIfActive(active);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const ticket = await createJupyterAccessTicket(active);
|
||||
const ticket = await api.createJupyterAccessTicket(active);
|
||||
if (!requestIsCurrent()) {
|
||||
await releaseFileLock(active);
|
||||
await api.releaseFileLock(active);
|
||||
clearSessionIfActive(active);
|
||||
return;
|
||||
}
|
||||
@@ -516,7 +504,7 @@ export default function ModelPlatformApp() {
|
||||
} catch (error) {
|
||||
if (newlyAcquired && active) {
|
||||
try {
|
||||
await releaseFileLock(active);
|
||||
await api.releaseFileLock(active);
|
||||
} catch {
|
||||
// The database lease is the final safety net if compensation cannot reach Runtime.
|
||||
}
|
||||
@@ -573,7 +561,7 @@ export default function ModelPlatformApp() {
|
||||
}
|
||||
setEditBusy(true);
|
||||
try {
|
||||
await releaseFileLock(active);
|
||||
await api.releaseFileLock(active);
|
||||
setEmbeddedJupyterUrl(null);
|
||||
setEditSession(null);
|
||||
editSessionRef.current = null;
|
||||
@@ -632,7 +620,7 @@ export default function ModelPlatformApp() {
|
||||
if (!publishTarget) return;
|
||||
setPublishing(true);
|
||||
try {
|
||||
const version = await publishScriptVersion({
|
||||
const version = await api.publishScriptVersion({
|
||||
script: publishTarget,
|
||||
releaseNote,
|
||||
visibility: publishVisibility,
|
||||
@@ -662,7 +650,7 @@ export default function ModelPlatformApp() {
|
||||
if (!form.name.trim()) return;
|
||||
setCreating(true);
|
||||
try {
|
||||
const created = await createScript(form);
|
||||
const created = await api.createScript(form);
|
||||
setScripts((items) => [created, ...items]);
|
||||
selectScript(created.script_id);
|
||||
setCreateOpen(false);
|
||||
@@ -718,7 +706,7 @@ export default function ModelPlatformApp() {
|
||||
let lastCreated: ScriptItem | null = null;
|
||||
try {
|
||||
for (const file of files) {
|
||||
lastCreated = await uploadScript(file, uploadParentPath);
|
||||
lastCreated = await api.uploadScript(file, uploadParentPath);
|
||||
}
|
||||
await load(true);
|
||||
if (lastCreated) selectScript(lastCreated.script_id);
|
||||
@@ -744,7 +732,7 @@ export default function ModelPlatformApp() {
|
||||
if (!folderDialog.name.trim()) return;
|
||||
setFolderDialog((current) => ({ ...current, busy: true }));
|
||||
try {
|
||||
await createWorkspaceDirectory(
|
||||
await api.createWorkspaceDirectory(
|
||||
folderDialog.name.trim(),
|
||||
folderDialog.parentPath,
|
||||
);
|
||||
@@ -778,7 +766,7 @@ export default function ModelPlatformApp() {
|
||||
if (editSessionRef.current?.script_id === script.script_id) return;
|
||||
}
|
||||
try {
|
||||
await deleteScript(script.script_id);
|
||||
await api.deleteScript(script.script_id);
|
||||
if (selectedIdRef.current === script.script_id) selectScript(null);
|
||||
await load(true);
|
||||
setToast({
|
||||
@@ -812,7 +800,7 @@ export default function ModelPlatformApp() {
|
||||
if (editSessionRef.current?.script_id === activeScript.script_id) return;
|
||||
}
|
||||
try {
|
||||
const result = await deleteWorkspaceDirectory(path);
|
||||
const result = await api.deleteWorkspaceDirectory(path);
|
||||
const selectedScript = scripts.find(
|
||||
(item) => item.script_id === selectedIdRef.current,
|
||||
);
|
||||
@@ -911,36 +899,29 @@ export default function ModelPlatformApp() {
|
||||
{apiOnline ? "服务已连接" : "服务未连接"}
|
||||
</div>
|
||||
<div className="topbar-menu-wrap">
|
||||
<button className="workspace-switcher" type="button" onClick={() => { setWorkspaceMenuOpen((value) => !value); setUserMenuOpen(false); }}>
|
||||
<button className="workspace-switcher" type="button" onClick={() => { setWorkspaceMenuOpen((value) => !value); }}>
|
||||
<span className="workspace-switcher__icon"><Icon name="workspace" size={18} /></span>
|
||||
<span><small>当前 Workspace</small><strong>{demoContext.workspaceName}</strong></span>
|
||||
<span><small>当前 Workspace</small><strong>{currentWorkspace.workspace_name}</strong></span>
|
||||
<Icon name="chevron" size={15} />
|
||||
</button>
|
||||
{workspaceMenuOpen && (
|
||||
<div className="topbar-dropdown">
|
||||
{demoWorkspaces.map((workspace) => (
|
||||
<button className={workspace.workspaceId === demoContext.workspaceId ? "is-selected" : ""} type="button" key={workspace.workspaceId} onClick={() => { setDemoContext({ workspace }); window.location.reload(); }}>
|
||||
<Icon name="workspace" size={15} /><span><strong>{workspace.workspaceName}</strong><small>{workspace.workspaceId === demoContext.workspaceId ? "当前使用" : "点击切换"}</small></span>
|
||||
{workspaces.map((workspace) => (
|
||||
<button className={workspace.workspace_id === currentWorkspace.workspace_id ? "is-selected" : ""} type="button" key={workspace.workspace_id} onClick={() => { setCurrentWorkspace(workspace.workspace_id); setWorkspaceMenuOpen(false); }}>
|
||||
<Icon name="workspace" size={15} /><span><strong>{workspace.workspace_name}</strong><small>{workspace.workspace_id === currentWorkspace.workspace_id ? "当前使用" : "点击切换"}</small></span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="topbar-menu-wrap">
|
||||
<button className="user-menu" type="button" onClick={() => { setUserMenuOpen((value) => !value); setWorkspaceMenuOpen(false); }}>
|
||||
<span className="avatar">{demoContext.userName.slice(0, 1)}</span>
|
||||
<span className="user-menu__copy"><strong>{demoContext.userName}</strong><small>{demoContext.roleName}</small></span>
|
||||
<Icon name="chevron" size={15} />
|
||||
<button className="user-menu" type="button">
|
||||
<span className="avatar">{user?.display_name?.slice(0, 1) ?? "?"}</span>
|
||||
<span className="user-menu__copy"><strong>{user?.display_name ?? "未知用户"}</strong><small>{user?.role_code === "admin" ? "管理员" : "开发人员"}</small></span>
|
||||
</button>
|
||||
<button className="text-button" type="button" onClick={() => { logout(); window.location.assign("/login"); }}>
|
||||
登出
|
||||
</button>
|
||||
{userMenuOpen && (
|
||||
<div className="topbar-dropdown topbar-dropdown--users">
|
||||
{demoUsers.map((user) => (
|
||||
<button className={user.userId === demoContext.userId ? "is-selected" : ""} type="button" key={user.userId} onClick={() => { setDemoContext({ user }); window.location.reload(); }}>
|
||||
<span className="avatar">{user.userName.slice(0, 1)}</span><span><strong>{user.userName}</strong><small>{user.roleName} · {user.username}</small></span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1009,18 +990,18 @@ export default function ModelPlatformApp() {
|
||||
<>
|
||||
{memberScriptGroups.map((group) => (
|
||||
<WorkspaceTreeGroup
|
||||
key={group.user.userId}
|
||||
title={`${group.user.userName}的文件`}
|
||||
key={group.user?.user_id ?? "anon"}
|
||||
title={`${group.user?.display_name}的文件`}
|
||||
scripts={group.scripts}
|
||||
directories={group.directories}
|
||||
selectedId={selectedId}
|
||||
onSelect={selectScript}
|
||||
onContextMenu={
|
||||
group.user.userId === demoContext.userId
|
||||
group.user?.user_id === user?.user_id
|
||||
? showContextMenu
|
||||
: undefined
|
||||
}
|
||||
readOnly={group.user.userId !== demoContext.userId}
|
||||
readOnly={group.user?.user_id !== user?.user_id}
|
||||
/>
|
||||
))}
|
||||
{filteredScripts.length === 0 && (
|
||||
@@ -1105,13 +1086,13 @@ export default function ModelPlatformApp() {
|
||||
</section>
|
||||
) : activePage === "schedules" ? (
|
||||
<SchedulePage
|
||||
key={`${demoContext.userId}-${demoContext.workspaceId}`}
|
||||
key={`${user?.user_id ?? "anon"}-${currentWorkspace?.workspace_id ?? "none"}`}
|
||||
onNotify={setToast}
|
||||
onConnectionChange={setApiOnline}
|
||||
/>
|
||||
) : activePage === "system" ? (
|
||||
<SystemAdminPage
|
||||
key={`${demoContext.userId}-${demoContext.workspaceId}`}
|
||||
key={`${user?.user_id ?? "anon"}-${currentWorkspace?.workspace_id ?? "none"}`}
|
||||
onNotify={setToast}
|
||||
onConnectionChange={setApiOnline}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
DragEvent,
|
||||
FormEvent,
|
||||
MouseEvent as ReactMouseEvent,
|
||||
PointerEvent as ReactPointerEvent,
|
||||
type DragEvent,
|
||||
type FormEvent,
|
||||
type MouseEvent as ReactMouseEvent,
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
@@ -11,22 +11,6 @@ import {
|
||||
|
||||
import {
|
||||
ApiRequestError,
|
||||
createSchedule,
|
||||
createScheduleEdge,
|
||||
createScheduleNode,
|
||||
deleteSchedule,
|
||||
deleteScheduleEdge,
|
||||
deleteScheduleNode,
|
||||
hideScheduleArtifact,
|
||||
getSchedule,
|
||||
listScheduleArtifacts,
|
||||
listScheduleRuns,
|
||||
listSchedules,
|
||||
previewCron,
|
||||
runScheduleNow,
|
||||
updateSchedule,
|
||||
updateScheduleNode,
|
||||
validateSchedule,
|
||||
type CronPreview,
|
||||
type Schedule,
|
||||
type ScheduleArtifact,
|
||||
@@ -34,6 +18,8 @@ import {
|
||||
type ScheduleNode,
|
||||
type ScheduleRunSummary,
|
||||
} from "../../services/api";
|
||||
|
||||
import { useApi } from "~/context/AuthContext";
|
||||
import Icon from "../../components/Icon";
|
||||
import "../../styles/schedule.css";
|
||||
|
||||
@@ -250,6 +236,7 @@ export default function SchedulePage({
|
||||
const dragRef = useRef<DragState | null>(null);
|
||||
const positionDraftsRef = useRef<Record<string, NodePositionDraft>>({});
|
||||
const [positionDraftCount, setPositionDraftCount] = useState(0);
|
||||
const api = useApi();
|
||||
|
||||
const selectedNode = schedule?.nodes.find(
|
||||
(item) => item.node_id === selectedNodeId,
|
||||
@@ -328,7 +315,7 @@ export default function SchedulePage({
|
||||
): Promise<void> => {
|
||||
if (showLoading) setRunsLoading(true);
|
||||
try {
|
||||
const items = await listScheduleRuns({
|
||||
const items = await api.listScheduleRuns({
|
||||
scheduleId,
|
||||
limit: 20,
|
||||
});
|
||||
@@ -349,8 +336,8 @@ export default function SchedulePage({
|
||||
preferredScheduleId?: string | null,
|
||||
): Promise<void> => {
|
||||
const [scheduleItems, artifactItems] = await Promise.all([
|
||||
listSchedules(),
|
||||
listScheduleArtifacts(),
|
||||
api.listSchedules(),
|
||||
api.listScheduleArtifacts(),
|
||||
]);
|
||||
setSchedules(scheduleItems);
|
||||
setArtifacts(artifactItems);
|
||||
@@ -362,20 +349,20 @@ export default function SchedulePage({
|
||||
setSchedule(null);
|
||||
return;
|
||||
}
|
||||
const detail = await getSchedule(targetId);
|
||||
const detail = await api.getSchedule(targetId);
|
||||
setSchedule(applyPositionDrafts(detail));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
Promise.all([listSchedules(), listScheduleArtifacts()])
|
||||
Promise.all([api.listSchedules(), api.listScheduleArtifacts()])
|
||||
.then(async ([scheduleItems, artifactItems]) => {
|
||||
if (cancelled) return;
|
||||
setSchedules(scheduleItems);
|
||||
setArtifacts(artifactItems);
|
||||
if (scheduleItems[0]) {
|
||||
const detail = await getSchedule(scheduleItems[0].schedule_id);
|
||||
const detail = await api.getSchedule(scheduleItems[0].schedule_id);
|
||||
if (!cancelled) setSchedule(applyPositionDrafts(detail));
|
||||
}
|
||||
onConnectionChange(true);
|
||||
@@ -404,7 +391,7 @@ export default function SchedulePage({
|
||||
}
|
||||
let cancelled = false;
|
||||
setRunsLoading(true);
|
||||
listScheduleRuns({ scheduleId, limit: 20 })
|
||||
api.listScheduleRuns({ scheduleId, limit: 20 })
|
||||
.then((items) => {
|
||||
if (!cancelled) setRuns(items);
|
||||
})
|
||||
@@ -507,7 +494,7 @@ export default function SchedulePage({
|
||||
setLinkSourceId(null);
|
||||
setCronResult(null);
|
||||
try {
|
||||
setSchedule(await getSchedule(scheduleId));
|
||||
setSchedule(await api.getSchedule(scheduleId));
|
||||
onConnectionChange(true);
|
||||
} catch (error) {
|
||||
await handleError(error, "调度详情加载失败");
|
||||
@@ -529,7 +516,7 @@ export default function SchedulePage({
|
||||
if (busy || !scheduleName) return;
|
||||
setBusy("create-schedule");
|
||||
try {
|
||||
const created = await createSchedule({
|
||||
const created = await api.createSchedule({
|
||||
schedule_name: scheduleName,
|
||||
description: "在画布中拖入稳定版本并配置执行顺序",
|
||||
trigger_type: "manual",
|
||||
@@ -557,7 +544,7 @@ export default function SchedulePage({
|
||||
if (!window.confirm(`确定删除调度“${selectedSchedule.schedule_name}”吗?`)) return;
|
||||
setBusy("delete-schedule");
|
||||
try {
|
||||
await deleteSchedule(
|
||||
await api.deleteSchedule(
|
||||
selectedSchedule.schedule_id,
|
||||
selectedSchedule.workflow_version,
|
||||
);
|
||||
@@ -571,7 +558,7 @@ export default function SchedulePage({
|
||||
setSelectedNodeId(null);
|
||||
setSelectedEdgeId(null);
|
||||
if (remaining[0]) {
|
||||
setSchedule(await getSchedule(remaining[0].schedule_id));
|
||||
setSchedule(await api.getSchedule(remaining[0].schedule_id));
|
||||
}
|
||||
}
|
||||
onNotify({ tone: "success", message: "调度方案已删除" });
|
||||
@@ -589,7 +576,7 @@ export default function SchedulePage({
|
||||
if (!scheduleName || scheduleName === target.schedule_name) return;
|
||||
setBusy("rename-schedule");
|
||||
try {
|
||||
const updated = await updateSchedule(target.schedule_id, {
|
||||
const updated = await api.updateSchedule(target.schedule_id, {
|
||||
workflow_version: target.workflow_version,
|
||||
schedule_name: scheduleName,
|
||||
});
|
||||
@@ -620,7 +607,7 @@ export default function SchedulePage({
|
||||
) return;
|
||||
setBusy("delete-artifact");
|
||||
try {
|
||||
await hideScheduleArtifact(artifact.versions_id);
|
||||
await api.hideScheduleArtifact(artifact.versions_id);
|
||||
setArtifacts((current) => current.filter(
|
||||
(item) => item.versions_id !== artifact.versions_id,
|
||||
));
|
||||
@@ -652,13 +639,13 @@ export default function SchedulePage({
|
||||
for (const [nodeId, position] of Object.entries(
|
||||
positionDraftsRef.current,
|
||||
)) {
|
||||
updated = await updateScheduleNode(updated.schedule_id, nodeId, {
|
||||
updated = await api.updateScheduleNode(updated.schedule_id, nodeId, {
|
||||
workflow_version: updated.workflow_version,
|
||||
position_x: position.position_x,
|
||||
position_y: position.position_y,
|
||||
});
|
||||
}
|
||||
updated = await updateSchedule(updated.schedule_id, {
|
||||
updated = await api.updateSchedule(updated.schedule_id, {
|
||||
workflow_version: updated.workflow_version,
|
||||
schedule_name: scheduleForm.scheduleName.trim(),
|
||||
description: scheduleForm.description.trim() || null,
|
||||
@@ -694,7 +681,7 @@ export default function SchedulePage({
|
||||
if (busy) return;
|
||||
setBusy("cron-preview");
|
||||
try {
|
||||
const result = await previewCron({
|
||||
const result = await api.previewCron({
|
||||
cron_expression: scheduleForm.cronExpression.trim(),
|
||||
timezone: scheduleForm.timezone.trim(),
|
||||
count: 5,
|
||||
@@ -727,7 +714,7 @@ export default function SchedulePage({
|
||||
}
|
||||
setBusy("run-now");
|
||||
try {
|
||||
const created = await runScheduleNow(schedule.schedule_id);
|
||||
const created = await api.runScheduleNow(schedule.schedule_id);
|
||||
setRuns((current) => [
|
||||
created,
|
||||
...current.filter((item) => item.run_id !== created.run_id),
|
||||
@@ -763,7 +750,7 @@ export default function SchedulePage({
|
||||
const nodeKey = artifactNodeKey(artifact, schedule);
|
||||
const updated = await withMutation(
|
||||
"add-node",
|
||||
() => createScheduleNode(schedule.schedule_id, {
|
||||
() => api.createScheduleNode(schedule.schedule_id, {
|
||||
workflow_version: schedule.workflow_version,
|
||||
node_key: nodeKey,
|
||||
node_name: artifact.script_name,
|
||||
@@ -898,7 +885,7 @@ export default function SchedulePage({
|
||||
setLinkSourceId(null);
|
||||
await withMutation(
|
||||
"create-edge",
|
||||
() => createScheduleEdge(schedule.schedule_id, {
|
||||
() => api.createScheduleEdge(schedule.schedule_id, {
|
||||
workflow_version: schedule.workflow_version,
|
||||
source_node_id: sourceId,
|
||||
target_node_id: targetNodeId,
|
||||
@@ -935,7 +922,7 @@ export default function SchedulePage({
|
||||
);
|
||||
await withMutation(
|
||||
"save-node",
|
||||
() => updateScheduleNode(schedule.schedule_id, selectedNode.node_id, {
|
||||
() => api.updateScheduleNode(schedule.schedule_id, selectedNode.node_id, {
|
||||
workflow_version: schedule.workflow_version,
|
||||
node_name: nodeForm.nodeName.trim(),
|
||||
timeout_seconds: timeoutSeconds,
|
||||
@@ -958,7 +945,7 @@ export default function SchedulePage({
|
||||
if (!window.confirm(`确定删除节点“${node.node_name}”吗?`)) return;
|
||||
const updated = await withMutation(
|
||||
"delete-node",
|
||||
() => deleteScheduleNode(
|
||||
() => api.deleteScheduleNode(
|
||||
schedule.schedule_id,
|
||||
node.node_id,
|
||||
schedule.workflow_version,
|
||||
@@ -974,7 +961,7 @@ export default function SchedulePage({
|
||||
setContextMenu(null);
|
||||
const updated = await withMutation(
|
||||
"delete-edge",
|
||||
() => deleteScheduleEdge(
|
||||
() => api.deleteScheduleEdge(
|
||||
schedule.schedule_id,
|
||||
edge.edge_id,
|
||||
schedule.workflow_version,
|
||||
@@ -988,7 +975,7 @@ export default function SchedulePage({
|
||||
if (!schedule || busy) return;
|
||||
setBusy("validate");
|
||||
try {
|
||||
const result = await validateSchedule(schedule.schedule_id);
|
||||
const result = await api.validateSchedule(schedule.schedule_id);
|
||||
setSchedule((current) => current
|
||||
? { ...current, dag_validation: result }
|
||||
: current);
|
||||
|
||||
Reference in New Issue
Block a user