merge: integrate feat/auth into develop

This commit is contained in:
Winnie
2026-08-03 17:44:00 +08:00
38 changed files with 2590 additions and 796 deletions
@@ -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";
@@ -123,9 +106,27 @@ function mergeDirectories(
).values()];
}
export default function ModelPlatformApp() {
const { currentWorkspace } = useAuth();
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>
);
}
return <AuthenticatedModelPlatformApp />;
}
function AuthenticatedModelPlatformApp() {
const location = useLocation();
const navigate = useNavigate();
const activePage = pageFromPath(location.pathname);
const auth = useAuth();
const { user, workspaces, setCurrentWorkspace, logout } = auth;
const currentWorkspace = auth.currentWorkspace!;
const api = useApi();
const [scripts, setScripts] = useState<ScriptItem[]>([]);
const [directories, setDirectories] = useState<WorkspaceDirectory[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
@@ -144,7 +145,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);
@@ -176,8 +176,8 @@ export default function ModelPlatformApp() {
setRefreshing(silent);
try {
const [items, folderItems] = await Promise.all([
listScripts(),
listWorkspaceDirectories(),
api.listScripts(),
api.listWorkspaceDirectories(),
]);
setScripts(items);
setDirectories(folderItems);
@@ -248,7 +248,7 @@ export default function ModelPlatformApp() {
}
let ignore = false;
setVersionsLoading(true);
void listScriptVersions(selectedId)
void api.listScriptVersions(selectedId)
.then((items) => {
if (!ignore) setVersions(items);
})
@@ -282,7 +282,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
@@ -319,7 +319,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
@@ -342,7 +342,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);
@@ -356,24 +356,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) => {
@@ -413,7 +406,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;
@@ -422,20 +415,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;
}
@@ -456,7 +449,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.
}
@@ -513,7 +506,7 @@ export default function ModelPlatformApp() {
}
setEditBusy(true);
try {
await releaseFileLock(active);
await api.releaseFileLock(active);
setEmbeddedJupyterUrl(null);
setEditSession(null);
editSessionRef.current = null;
@@ -572,7 +565,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,
@@ -602,7 +595,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);
@@ -658,7 +651,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);
@@ -684,7 +677,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,
);
@@ -718,7 +711,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({
@@ -752,7 +745,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,
);
@@ -851,36 +844,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>
@@ -949,18 +935,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 && (
@@ -1045,13 +1031,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}
/>
@@ -1458,4 +1444,4 @@ export default function ModelPlatformApp() {
)}
</div>
);
}
}