feat: auth

This commit is contained in:
tao.chen
2026-07-31 17:22:25 +08:00
parent 1994937349
commit fb073c6f99
24 changed files with 1177 additions and 742 deletions
+20 -14
View File
@@ -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);
+6 -1
View File
@@ -8,6 +8,7 @@ import {
} from "react-router";
import type { Route } from "./+types/root";
import { AuthProvider } from "~/context/AuthContext";
import "./app.css";
export const links: Route.LinksFunction = () => [
@@ -42,7 +43,11 @@ export function Layout({ children }: { children: React.ReactNode }) {
}
export default function App() {
return <Outlet />;
return (
<AuthProvider>
<Outlet />
</AuthProvider>
);
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
+5 -2
View File
@@ -1,3 +1,6 @@
import { type RouteConfig, route } from "@react-router/dev/routes";
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [route("*", "routes/platform.tsx")] satisfies RouteConfig;
export default [
route("/login", "routes/login.tsx"),
index("routes/platform.tsx"),
] satisfies RouteConfig;
+423 -215
View File
@@ -1,67 +1,15 @@
export type DemoUser = {
userId: string;
userName: string;
username: string;
roleCode: "admin" | "developer";
roleName: string;
};
export type DemoWorkspace = {
workspaceId: string;
workspaceName: string;
};
export const demoUsers: DemoUser[] = [
{ userId: "0000000000RF6FG1SDBXG59S13", userName: "张三", username: "admin-zhang", roleCode: "admin", roleName: "管理员" },
{ userId: "0000000000H2QYCGPCWQM1JSGS", userName: "李四", username: "admin-li", roleCode: "admin", roleName: "管理员" },
{ userId: "0000000000RWG40ESZPGJT629J", userName: "王五", username: "dev-wang", roleCode: "developer", roleName: "开发人员" },
{ userId: "00000000004CQV7WASJA6N6FW4", userName: "赵六", username: "dev-zhao", roleCode: "developer", roleName: "开发人员" },
];
export const demoWorkspaces: DemoWorkspace[] = [
{ workspaceId: "00000000000BM630VT9ARVFZPC", workspaceName: "模型开发 Workspace" },
{ workspaceId: "0000000000AE0NC0V5T424KK86", workspaceName: "风险验证 Workspace" },
];
function readStoredContext(): Partial<{
userId: string;
workspaceId: string;
}> {
if (typeof window === "undefined") return {};
try {
return JSON.parse(
window.localStorage.getItem("model-platform-demo-context") ?? "{}",
) as Partial<{ userId: string; workspaceId: string }>;
} catch {
return {};
}
}
const storedContext = readStoredContext();
const initialUser = demoUsers.find((item) => item.userId === storedContext.userId)
?? demoUsers[0];
const initialWorkspace = demoWorkspaces.find(
(item) => item.workspaceId === storedContext.workspaceId,
) ?? demoWorkspaces[0];
export const demoContext = {
...initialUser,
...initialWorkspace,
};
export function setDemoContext(input: {
user?: DemoUser;
workspace?: DemoWorkspace;
}): void {
if (input.user) Object.assign(demoContext, input.user);
if (input.workspace) Object.assign(demoContext, input.workspace);
if (typeof window !== "undefined") {
window.localStorage.setItem("model-platform-demo-context", JSON.stringify({
userId: demoContext.userId,
workspaceId: demoContext.workspaceId,
}));
}
}
// API client for the platform backend.
//
// All endpoints that take a workspace context require the caller to
// pass `workspaceId` explicitly. Components read the active workspace
// from `useAuth().currentWorkspace` and thread it through; the cookie
// set by `/api/v1/auth/login` is sent automatically thanks to
// `credentials: "same-origin"`, and the backend reads it via the
// shared `request_context` dependency.
//
// 401 from any endpoint means the session has expired or was never
// established; the global `apiRequest` helper bounces the user to
// `/login` so the platform never tries to render with a stale identity.
export type ScriptType = "python" | "notebook";
export type Visibility = "private" | "workspace" | "public";
@@ -132,22 +80,40 @@ export class ApiRequestError extends Error {
}
}
function appendWorkspaceId(path: string, workspaceId: string): string {
// `path` may already contain a query string. Use URLSearchParams to
// merge cleanly either way.
const separator = path.includes("?") ? "&" : "?";
return `${path}${separator}workspace_id=${encodeURIComponent(workspaceId)}`;
}
async function apiRequest<T>(
path: string,
init: RequestInit = {},
workspaceId?: string,
): Promise<T> {
const response = await fetch(path, {
const finalPath = workspaceId ? appendWorkspaceId(path, workspaceId) : path;
const response = await fetch(finalPath, {
...init,
credentials: "same-origin",
headers: {
"X-User-ID": demoContext.userId,
"X-Workspace-ID": demoContext.workspaceId,
"X-Request-ID": crypto.randomUUID().replaceAll("-", ""),
...(init.body ? { "Content-Type": "application/json" } : {}),
...init.headers,
},
});
// Session expired / never authenticated — bounce to login. The
// /login route itself is the only path that must remain reachable
// while anonymous, so the redirect there is safe.
if (response.status === 401 && typeof window !== "undefined") {
const here = window.location.pathname;
if (here !== "/login") {
window.location.assign("/login");
}
throw new ApiRequestError("未登录或登录已过期", 401);
}
const payload = (await response.json().catch(() => ({}))) as
| ApiEnvelope<T>
| ApiErrorEnvelope;
@@ -171,8 +137,8 @@ async function apiRequest<T>(
return (payload as ApiEnvelope<T>).data;
}
export async function listScripts(): Promise<ScriptItem[]> {
return apiRequest<ScriptItem[]>("/api/v1/scripts");
export async function listScripts(workspaceId: string): Promise<ScriptItem[]> {
return apiRequest<ScriptItem[]>("/api/v1/scripts", {}, workspaceId);
}
function initialContent(scriptType: ScriptType): string {
@@ -197,14 +163,14 @@ function initialContent(scriptType: ScriptType): string {
{
cell_type: "markdown",
metadata: {},
source: ["# 新建模型实验\\n", "在这里开始数据探索与模型构建。"],
source: ["# 新建模型实验\n", "在这里开始数据探索与模型构建。"],
},
{
cell_type: "code",
execution_count: null,
metadata: {},
outputs: [],
source: ["print('Hello, Model Platform!')\\n"],
source: ["print('Hello, Model Platform!')\n"],
},
],
metadata: {
@@ -226,25 +192,33 @@ function initialContent(scriptType: ScriptType): string {
);
}
export async function createScript(input: {
name: string;
scriptType: ScriptType;
visibility: Visibility;
parentPath?: string | null;
}): Promise<ScriptItem> {
return apiRequest<ScriptItem>("/api/v1/scripts", {
method: "POST",
body: JSON.stringify({
script_name: input.name.trim(),
script_type: input.scriptType,
visibility: input.visibility,
content: initialContent(input.scriptType),
parent_path: input.parentPath,
}),
});
export async function createScript(
workspaceId: string,
input: {
name: string;
scriptType: ScriptType;
visibility: Visibility;
parentPath?: string | null;
},
): Promise<ScriptItem> {
return apiRequest<ScriptItem>(
"/api/v1/scripts",
{
method: "POST",
body: JSON.stringify({
script_name: input.name.trim(),
script_type: input.scriptType,
visibility: input.visibility,
content: initialContent(input.scriptType),
parent_path: input.parentPath,
}),
},
workspaceId,
);
}
export async function uploadScript(
workspaceId: string,
file: File,
parentPath = "",
visibility: Visibility = "workspace",
@@ -261,38 +235,64 @@ export async function uploadScript(
headers: { "Content-Type": "application/octet-stream" },
body: file,
},
workspaceId,
);
}
export async function updateScript(
workspaceId: string,
scriptId: string,
input: { content: string },
): Promise<ScriptItem> {
return apiRequest<ScriptItem>(
`/api/v1/scripts/${scriptId}`,
{ method: "PUT", body: JSON.stringify(input) },
workspaceId,
);
}
export async function deleteScript(
workspaceId: string,
scriptId: string,
): Promise<{ script_id: string; status: string; versions_preserved: boolean }> {
return apiRequest(`/api/v1/scripts/${scriptId}`, { method: "DELETE" });
return apiRequest(
`/api/v1/scripts/${scriptId}`,
{ method: "DELETE" },
workspaceId,
);
}
export async function listWorkspaceDirectories(): Promise<
WorkspaceDirectory[]
> {
export async function listWorkspaceDirectories(
workspaceId: string,
): Promise<WorkspaceDirectory[]> {
const data = await apiRequest<{ directories: WorkspaceDirectory[] }>(
"/api/v1/workspace-tree",
{},
workspaceId,
);
return data.directories;
}
export async function createWorkspaceDirectory(
workspaceId: string,
directoryName: string,
parentPath = "",
): Promise<WorkspaceDirectory> {
return apiRequest<WorkspaceDirectory>("/api/v1/workspace-directories", {
method: "POST",
body: JSON.stringify({
directory_name: directoryName,
parent_path: parentPath,
}),
});
return apiRequest<WorkspaceDirectory>(
"/api/v1/workspace-directories",
{
method: "POST",
body: JSON.stringify({
directory_name: directoryName,
parent_path: parentPath,
}),
},
workspaceId,
);
}
export async function deleteWorkspaceDirectory(
workspaceId: string,
path: string,
): Promise<{
path: string;
@@ -301,9 +301,11 @@ export async function deleteWorkspaceDirectory(
versions_preserved: boolean;
}> {
const parameters = new URLSearchParams({ path });
return apiRequest(`/api/v1/workspace-directories?${parameters.toString()}`, {
method: "DELETE",
});
return apiRequest(
`/api/v1/workspace-directories?${parameters.toString()}`,
{ method: "DELETE" },
workspaceId,
);
}
export type FileLockSession = {
@@ -353,12 +355,20 @@ export type StableVersion = {
created_at: string;
};
// Note: the file-lock and jupyter-ticket endpoints are not yet
// implemented in the backend (see the cookie+JWT auth refactor plan).
// They are retained here so the editor UI keeps its existing call
// sites, but they will return 404 until the backend ships the
// corresponding routes.
export async function acquireFileLock(
workspaceId: string,
script: ScriptItem,
): Promise<ActiveEditSession> {
const session = await apiRequest<FileLockSession>(
`/api/v1/files/${script.current_object_id}/lock`,
{ method: "POST" },
workspaceId,
);
if (!session.lock_token) {
throw new Error("加锁成功响应缺少 lock_token");
@@ -372,6 +382,7 @@ export async function acquireFileLock(
}
export async function heartbeatFileLock(
workspaceId: string,
session: ActiveEditSession,
): Promise<FileLockSession> {
return apiRequest<FileLockSession>(
@@ -380,10 +391,12 @@ export async function heartbeatFileLock(
method: "POST",
body: JSON.stringify({ lock_token: session.lock_token }),
},
workspaceId,
);
}
export async function releaseFileLock(
workspaceId: string,
session: ActiveEditSession,
): Promise<FileLockSession> {
return apiRequest<FileLockSession>(
@@ -392,47 +405,64 @@ export async function releaseFileLock(
method: "DELETE",
body: JSON.stringify({ lock_token: session.lock_token }),
},
workspaceId,
);
}
export function releaseFileLockOnUnload(session: ActiveEditSession): void {
void fetch(`/api/v1/file-locks/${session.edit_session_id}`, {
method: "DELETE",
credentials: "same-origin",
keepalive: true,
headers: {
"Content-Type": "application/json",
"X-User-ID": demoContext.userId,
"X-Workspace-ID": demoContext.workspaceId,
"X-Request-ID": crypto.randomUUID().replaceAll("-", ""),
export function releaseFileLockOnUnload(
workspaceId: string,
session: ActiveEditSession,
): void {
void fetch(
`/api/v1/file-locks/${session.edit_session_id}?workspace_id=${
encodeURIComponent(workspaceId)
}`,
{
method: "DELETE",
credentials: "same-origin",
keepalive: true,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ lock_token: session.lock_token }),
},
body: JSON.stringify({ lock_token: session.lock_token }),
});
);
}
export async function createJupyterAccessTicket(
workspaceId: string,
session: ActiveEditSession,
): Promise<JupyterAccessTicket> {
return apiRequest<JupyterAccessTicket>("/api/v1/jupyter/access-tickets", {
method: "POST",
body: JSON.stringify({
edit_session_id: session.edit_session_id,
lock_token: session.lock_token,
}),
});
return apiRequest<JupyterAccessTicket>(
"/api/v1/jupyter/access-tickets",
{
method: "POST",
body: JSON.stringify({
edit_session_id: session.edit_session_id,
lock_token: session.lock_token,
}),
},
workspaceId,
);
}
export async function listScriptVersions(
workspaceId: string,
scriptId: string,
): Promise<StableVersion[]> {
return apiRequest<StableVersion[]>(`/api/v1/scripts/${scriptId}/versions`);
return apiRequest<StableVersion[]>(
`/api/v1/scripts/${scriptId}/versions`,
{},
workspaceId,
);
}
export async function publishScriptVersion(input: {
script: ScriptItem;
releaseNote: string;
visibility: Visibility;
}): Promise<StableVersion> {
export async function publishScriptVersion(
workspaceId: string,
input: {
script: ScriptItem;
releaseNote: string;
visibility: Visibility;
},
): Promise<StableVersion> {
return apiRequest<StableVersion>(
`/api/v1/scripts/${input.script.script_id}/versions`,
{
@@ -443,6 +473,7 @@ export async function publishScriptVersion(input: {
visibility: input.visibility,
}),
},
workspaceId,
);
}
@@ -595,31 +626,43 @@ export type ScheduleRunDetail = ScheduleRunSummary & {
node_runs: ScheduleNodeRun[];
};
export async function listSchedules(): Promise<Schedule[]> {
return apiRequest<Schedule[]>("/api/v1/schedules");
export async function listSchedules(workspaceId: string): Promise<Schedule[]> {
return apiRequest<Schedule[]>("/api/v1/schedules", {}, workspaceId);
}
export async function getSchedule(scheduleId: string): Promise<Schedule> {
return apiRequest<Schedule>(`/api/v1/schedules/${scheduleId}`);
export async function getSchedule(
workspaceId: string,
scheduleId: string,
): Promise<Schedule> {
return apiRequest<Schedule>(
`/api/v1/schedules/${scheduleId}`,
{},
workspaceId,
);
}
export async function createSchedule(input: {
schedule_name: string;
description?: string | null;
trigger_type?: "manual" | "cron" | "api";
cron_expression?: string | null;
timezone?: string;
enabled?: boolean;
max_concurrency?: number;
failure_policy?: "stop" | "continue";
}): Promise<Schedule> {
return apiRequest<Schedule>("/api/v1/schedules", {
method: "POST",
body: JSON.stringify(input),
});
export async function createSchedule(
workspaceId: string,
input: {
schedule_name: string;
description?: string | null;
trigger_type?: "manual" | "cron" | "api";
cron_expression?: string | null;
timezone?: string;
enabled?: boolean;
max_concurrency?: number;
failure_policy?: "stop" | "continue";
},
): Promise<Schedule> {
return apiRequest<Schedule>(
"/api/v1/schedules",
{ method: "POST", body: JSON.stringify(input) },
workspaceId,
);
}
export async function updateSchedule(
workspaceId: string,
scheduleId: string,
input: {
workflow_version: number;
@@ -633,55 +676,72 @@ export async function updateSchedule(
failure_policy?: "stop" | "continue";
},
): Promise<Schedule> {
return apiRequest<Schedule>(`/api/v1/schedules/${scheduleId}`, {
method: "PATCH",
body: JSON.stringify(input),
});
return apiRequest<Schedule>(
`/api/v1/schedules/${scheduleId}`,
{ method: "PATCH", body: JSON.stringify(input) },
workspaceId,
);
}
export async function deleteSchedule(
workspaceId: string,
scheduleId: string,
workflowVersion: number,
): Promise<{ schedule_id: string; deleted: boolean; workflow_version: number }> {
return apiRequest(`/api/v1/schedules/${scheduleId}`, {
method: "DELETE",
body: JSON.stringify({ workflow_version: workflowVersion }),
});
return apiRequest(
`/api/v1/schedules/${scheduleId}`,
{ method: "DELETE", body: JSON.stringify({ workflow_version: workflowVersion }) },
workspaceId,
);
}
export async function listScheduleArtifacts(): Promise<ScheduleArtifact[]> {
return apiRequest<ScheduleArtifact[]>("/api/v1/schedule-artifacts");
export async function listScheduleArtifacts(
workspaceId: string,
): Promise<ScheduleArtifact[]> {
return apiRequest<ScheduleArtifact[]>(
"/api/v1/schedule-artifacts",
{},
workspaceId,
);
}
export async function hideScheduleArtifact(
workspaceId: string,
versionsId: string,
): Promise<{
versions_id: string;
deleted: boolean;
artifact_preserved: boolean;
}> {
return apiRequest(`/api/v1/versions/${versionsId}`, {
method: "DELETE",
});
return apiRequest(
`/api/v1/versions/${versionsId}`,
{ method: "DELETE" },
workspaceId,
);
}
export async function listEmployees(): Promise<Employee[]> {
return apiRequest<Employee[]>("/api/v1/admin/employees");
export async function listEmployees(workspaceId: string): Promise<Employee[]> {
return apiRequest<Employee[]>("/api/v1/admin/employees", {}, workspaceId);
}
export async function createEmployee(input: {
username: string;
display_name: string;
email?: string | null;
role_code: "admin" | "developer";
}): Promise<Employee> {
return apiRequest<Employee>("/api/v1/admin/employees", {
method: "POST",
body: JSON.stringify(input),
});
export async function createEmployee(
workspaceId: string,
input: {
username: string;
display_name: string;
email?: string | null;
role_code: "admin" | "developer";
},
): Promise<Employee> {
return apiRequest<Employee>(
"/api/v1/admin/employees",
{ method: "POST", body: JSON.stringify(input) },
workspaceId,
);
}
export async function updateEmployee(
workspaceId: string,
userId: string,
input: {
display_name?: string;
@@ -690,21 +750,26 @@ export async function updateEmployee(
status?: "active" | "disabled" | "locked";
},
): Promise<Employee> {
return apiRequest<Employee>(`/api/v1/admin/employees/${userId}`, {
method: "PATCH",
body: JSON.stringify(input),
});
return apiRequest<Employee>(
`/api/v1/admin/employees/${userId}`,
{ method: "PATCH", body: JSON.stringify(input) },
workspaceId,
);
}
export async function deleteEmployee(
workspaceId: string,
userId: string,
): Promise<{ user_id: string; deleted: boolean }> {
return apiRequest(`/api/v1/admin/employees/${userId}`, {
method: "DELETE",
});
return apiRequest(
`/api/v1/admin/employees/${userId}`,
{ method: "DELETE" },
workspaceId,
);
}
export async function createScheduleNode(
workspaceId: string,
scheduleId: string,
input: {
workflow_version: number;
@@ -720,13 +785,15 @@ export async function createScheduleNode(
env_refs_json?: Record<string, string>;
},
): Promise<Schedule> {
return apiRequest<Schedule>(`/api/v1/schedules/${scheduleId}/nodes`, {
method: "POST",
body: JSON.stringify(input),
});
return apiRequest<Schedule>(
`/api/v1/schedules/${scheduleId}/nodes`,
{ method: "POST", body: JSON.stringify(input) },
workspaceId,
);
}
export async function updateScheduleNode(
workspaceId: string,
scheduleId: string,
nodeId: string,
input: {
@@ -744,28 +811,26 @@ export async function updateScheduleNode(
): Promise<Schedule> {
return apiRequest<Schedule>(
`/api/v1/schedules/${scheduleId}/nodes/${nodeId}`,
{
method: "PUT",
body: JSON.stringify(input),
},
{ method: "PUT", body: JSON.stringify(input) },
workspaceId,
);
}
export async function deleteScheduleNode(
workspaceId: string,
scheduleId: string,
nodeId: string,
workflowVersion: number,
): Promise<Schedule> {
return apiRequest<Schedule>(
`/api/v1/schedules/${scheduleId}/nodes/${nodeId}`,
{
method: "DELETE",
body: JSON.stringify({ workflow_version: workflowVersion }),
},
{ method: "DELETE", body: JSON.stringify({ workflow_version: workflowVersion }) },
workspaceId,
);
}
export async function createScheduleEdge(
workspaceId: string,
scheduleId: string,
input: {
workflow_version: number;
@@ -774,50 +839,58 @@ export async function createScheduleEdge(
condition_expr?: string | null;
},
): Promise<Schedule> {
return apiRequest<Schedule>(`/api/v1/schedules/${scheduleId}/edges`, {
method: "POST",
body: JSON.stringify(input),
});
return apiRequest<Schedule>(
`/api/v1/schedules/${scheduleId}/edges`,
{ method: "POST", body: JSON.stringify(input) },
workspaceId,
);
}
export async function deleteScheduleEdge(
workspaceId: string,
scheduleId: string,
edgeId: string,
workflowVersion: number,
): Promise<Schedule> {
return apiRequest<Schedule>(
`/api/v1/schedules/${scheduleId}/edges/${edgeId}`,
{
method: "DELETE",
body: JSON.stringify({ workflow_version: workflowVersion }),
},
{ method: "DELETE", body: JSON.stringify({ workflow_version: workflowVersion }) },
workspaceId,
);
}
export async function validateSchedule(
workspaceId: string,
scheduleId: string,
): Promise<DagValidation & {
schedule_id: string;
workflow_version: number;
}> {
return apiRequest(`/api/v1/schedules/${scheduleId}/validate`, {
method: "POST",
});
return apiRequest(
`/api/v1/schedules/${scheduleId}/validate`,
{ method: "POST" },
workspaceId,
);
}
export async function previewCron(input: {
cron_expression: string;
timezone: string;
count?: number;
base_time?: string;
}): Promise<CronPreview> {
return apiRequest<CronPreview>("/api/v1/cron/preview", {
method: "POST",
body: JSON.stringify(input),
});
export async function previewCron(
workspaceId: string,
input: {
cron_expression: string;
timezone: string;
count?: number;
base_time?: string;
},
): Promise<CronPreview> {
return apiRequest<CronPreview>(
"/api/v1/cron/preview",
{ method: "POST", body: JSON.stringify(input) },
workspaceId,
);
}
export async function runScheduleNow(
workspaceId: string,
scheduleId: string,
): Promise<ScheduleRunDetail> {
return apiRequest<ScheduleRunDetail>(
@@ -829,25 +902,160 @@ export async function runScheduleNow(
},
body: JSON.stringify({ reason: "manual_run" }),
},
workspaceId,
);
}
export async function listScheduleRuns(input: {
scheduleId?: string;
status?: ScheduleRunStatus;
limit?: number;
} = {}): Promise<ScheduleRunSummary[]> {
export async function listScheduleRuns(
workspaceId: string,
input: {
scheduleId?: string;
status?: ScheduleRunStatus;
limit?: number;
} = {},
): Promise<ScheduleRunSummary[]> {
const query = new URLSearchParams();
if (input.scheduleId) query.set("schedule_id", input.scheduleId);
if (input.status) query.set("status", input.status);
query.set("limit", String(input.limit ?? 20));
return apiRequest<ScheduleRunSummary[]>(
`/api/v1/schedule-runs?${query.toString()}`,
{},
workspaceId,
);
}
export async function getScheduleRun(
workspaceId: string,
runId: string,
): Promise<ScheduleRunDetail> {
return apiRequest<ScheduleRunDetail>(`/api/v1/schedule-runs/${runId}`);
return apiRequest<ScheduleRunDetail>(
`/api/v1/schedule-runs/${runId}`,
{},
workspaceId,
);
}
// ----------------------------------------------------------------------------
// Workspace-bound API surface.
//
// `useApi()` in ~/context/AuthContext returns an object where every
// function has had its first `workspaceId` argument pre-filled. The
// type below lets consumers import the bound type without depending
// on the raw functions. Keep this last in the file so the type
// references all the exports above.
// ----------------------------------------------------------------------------
export type WorkspaceBoundApi = {
listScripts: () => Promise<ScriptItem[]>;
createScript: (
input: Parameters<typeof createScript>[1],
) => Promise<ScriptItem>;
uploadScript: (
file: File,
parentPath?: string,
visibility?: Visibility,
) => Promise<ScriptItem>;
updateScript: (
scriptId: string,
input: Parameters<typeof updateScript>[1],
) => Promise<ScriptItem>;
deleteScript: (
scriptId: string,
) => Promise<{ script_id: string; status: string; versions_preserved: boolean }>;
listWorkspaceDirectories: () => Promise<WorkspaceDirectory[]>;
createWorkspaceDirectory: (
directoryName: string,
parentPath?: string,
) => Promise<WorkspaceDirectory>;
deleteWorkspaceDirectory: (
path: string,
) => Promise<{
path: string;
status: string;
deleted_scripts: number;
versions_preserved: boolean;
}>;
acquireFileLock: (
script: ScriptItem,
) => Promise<ActiveEditSession>;
heartbeatFileLock: (
session: ActiveEditSession,
) => Promise<FileLockSession>;
releaseFileLock: (
session: ActiveEditSession,
) => Promise<FileLockSession>;
releaseFileLockOnUnload: (session: ActiveEditSession) => void;
createJupyterAccessTicket: (
session: ActiveEditSession,
) => Promise<JupyterAccessTicket>;
listScriptVersions: (scriptId: string) => Promise<StableVersion[]>;
publishScriptVersion: (
input: Parameters<typeof publishScriptVersion>[1],
) => Promise<StableVersion>;
listSchedules: () => Promise<Schedule[]>;
getSchedule: (scheduleId: string) => Promise<Schedule>;
createSchedule: (
input: Parameters<typeof createSchedule>[1],
) => Promise<Schedule>;
updateSchedule: (
scheduleId: string,
input: Parameters<typeof updateSchedule>[2],
) => Promise<Schedule>;
deleteSchedule: (
scheduleId: string,
workflowVersion: number,
) => Promise<{ schedule_id: string; deleted: boolean; workflow_version: number }>;
listScheduleArtifacts: () => Promise<ScheduleArtifact[]>;
hideScheduleArtifact: (
versionsId: string,
) => Promise<{
versions_id: string;
deleted: boolean;
artifact_preserved: boolean;
}>;
listEmployees: () => Promise<Employee[]>;
createEmployee: (
input: Parameters<typeof createEmployee>[1],
) => Promise<Employee>;
updateEmployee: (
userId: string,
input: Parameters<typeof updateEmployee>[2],
) => Promise<Employee>;
deleteEmployee: (
userId: string,
) => Promise<{ user_id: string; deleted: boolean }>;
createScheduleNode: (
scheduleId: string,
input: Parameters<typeof createScheduleNode>[2],
) => Promise<Schedule>;
updateScheduleNode: (
scheduleId: string,
nodeId: string,
input: Parameters<typeof updateScheduleNode>[3],
) => Promise<Schedule>;
deleteScheduleNode: (
scheduleId: string,
nodeId: string,
workflowVersion: number,
) => Promise<Schedule>;
createScheduleEdge: (
scheduleId: string,
input: Parameters<typeof createScheduleEdge>[2],
) => Promise<Schedule>;
deleteScheduleEdge: (
scheduleId: string,
edgeId: string,
workflowVersion: number,
) => Promise<Schedule>;
validateSchedule: (
scheduleId: string,
) => Promise<DagValidation & { schedule_id: string; workflow_version: number }>;
previewCron: (
input: Parameters<typeof previewCron>[1],
) => Promise<CronPreview>;
runScheduleNow: (scheduleId: string) => Promise<ScheduleRunDetail>;
listScheduleRuns: (
input?: Parameters<typeof listScheduleRuns>[1],
) => Promise<ScheduleRunSummary[]>;
getScheduleRun: (runId: string) => Promise<ScheduleRunDetail>;
};