diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 35410ca..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# 默认忽略的文件 -/shelf/ -/workspace.xml -# 基于编辑器的 HTTP 客户端请求 -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 2eea525..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -.env \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 862d09b..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/model-develop-refactored.iml b/.idea/model-develop-refactored.iml deleted file mode 100644 index d6ebd48..0000000 --- a/.idea/model-develop-refactored.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 4cd2323..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.pnpm-store/v11/index.db b/.pnpm-store/v11/index.db deleted file mode 100644 index 044636a..0000000 Binary files a/.pnpm-store/v11/index.db and /dev/null differ diff --git a/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/.dockerignore b/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/.dockerignore deleted file mode 100644 index 9b8d514..0000000 --- a/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -.react-router -build -node_modules -README.md \ No newline at end of file diff --git a/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/.gitignore b/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/.gitignore deleted file mode 100644 index 039ee62..0000000 --- a/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -.DS_Store -.env -/node_modules/ - -# React Router -/.react-router/ -/build/ diff --git a/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/README.md b/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/README.md deleted file mode 100644 index a3844c1..0000000 --- a/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Frontend - -React Router SPA,已将原前端按功能拆分到 `app/`: - -```text -app/components/ 通用图标组件 -app/features/platform/ 主框架、脚本工作区和 Jupyter 编辑 -app/features/schedules/ DAG 调度页面 -app/features/admin/ 工作台与系统管理 -app/routes/ React Router 路由入口 -app/services/ API 请求、DTO 和演示上下文 -app/styles/ 分功能样式 -``` - -本地开发: - -```bash -pnpm install -pnpm dev -``` - -生产构建由根目录 `nginx/Dockerfile` 完成,构建结果复制到 Nginx 静态目录。 diff --git a/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/app/app.css b/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/app/app.css deleted file mode 100644 index 928fa83..0000000 --- a/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/app/app.css +++ /dev/null @@ -1,11 +0,0 @@ -:root { - font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; - color: #1f354b; - background: #f3f6f9; - font-synthesis: none; - text-rendering: optimizeLegibility; -} - -* { box-sizing: border-box; } -html, body { margin: 0; min-width: 1024px; min-height: 100%; } -button, input, select, textarea { font: inherit; } diff --git a/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/app/components/Icon.tsx b/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/app/components/Icon.tsx deleted file mode 100644 index ec50beb..0000000 --- a/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/app/components/Icon.tsx +++ /dev/null @@ -1,183 +0,0 @@ -type IconName = - | "brand" - | "home" - | "script" - | "schedule" - | "experiment" - | "database" - | "settings" - | "chevron" - | "search" - | "plus" - | "upload" - | "folder" - | "notebook" - | "python" - | "refresh" - | "close" - | "check" - | "info" - | "workspace" - | "menu" - | "external" - | "release" - | "play"; - -type IconProps = { - name: IconName; - size?: number; - strokeWidth?: number; -}; - -export default function Icon({ - name, - size = 18, - strokeWidth = 1.8, -}: IconProps) { - const common = { - width: size, - height: size, - viewBox: "0 0 24 24", - fill: "none", - xmlns: "http://www.w3.org/2000/svg", - "aria-hidden": true, - }; - - if (name === "brand") { - return ( - - - - - - ); - } - - const paths: Record, React.ReactNode> = { - home: ( - <> - - - - ), - script: ( - <> - - - - ), - schedule: ( - <> - - - - ), - experiment: ( - <> - - - - ), - database: ( - <> - - - - - ), - settings: ( - <> - - - - ), - chevron: , - search: ( - <> - - - - ), - plus: , - upload: ( - <> - - - - ), - folder: ( - - ), - notebook: ( - <> - - - - ), - python: ( - <> - - - - - - ), - refresh: ( - <> - - - - ), - close: , - check: , - info: ( - <> - - - - ), - workspace: ( - <> - - - - ), - menu: ( - <> - - - ), - external: ( - <> - - - - ), - release: ( - <> - - - - ), - play: , - }; - - return ( - - {paths[name]} - - ); -} diff --git a/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/app/features/admin/AdminPages.tsx b/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/app/features/admin/AdminPages.tsx deleted file mode 100644 index 1f7ad1d..0000000 --- a/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/app/features/admin/AdminPages.tsx +++ /dev/null @@ -1,256 +0,0 @@ -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 ( -
-
-
- MODEL DEVELOPMENT PLATFORM -

下午好,{demoContext.userName}

-

当前位于 {demoContext.workspaceName},可以继续构建脚本或配置调度。

-
- {online ? "服务正常" : "服务连接中"} -
-
-
{scriptCount}工作副本
-
2Workspace
-
4平台员工
-
{online ? "正常" : "检查中"}平台状态
-
-
- - - -
-
-
-
运行趋势

近 7 天调度执行

成功率 92.6%
-
- {[38, 55, 44, 73, 61, 86, 78].map((value, index) => ( -
- {Math.round(value / 7)} - - {["周一", "周二", "周三", "周四", "周五", "周六", "今天"][index]} -
- ))} -
-
成功 75失败 6
-
-
-
脚本资产

类型分布

-
-
{scriptCount}全部脚本
-
- Notebook{Math.max(1, Math.round(scriptCount * .67))} 个 · 67% - Python{Math.max(0, scriptCount - Math.round(scriptCount * .67))} 个 · 33% - 稳定版本3 个已发布 -
-
-
-
-
ACTIVITY

最近平台活动

-
-
操作内容执行人状态时间
- {[ - ["数据探索.ipynb 发布稳定版本 v3.0", "张三", "成功", "16:42"], - ["每日模型训练流程完成调度运行", "Scheduler", "成功", "15:25"], - ["批量预测.py 更新工作副本", "王五", "已同步", "14:18"], - ["风险验证流程完成 DAG 校验", "李四", "成功", "11:06"], - ].map((row) => ( -
- {row[0]} - {row[1]}{row[2]}{row[3]} -
- ))} -
-
-
-
- ); -} - - -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([]); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [editing, setEditing] = useState(null); - const [dialogOpen, setDialogOpen] = useState(false); - const [form, setForm] = useState(EMPTY_FORM); - const canManage = demoContext.roleCode === "admin"; - - const load = async (): Promise => { - 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 => { - 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 => { - 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 ( -
-
-
系统管理

员工管理

{demoContext.workspaceName} · {employees.length} 名员工

- -
- {!canManage &&
当前为开发人员,只能查看员工列表。
} -
-
员工账号角色状态操作
- {loading ?

正在加载员工…

: employees.map((employee) => { - const isProtectedAdmin = employee.role_code === "admin"; - return ( -
- {employee.display_name.slice(0, 1)}{employee.display_name}{employee.email ?? "未设置邮箱"} - {employee.username} - {employee.role_name} - {employee.status === "active" ? "正常" : employee.status === "disabled" ? "已停用" : "已锁定"} - - - - -
- ); - })} -
- - {dialogOpen && ( -
-
-
EMPLOYEE

{editing ? "编辑员工" : "添加员工"}

-
void submit(event)}> - - - - - {editing && } -
-
-
-
- )} -
- ); -} diff --git a/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/app/features/platform/ModelPlatformApp.tsx b/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/app/features/platform/ModelPlatformApp.tsx deleted file mode 100644 index 73a3b9c..0000000 --- a/.pnpm-store/v11/projects/2dad80fd1dced2194639b53db6e470b3/app/features/platform/ModelPlatformApp.tsx +++ /dev/null @@ -1,1965 +0,0 @@ -import { - type ChangeEvent, - FormEvent, - type MouseEvent as ReactMouseEvent, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -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, - type StableVersion, - type Visibility, - type WorkspaceDirectory, -} from "../../services/api"; -import Icon from "../../components/Icon"; -import SchedulePage from "../schedules/SchedulePage"; -import { DashboardPage, SystemAdminPage } from "../admin/AdminPages"; -import "../../styles/platform.css"; - - -type NewScriptForm = { - name: string; - scriptType: ScriptType; - visibility: Visibility; - parentPath: string; -}; - -type ContextMenuState = { - x: number; - y: number; - kind: "root" | "directory" | "file"; - path: string; - script?: ScriptItem; -}; - -type ToastState = { - tone: "success" | "error" | "info"; - message: string; -}; - -const navigation = [ - { label: "工作台", icon: "home" as const, page: "home" as const }, - { label: "构建脚本", icon: "script" as const, page: "scripts" as const }, - { label: "调度配置", icon: "schedule" as const, page: "schedules" as const }, - { label: "系统管理", icon: "settings" as const, page: "system" as const }, -]; - -type ActivePage = "home" | "scripts" | "schedules" | "system"; - -function pageFromPath(pathname: string): ActivePage { - const page = pathname.replace(/^\/+|\/+$/g, ""); - return ["scripts", "schedules", "system"].includes(page) - ? page as ActivePage - : "home"; -} - -function pathForPage(page: ActivePage): string { - return page === "home" ? "/workbench" : `/${page}`; -} - -const initialForm: NewScriptForm = { - name: "", - scriptType: "notebook", - visibility: "workspace", - parentPath: "", -}; - -function formatTime(value: string) { - return new Intl.DateTimeFormat("zh-CN", { - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - hour12: false, - }).format(new Date(value)); -} - -function formatBytes(value: number) { - if (value < 1024) return `${value} B`; - return `${(value / 1024).toFixed(1)} KB`; -} - -function shortHash(value: string) { - return value ? `${value.slice(0, 8)}…${value.slice(-6)}` : "—"; -} - -function scriptIcon(item: ScriptItem) { - return item.script_type === "notebook" ? "notebook" : "python"; -} - -function ownedScriptPath(item: ScriptItem) { - return item.relative_path.replaceAll("\\", "/").split("/").slice(2).join("/"); -} - -function parentOf(path: string) { - const parts = path.split("/"); - parts.pop(); - return parts.join("/"); -} - -function inferredDirectories(items: ScriptItem[]): WorkspaceDirectory[] { - const result = new Map(); - for (const item of items) { - const parts = parentOf(ownedScriptPath(item)).split("/").filter(Boolean); - let parentPath = ""; - for (const name of parts) { - const path = parentPath ? `${parentPath}/${name}` : name; - result.set(path, { path, name, parent_path: parentPath }); - parentPath = path; - } - } - return [...result.values()]; -} - -function mergeDirectories( - left: WorkspaceDirectory[], - right: WorkspaceDirectory[], -): WorkspaceDirectory[] { - return [...new Map( - [...left, ...right].map((item) => [item.path, item]), - ).values()]; -} - -function confineJupyterFrame(frame: HTMLIFrameElement): void { - try { - const document = frame.contentDocument; - if (!document?.documentElement) return; - const keepInside = (): void => { - document.querySelectorAll("button,[role='button']").forEach( - (element) => { - const label = `${element.getAttribute("aria-label") ?? ""} ${ - element.getAttribute("title") ?? "" - } ${element.textContent ?? ""}`.trim(); - if (/^open in\b/i.test(label) || /\bopen in\.\.\./i.test(label)) { - element.style.setProperty("display", "none", "important"); - } - }, - ); - document.querySelectorAll("a[target]").forEach((link) => { - if (["_blank", "_top", "_parent"].includes(link.target)) { - link.target = "_self"; - } - }); - }; - keepInside(); - new MutationObserver(keepInside).observe(document.documentElement, { - childList: true, - subtree: true, - }); - document.addEventListener("click", (event) => { - const target = event.target as HTMLElement | null; - const link = target?.closest?.("a") as HTMLAnchorElement | null; - if (link && ["_blank", "_top", "_parent"].includes(link.target)) { - link.target = "_self"; - } - }, true); - } catch { - // The iframe remains sandboxed even if its document is not yet accessible. - } -} - -export default function ModelPlatformApp() { - const location = useLocation(); - const navigate = useNavigate(); - const activePage = pageFromPath(location.pathname); - const [scripts, setScripts] = useState([]); - const [directories, setDirectories] = useState([]); - const [selectedId, setSelectedId] = useState(null); - const [keyword, setKeyword] = useState(""); - const [loading, setLoading] = useState(true); - const [refreshing, setRefreshing] = useState(false); - const [apiOnline, setApiOnline] = useState(false); - const [createOpen, setCreateOpen] = useState(false); - const [creating, setCreating] = useState(false); - const [form, setForm] = useState(initialForm); - const [folderDialog, setFolderDialog] = useState<{ - open: boolean; - parentPath: string; - name: string; - busy: boolean; - }>({ open: false, parentPath: "", name: "", busy: false }); - const [contextMenu, setContextMenu] = useState(null); - const [workspaceMenuOpen, setWorkspaceMenuOpen] = useState(false); - const [userMenuOpen, setUserMenuOpen] = useState(false); - const [uploadParentPath, setUploadParentPath] = useState(""); - const [uploading, setUploading] = useState(false); - const uploadInputRef = useRef(null); - const [toast, setToast] = useState(null); - const [editSession, setEditSession] = useState(null); - const editSessionRef = useRef(null); - const selectedIdRef = useRef(null); - const editorOpenRequestRef = useRef(0); - const editorOpeningRef = useRef(false); - const [embeddedJupyterUrl, setEmbeddedJupyterUrl] = - useState(null); - const [editBusy, setEditBusy] = useState(false); - const [editorOpenError, setEditorOpenError] = useState<{ - scriptId: string; - message: string; - } | null>(null); - const [versions, setVersions] = useState([]); - const [versionsLoading, setVersionsLoading] = useState(false); - const [publishTarget, setPublishTarget] = useState(null); - const [releaseNote, setReleaseNote] = useState(""); - const [publishVisibility, setPublishVisibility] = - useState("workspace"); - const [publishing, setPublishing] = useState(false); - const [publishedVersion, setPublishedVersion] = - useState(null); - - const load = async (silent = false) => { - if (!silent) setLoading(true); - setRefreshing(silent); - try { - const [items, folderItems] = await Promise.all([ - listScripts(), - listWorkspaceDirectories(), - ]); - setScripts(items); - setDirectories(folderItems); - setApiOnline(true); - setSelectedId((current) => { - if (current && items.some((item) => item.script_id === current)) { - selectedIdRef.current = current; - return current; - } - const nextSelectedId = ( - items.find((item) => item.script_type === "notebook")?.script_id - ?? items[0]?.script_id - ?? null - ); - selectedIdRef.current = nextSelectedId; - return nextSelectedId; - }); - } catch (error) { - setApiOnline(false); - setToast({ - tone: "error", - message: error instanceof Error ? error.message : "脚本列表加载失败", - }); - } finally { - setLoading(false); - setRefreshing(false); - } - }; - - useEffect(() => { - void load(); - }, []); - - useEffect(() => { - if (!toast) return; - const timer = window.setTimeout(() => setToast(null), 3200); - return () => window.clearTimeout(timer); - }, [toast]); - - useEffect(() => { - if (!contextMenu) return; - const close = () => setContextMenu(null); - const closeOnEscape = (event: KeyboardEvent) => { - if (event.key === "Escape") close(); - }; - window.addEventListener("pointerdown", close); - window.addEventListener("blur", close); - window.addEventListener("resize", close); - window.addEventListener("scroll", close, true); - window.addEventListener("keydown", closeOnEscape); - return () => { - window.removeEventListener("pointerdown", close); - window.removeEventListener("blur", close); - window.removeEventListener("resize", close); - window.removeEventListener("scroll", close, true); - window.removeEventListener("keydown", closeOnEscape); - }; - }, [contextMenu]); - - useEffect(() => { - editSessionRef.current = editSession; - }, [editSession]); - - useEffect(() => { - if (!selectedId) { - setVersions([]); - return; - } - let ignore = false; - setVersionsLoading(true); - void listScriptVersions(selectedId) - .then((items) => { - if (!ignore) setVersions(items); - }) - .catch((error) => { - if (!ignore) { - setToast({ - tone: "error", - message: error instanceof Error ? error.message : "版本列表加载失败", - }); - } - }) - .finally(() => { - if (!ignore) setVersionsLoading(false); - }); - return () => { - ignore = true; - }; - }, [selectedId]); - - useEffect(() => { - if (!editSession) return; - const intervalSeconds = Math.max( - 5, - editSession.heartbeat_interval_seconds || 15, - ); - let heartbeatRunning = false; - const timer = window.setInterval(() => { - if (heartbeatRunning) return; - const current = editSessionRef.current; - if (!current || current.edit_session_id !== editSession.edit_session_id) { - return; - } - heartbeatRunning = true; - void heartbeatFileLock(current) - .then((updated) => { - setEditSession((active) => active - && active.edit_session_id === updated.edit_session_id - ? { - ...active, - session_status: updated.session_status, - expires_at: updated.expires_at, - } - : active); - }) - .catch((error) => { - setEditSession(null); - setEmbeddedJupyterUrl(null); - setToast({ - tone: "error", - message: `编辑锁心跳已中断:${ - error instanceof Error ? error.message : "请重新打开文件" - }`, - }); - }) - .finally(() => { - heartbeatRunning = false; - }); - }, intervalSeconds * 1000); - return () => window.clearInterval(timer); - }, [editSession?.edit_session_id, editSession?.heartbeat_interval_seconds]); - - useEffect(() => { - if (!editSession?.ticket_expires_at) return; - const expiresAt = new Date(editSession.ticket_expires_at).getTime(); - const renewAfter = Math.max(15_000, expiresAt - Date.now() - 60_000); - const timer = window.setTimeout(() => { - const current = editSessionRef.current; - if (!current || current.edit_session_id !== editSession.edit_session_id) { - return; - } - void createJupyterAccessTicket(current) - .then((ticket) => { - setEditSession((active) => active - && active.edit_session_id === ticket.edit_session_id - ? { ...active, ticket_expires_at: ticket.expires_at } - : active); - }) - .catch((error) => { - setToast({ - tone: "error", - message: `Jupyter 访问票据续签失败:${ - error instanceof Error ? error.message : "请重新打开文件" - }`, - }); - }); - }, renewAfter); - return () => window.clearTimeout(timer); - }, [editSession?.edit_session_id, editSession?.ticket_expires_at]); - - useEffect(() => { - if (!editSession) return; - const handleUnload = () => { - const current = editSessionRef.current; - if (current) releaseFileLockOnUnload(current); - }; - window.addEventListener("beforeunload", handleUnload); - return () => window.removeEventListener("beforeunload", handleUnload); - }, [editSession?.edit_session_id]); - - const filteredScripts = useMemo(() => { - const normalized = keyword.trim().toLocaleLowerCase(); - if (!normalized) return scripts; - return scripts.filter((item) => - item.script_name.toLocaleLowerCase().includes(normalized), - ); - }, [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 inferred = inferredDirectories(memberScripts); - return { - user, - scripts: memberScripts, - directories: user.userId === demoContext.userId - ? mergeDirectories(directories, inferred) - : inferred, - }; - }); - const selected = scripts.find((item) => item.script_id === selectedId) ?? null; - - const selectScript = (scriptId: string | null) => { - if (selectedIdRef.current !== scriptId) { - editorOpenRequestRef.current += 1; - setEditorOpenError(null); - } - selectedIdRef.current = scriptId; - setSelectedId(scriptId); - }; - - const openScriptEditor = async ( - script: ScriptItem, - showToast = true, - ) => { - if (editorOpeningRef.current) return; - editorOpeningRef.current = true; - const requestId = editorOpenRequestRef.current + 1; - editorOpenRequestRef.current = requestId; - const requestIsCurrent = () => - editorOpenRequestRef.current === requestId - && selectedIdRef.current === script.script_id; - const clearSessionIfActive = (session: ActiveEditSession) => { - if ( - editSessionRef.current?.edit_session_id === session.edit_session_id - ) { - setEmbeddedJupyterUrl(null); - setEditSession(null); - editSessionRef.current = null; - } - }; - - setEditBusy(true); - setEditorOpenError((current) => - current?.scriptId === script.script_id ? null : current); - let active = editSessionRef.current; - let newlyAcquired = false; - try { - if (active && active.script_id !== script.script_id) { - await releaseFileLock(active); - setEmbeddedJupyterUrl(null); - setEditSession(null); - editSessionRef.current = null; - active = null; - } - if (!requestIsCurrent()) return; - - if (!active) { - active = await acquireFileLock(script); - newlyAcquired = true; - } - if (!requestIsCurrent()) { - if (active) { - await releaseFileLock(active); - clearSessionIfActive(active); - } - return; - } - - const ticket = await createJupyterAccessTicket(active); - if (!requestIsCurrent()) { - await releaseFileLock(active); - clearSessionIfActive(active); - return; - } - - const readySession = { - ...active, - ticket_expires_at: ticket.expires_at, - }; - setEditSession(readySession); - editSessionRef.current = readySession; - setEmbeddedJupyterUrl(ticket.jupyter_url); - if (showToast) { - setToast({ - tone: "success", - message: `${script.script_name} 已打开,Jupyter 已嵌入当前工作区`, - }); - } - } catch (error) { - if (newlyAcquired && active) { - try { - await releaseFileLock(active); - } catch { - // The database lease is the final safety net if compensation cannot reach Runtime. - } - setEditSession(null); - editSessionRef.current = null; - } - setEmbeddedJupyterUrl(null); - if (requestIsCurrent()) { - const message = error instanceof Error ? error.message : "打开编辑器失败"; - setEditorOpenError({ scriptId: script.script_id, message }); - if (showToast) { - setToast({ tone: "error", message }); - } - } - } finally { - editorOpeningRef.current = false; - setEditBusy(false); - } - }; - - useEffect(() => { - if ( - activePage !== "scripts" - || !selected - || selected.script_type !== "notebook" - || editBusy - || editorOpenError?.scriptId === selected.script_id - || ( - editSession?.script_id === selected.script_id - && embeddedJupyterUrl - ) - ) { - return; - } - - void openScriptEditor(selected, false); - }, [ - activePage, - editBusy, - editSession?.script_id, - editorOpenError?.scriptId, - embeddedJupyterUrl, - selected?.script_id, - selected?.script_type, - ]); - - const endEditing = async (closeTab = false, showToast = true) => { - editorOpenRequestRef.current += 1; - setEditorOpenError(null); - const active = editSessionRef.current; - if (!active) { - if (closeTab) selectScript(null); - return; - } - setEditBusy(true); - try { - await releaseFileLock(active); - setEmbeddedJupyterUrl(null); - setEditSession(null); - editSessionRef.current = null; - if (closeTab) selectScript(null); - if (showToast) { - setToast({ - tone: "success", - message: `${active.script_name} 的编辑锁已释放`, - }); - } - } catch (error) { - setToast({ - tone: "error", - message: error instanceof Error ? error.message : "释放编辑锁失败", - }); - } finally { - setEditBusy(false); - } - }; - - useEffect(() => { - const active = editSessionRef.current; - if ( - !active - || !selected - || selected.script_type === "notebook" - || selected.script_id === active.script_id - || editBusy - ) { - return; - } - void endEditing(false, false); - }, [editBusy, selected?.script_id, selected?.script_type]); - - useEffect(() => { - if ( - activePage === "scripts" - || editBusy - || (!editSessionRef.current && !editorOpeningRef.current) - ) { - return; - } - void endEditing(true, false); - }, [activePage, editBusy]); - - const openPublishDialog = (script: ScriptItem) => { - setPublishTarget(script); - setReleaseNote(""); - setPublishVisibility( - script.visibility === "private" ? "private" : "workspace", - ); - }; - - const submitPublish = async (event: FormEvent) => { - event.preventDefault(); - if (!publishTarget) return; - setPublishing(true); - try { - const version = await publishScriptVersion({ - script: publishTarget, - releaseNote, - visibility: publishVisibility, - }); - setVersions((items) => [ - version, - ...items.filter((item) => item.versions_id !== version.versions_id), - ]); - setPublishTarget(null); - setPublishedVersion(version); - setToast({ - tone: "success", - message: `${version.version_label} 稳定版本发布成功`, - }); - } catch (error) { - setToast({ - tone: "error", - message: error instanceof Error ? error.message : "稳定版本发布失败", - }); - } finally { - setPublishing(false); - } - }; - - const submitCreate = async (event: FormEvent) => { - event.preventDefault(); - if (!form.name.trim()) return; - setCreating(true); - try { - const created = await createScript(form); - setScripts((items) => [created, ...items]); - selectScript(created.script_id); - setCreateOpen(false); - setForm(initialForm); - setToast({ - tone: "success", - message: `${created.script_name} 已创建`, - }); - } catch (error) { - setToast({ - tone: "error", - message: error instanceof Error ? error.message : "创建失败", - }); - } finally { - setCreating(false); - } - }; - - const openCreateDialog = ( - parentPath = "", - scriptType: ScriptType = "notebook", - ) => { - setContextMenu(null); - setForm({ - ...initialForm, - parentPath, - scriptType, - }); - setCreateOpen(true); - }; - - const openFolderDialog = (parentPath = "") => { - setContextMenu(null); - setFolderDialog({ - open: true, - parentPath, - name: "", - busy: false, - }); - }; - - const chooseUpload = (parentPath = "") => { - setContextMenu(null); - setUploadParentPath(parentPath); - uploadInputRef.current?.click(); - }; - - const handleUpload = async (event: ChangeEvent) => { - const files = Array.from(event.target.files ?? []); - event.target.value = ""; - if (files.length === 0) return; - setUploading(true); - let lastCreated: ScriptItem | null = null; - try { - for (const file of files) { - lastCreated = await uploadScript(file, uploadParentPath); - } - await load(true); - if (lastCreated) selectScript(lastCreated.script_id); - setToast({ - tone: "success", - message: `${files.length} 个文件已上传到${ - uploadParentPath ? ` ${uploadParentPath}` : "当前目录" - }`, - }); - } catch (error) { - await load(true); - setToast({ - tone: "error", - message: error instanceof Error ? error.message : "文件上传失败", - }); - } finally { - setUploading(false); - } - }; - - const submitFolder = async (event: FormEvent) => { - event.preventDefault(); - if (!folderDialog.name.trim()) return; - setFolderDialog((current) => ({ ...current, busy: true })); - try { - await createWorkspaceDirectory( - folderDialog.name.trim(), - folderDialog.parentPath, - ); - await load(true); - setFolderDialog({ - open: false, - parentPath: "", - name: "", - busy: false, - }); - setToast({ - tone: "success", - message: `${folderDialog.name.trim()} 文件夹已创建`, - }); - } catch (error) { - setFolderDialog((current) => ({ ...current, busy: false })); - setToast({ - tone: "error", - message: error instanceof Error ? error.message : "文件夹创建失败", - }); - } - }; - - const removeScript = async (script: ScriptItem) => { - setContextMenu(null); - if (!window.confirm(`确定删除文件“${script.script_name}”吗?稳定版本会保留。`)) { - return; - } - if (editSessionRef.current?.script_id === script.script_id) { - await endEditing(false, false); - if (editSessionRef.current?.script_id === script.script_id) return; - } - try { - await deleteScript(script.script_id); - if (selectedIdRef.current === script.script_id) selectScript(null); - await load(true); - setToast({ - tone: "success", - message: `${script.script_name} 已删除`, - }); - } catch (error) { - setToast({ - tone: "error", - message: error instanceof Error ? error.message : "文件删除失败", - }); - } - }; - - const removeDirectory = async (path: string) => { - setContextMenu(null); - if (!window.confirm(`确定递归删除文件夹“${path}”及其内容吗?稳定版本会保留。`)) { - return; - } - const activeScript = scripts.find( - (item) => item.script_id === editSessionRef.current?.script_id, - ); - if ( - activeScript - && ( - ownedScriptPath(activeScript) === path - || ownedScriptPath(activeScript).startsWith(`${path}/`) - ) - ) { - await endEditing(false, false); - if (editSessionRef.current?.script_id === activeScript.script_id) return; - } - try { - const result = await deleteWorkspaceDirectory(path); - const selectedScript = scripts.find( - (item) => item.script_id === selectedIdRef.current, - ); - if ( - selectedScript - && ownedScriptPath(selectedScript).startsWith(`${path}/`) - ) { - selectScript(null); - } - await load(true); - setToast({ - tone: "success", - message: `${path} 已删除(含 ${result.deleted_scripts} 个脚本)`, - }); - } catch (error) { - setToast({ - tone: "error", - message: error instanceof Error ? error.message : "文件夹删除失败", - }); - } - }; - - const showContextMenu = ( - event: ReactMouseEvent, - target: Omit, - ) => { - event.preventDefault(); - event.stopPropagation(); - const width = 188; - const height = target.kind === "file" ? 92 : 190; - setContextMenu({ - ...target, - x: Math.min(event.clientX, window.innerWidth - width - 8), - y: Math.min(event.clientY, window.innerHeight - height - 8), - }); - }; - - return ( -
- - -
-
-
- -
- 开发工作区 -

{{ - home: "工作台", - scripts: "构建脚本", - schedules: "调度配置", - system: "系统管理", - }[activePage]}

-
-
-
-
- - {apiOnline ? "服务已连接" : "服务未连接"} -
-
- - {workspaceMenuOpen && ( -
- {demoWorkspaces.map((workspace) => ( - - ))} -
- )} -
-
- - {userMenuOpen && ( -
- {demoUsers.map((user) => ( - - ))} -
- )} -
-
-
- - {activePage === "scripts" ? ( -
- - -
- {selected ? ( - void openScriptEditor(selected)} - onEndEditing={() => void endEditing()} - onClose={() => { - if ( - editSessionRef.current?.script_id === selected.script_id - ) { - void endEditing(true); - } else { - selectScript(null); - } - }} - onPublish={() => openPublishDialog(selected)} - onInfo={setToast} - /> - ) : ( -
-
- -
- 构建脚本工作台 -

创建你的第一个模型脚本

-

- 通过 Notebook 完成数据探索,或使用 Python - 脚本构建可调度的处理任务。 -

- -
- )} -
-
- ) : activePage === "schedules" ? ( - - ) : activePage === "system" ? ( - - ) : ( - { - navigate(pathForPage(page)); - }} - /> - )} -
- - {createOpen && ( -
-
-
-
- 工作副本 -

新建构建脚本

-
- -
-
-
- - 保存到:{form.parentPath || "个人根目录"} -
- - -
- 脚本类型 - - -
- - - -
- - -
-
-
-
- )} - - {folderDialog.open && ( -
-
-
-
- WORKSPACE -

新建文件夹

-
- -
-
-
- - 创建到:{folderDialog.parentPath || "个人根目录"} -
- -
- - -
-
-
-
- )} - - {contextMenu && ( -
event.stopPropagation()} - > - {contextMenu.kind === "file" && contextMenu.script ? ( - <> - - - - ) : ( - <> - - - - - {contextMenu.kind === "directory" && ( - <> - - - - )} - - )} -
- )} - - {publishTarget && ( -
-
-
-
- 不可变制品 -

发布稳定版本

-
- -
-
-
- - - - - {publishTarget.script_name} - 当前 Workspace 工作副本 - -
-