update:多iframe
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -134,7 +134,7 @@ function AuthenticatedModelPlatformApp() {
|
|||||||
// Toast 通知
|
// Toast 通知
|
||||||
const [toast, setToast] = useState<ToastState | null>(null);
|
const [toast, setToast] = useState<ToastState | null>(null);
|
||||||
|
|
||||||
// 编辑会话相关
|
// 编辑会话相关 - 为每个标签维护独立 iframe 和状态
|
||||||
const [editSession, setEditSession] = useState<ActiveEditSession | null>(null);
|
const [editSession, setEditSession] = useState<ActiveEditSession | null>(null);
|
||||||
const editSessionRef = useRef<ActiveEditSession | null>(null);
|
const editSessionRef = useRef<ActiveEditSession | null>(null);
|
||||||
const selectedIdRef = useRef<string | null>(null);
|
const selectedIdRef = useRef<string | null>(null);
|
||||||
@@ -147,6 +147,14 @@ function AuthenticatedModelPlatformApp() {
|
|||||||
message: string;
|
message: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
|
// 为每个打开的标签维护独立的缓存会话(多 iframe 共存方案)
|
||||||
|
interface CachedSession {
|
||||||
|
session: ActiveEditSession;
|
||||||
|
jupyterUrl: string;
|
||||||
|
lastActiveTime: number; // 最后激活时间戳(用于清理)
|
||||||
|
}
|
||||||
|
const sessionCacheRef = useRef<Map<string, CachedSession>>(new Map());
|
||||||
|
|
||||||
// 版本发布相关
|
// 版本发布相关
|
||||||
const [latestVersion, setLatestVersion] = useState<LatestVersion | null>(null);
|
const [latestVersion, setLatestVersion] = useState<LatestVersion | null>(null);
|
||||||
const [latestVersionLoading, setLatestVersionLoading] = useState(false);
|
const [latestVersionLoading, setLatestVersionLoading] = useState(false);
|
||||||
@@ -171,27 +179,25 @@ function AuthenticatedModelPlatformApp() {
|
|||||||
setScripts(items);
|
setScripts(items);
|
||||||
setDirectories(folderItems);
|
setDirectories(folderItems);
|
||||||
setApiOnline(true);
|
setApiOnline(true);
|
||||||
|
// 刷新时保留选中的文件,不自动选中第一个
|
||||||
setSelectedId((current) => {
|
setSelectedId((current) => {
|
||||||
if (current && items.some((item) => item.script_id === current)) {
|
const validIds = new Set(items.map(item => item.script_id));
|
||||||
|
// 当前选中的文件还在 → 保持
|
||||||
|
if (current && validIds.has(current)) {
|
||||||
selectedIdRef.current = current;
|
selectedIdRef.current = current;
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
const nextSelectedId = (
|
// 否则设为 null(不自动选中)
|
||||||
items.find((item) => item.script_type === "notebook")?.script_id
|
selectedIdRef.current = null;
|
||||||
?? items[0]?.script_id
|
return null;
|
||||||
?? null
|
|
||||||
);
|
|
||||||
selectedIdRef.current = nextSelectedId;
|
|
||||||
return nextSelectedId;
|
|
||||||
});
|
});
|
||||||
// 同时更新打开的标签页
|
// 同时更新打开的标签页:保留有效标签,移除已删除的文件
|
||||||
setOpenTabIds((current) => {
|
setOpenTabIds((current) => {
|
||||||
const firstNotebook = items.find((item) => item.script_type === "notebook")?.script_id
|
const validIds = new Set(items.map(item => item.script_id));
|
||||||
?? items[0]?.script_id;
|
// 过滤掉已删除的文件
|
||||||
if (!firstNotebook) return [];
|
const preserved = current.filter(id => validIds.has(id));
|
||||||
// 如果当前标签页已经包含,则保持不变
|
// 直接返回(有就有,没有就没有,不自动补充)
|
||||||
if (current.includes(firstNotebook)) return current;
|
return preserved;
|
||||||
return [firstNotebook];
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setApiOnline(false);
|
setApiOnline(false);
|
||||||
@@ -318,32 +324,76 @@ function AuthenticatedModelPlatformApp() {
|
|||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer);
|
||||||
}, [editSession?.edit_session_id, editSession?.heartbeat_interval_seconds]);
|
}, [editSession?.edit_session_id, editSession?.heartbeat_interval_seconds]);
|
||||||
|
|
||||||
// Jupyter 访问票据续签
|
// 统一心跳管理:为所有缓存的会话维持心跳(包括隐藏的 iframe)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editSession?.ticket_expires_at) return;
|
const intervalSeconds = 15;
|
||||||
const expiresAt = new Date(editSession.ticket_expires_at).getTime();
|
let heartbeatRunning = false;
|
||||||
const renewAfter = Math.max(15_000, expiresAt - Date.now() - 60_000);
|
const timer = window.setInterval(() => {
|
||||||
const timer = window.setTimeout(() => {
|
if (heartbeatRunning) return;
|
||||||
const current = editSessionRef.current;
|
heartbeatRunning = true;
|
||||||
if (!current || current.edit_session_id !== editSession.edit_session_id) {
|
// 为所有缓存的会话调用心跳
|
||||||
return;
|
const promises: Promise<void>[] = [];
|
||||||
}
|
for (const cached of sessionCacheRef.current.values()) {
|
||||||
void api.createJupyterAccessTicket(current)
|
// 只更新缓存中的 session 对象,不更新 React 状态
|
||||||
.then((ticket) => {
|
promises.push(
|
||||||
setEditSession((active) => active
|
api.heartbeatFileLock(cached.session)
|
||||||
&& active.edit_session_id === ticket.edit_session_id
|
.then((updated) => {
|
||||||
? { ...active, ticket_expires_at: ticket.expires_at }
|
cached.session.session_status = updated.session_status;
|
||||||
: active);
|
cached.session.expires_at = updated.expires_at;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(() => {
|
||||||
|
// 不删除缓存,等待用户切回来时再处理
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Promise.allSettled(promises).finally(() => {
|
||||||
|
heartbeatRunning = false;
|
||||||
|
});
|
||||||
|
}, intervalSeconds * 1000);
|
||||||
|
return () => window.clearInterval(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 定时清理:10 分钟无活动的会话
|
||||||
|
useEffect(() => {
|
||||||
|
const TEN_MINUTES = 10 * 60 * 1000;
|
||||||
|
const CHECK_INTERVAL = 60 * 1000; // 每分钟检查一次
|
||||||
|
|
||||||
|
const cleanupTimer = window.setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
const toCleanup: string[] = [];
|
||||||
|
|
||||||
|
// 找出需要清理的会话
|
||||||
|
for (const [scriptId, cached] of sessionCacheRef.current.entries()) {
|
||||||
|
// 只清理非激活的会话
|
||||||
|
if (scriptId !== selectedIdRef.current) {
|
||||||
|
const inactiveTime = now - cached.lastActiveTime;
|
||||||
|
if (inactiveTime > TEN_MINUTES) {
|
||||||
|
toCleanup.push(scriptId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行清理
|
||||||
|
if (toCleanup.length > 0) {
|
||||||
|
for (const scriptId of toCleanup) {
|
||||||
|
const cached = sessionCacheRef.current.get(scriptId);
|
||||||
|
if (cached) {
|
||||||
|
// 释放编辑锁
|
||||||
|
api.releaseFileLock(cached.session).catch(console.warn);
|
||||||
|
// 从缓存删除
|
||||||
|
sessionCacheRef.current.delete(scriptId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 通知用户
|
||||||
setToast({
|
setToast({
|
||||||
tone: "error",
|
tone: "info",
|
||||||
message: `Jupyter 访问票据续签失败:${error instanceof Error ? error.message : "请重新打开文件"}`,
|
message: `已清理 ${toCleanup.length} 个长时间未活动的编辑会话`,
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}, renewAfter);
|
}, CHECK_INTERVAL);
|
||||||
return () => window.clearTimeout(timer);
|
|
||||||
}, [editSession?.edit_session_id, editSession?.ticket_expires_at]);
|
return () => window.clearInterval(cleanupTimer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 页面卸载时释放编辑锁
|
// 页面卸载时释放编辑锁
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -390,16 +440,23 @@ function AuthenticatedModelPlatformApp() {
|
|||||||
|
|
||||||
// 关闭标签
|
// 关闭标签
|
||||||
const closeTab = async (scriptId: string, event?: ReactMouseEvent) => {
|
const closeTab = async (scriptId: string, event?: ReactMouseEvent) => {
|
||||||
|
console.log('[closeTab] 开始关闭:', scriptId, '当前 selectedId:', selectedId);
|
||||||
event?.stopPropagation();
|
event?.stopPropagation();
|
||||||
if (editSessionRef.current?.script_id === scriptId) {
|
if (editSessionRef.current?.script_id === scriptId) {
|
||||||
|
console.log('[closeTab] 需要结束编辑');
|
||||||
await endEditing(false, false);
|
await endEditing(false, false);
|
||||||
}
|
}
|
||||||
|
// 从缓存中删除
|
||||||
|
sessionCacheRef.current.delete(scriptId);
|
||||||
setOpenTabIds((current) => {
|
setOpenTabIds((current) => {
|
||||||
|
console.log('[closeTab] 当前标签栏:', current);
|
||||||
const index = current.indexOf(scriptId);
|
const index = current.indexOf(scriptId);
|
||||||
if (index === -1) return current;
|
if (index === -1) return current;
|
||||||
const newTabs = current.filter((id) => id !== scriptId);
|
const newTabs = current.filter((id) => id !== scriptId);
|
||||||
|
console.log('[closeTab] 关闭后的标签栏:', newTabs);
|
||||||
if (selectedId === scriptId) {
|
if (selectedId === scriptId) {
|
||||||
const nextId = newTabs[index] ?? newTabs[index - 1] ?? null;
|
const nextId = newTabs[index] ?? newTabs[index - 1] ?? null;
|
||||||
|
console.log('[closeTab] 选中下一个:', nextId);
|
||||||
setSelectedId(nextId);
|
setSelectedId(nextId);
|
||||||
selectedIdRef.current = nextId;
|
selectedIdRef.current = nextId;
|
||||||
}
|
}
|
||||||
@@ -407,7 +464,7 @@ function AuthenticatedModelPlatformApp() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换标签
|
// 切换标签 - 直接复用缓存的会话,不重新打开
|
||||||
const switchTab = (scriptId: string) => {
|
const switchTab = (scriptId: string) => {
|
||||||
if (selectedIdRef.current !== scriptId) {
|
if (selectedIdRef.current !== scriptId) {
|
||||||
editorOpenRequestRef.current += 1;
|
editorOpenRequestRef.current += 1;
|
||||||
@@ -415,6 +472,15 @@ function AuthenticatedModelPlatformApp() {
|
|||||||
}
|
}
|
||||||
setSelectedId(scriptId);
|
setSelectedId(scriptId);
|
||||||
selectedIdRef.current = scriptId;
|
selectedIdRef.current = scriptId;
|
||||||
|
// 如果有缓存的会话,直接切换
|
||||||
|
const cached = sessionCacheRef.current.get(scriptId);
|
||||||
|
if (cached) {
|
||||||
|
// 更新最后激活时间
|
||||||
|
cached.lastActiveTime = Date.now();
|
||||||
|
setEditSession(cached.session);
|
||||||
|
editSessionRef.current = cached.session;
|
||||||
|
setEmbeddedJupyterUrl(cached.jupyterUrl);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开脚本编辑器
|
// 打开脚本编辑器
|
||||||
@@ -437,9 +503,28 @@ function AuthenticatedModelPlatformApp() {
|
|||||||
setEditBusy(true);
|
setEditBusy(true);
|
||||||
setEditorOpenError((current) =>
|
setEditorOpenError((current) =>
|
||||||
current?.scriptId === script.script_id ? null : current);
|
current?.scriptId === script.script_id ? null : current);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查是否已有缓存的编辑会话
|
||||||
|
const cached = sessionCacheRef.current.get(script.script_id);
|
||||||
|
if (cached) {
|
||||||
|
if (!requestIsCurrent()) return;
|
||||||
|
// 直接复用缓存的会话和 URL
|
||||||
|
setEditSession(cached.session);
|
||||||
|
editSessionRef.current = cached.session;
|
||||||
|
setEmbeddedJupyterUrl(cached.jupyterUrl);
|
||||||
|
if (showToast) {
|
||||||
|
setToast({
|
||||||
|
tone: "success",
|
||||||
|
message: `${script.script_name} 已打开,Jupyter 已嵌入当前工作区`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有缓存,需要获取新锁
|
||||||
let active = editSessionRef.current;
|
let active = editSessionRef.current;
|
||||||
let newlyAcquired = false;
|
let newlyAcquired = false;
|
||||||
try {
|
|
||||||
if (active && active.script_id !== script.script_id) {
|
if (active && active.script_id !== script.script_id) {
|
||||||
await api.releaseFileLock(active);
|
await api.releaseFileLock(active);
|
||||||
setEmbeddedJupyterUrl(null);
|
setEmbeddedJupyterUrl(null);
|
||||||
@@ -463,8 +548,10 @@ function AuthenticatedModelPlatformApp() {
|
|||||||
|
|
||||||
const ticket = await api.createJupyterAccessTicket(active);
|
const ticket = await api.createJupyterAccessTicket(active);
|
||||||
if (!requestIsCurrent()) {
|
if (!requestIsCurrent()) {
|
||||||
|
if (newlyAcquired) {
|
||||||
await api.releaseFileLock(active);
|
await api.releaseFileLock(active);
|
||||||
clearSessionIfActive(active);
|
clearSessionIfActive(active);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,6 +559,12 @@ function AuthenticatedModelPlatformApp() {
|
|||||||
setEditSession(readySession);
|
setEditSession(readySession);
|
||||||
editSessionRef.current = readySession;
|
editSessionRef.current = readySession;
|
||||||
setEmbeddedJupyterUrl(ticket.jupyter_url);
|
setEmbeddedJupyterUrl(ticket.jupyter_url);
|
||||||
|
// 缓存会话(多 iframe 方案:增加 lastActiveTime)
|
||||||
|
sessionCacheRef.current.set(script.script_id, {
|
||||||
|
session: readySession,
|
||||||
|
jupyterUrl: ticket.jupyter_url,
|
||||||
|
lastActiveTime: Date.now(),
|
||||||
|
});
|
||||||
if (showToast) {
|
if (showToast) {
|
||||||
setToast({
|
setToast({
|
||||||
tone: "success",
|
tone: "success",
|
||||||
@@ -479,15 +572,6 @@ function AuthenticatedModelPlatformApp() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (newlyAcquired && active) {
|
|
||||||
try {
|
|
||||||
await api.releaseFileLock(active);
|
|
||||||
} catch {
|
|
||||||
// The database lease is the final safety net
|
|
||||||
}
|
|
||||||
setEditSession(null);
|
|
||||||
editSessionRef.current = null;
|
|
||||||
}
|
|
||||||
setEmbeddedJupyterUrl(null);
|
setEmbeddedJupyterUrl(null);
|
||||||
if (requestIsCurrent()) {
|
if (requestIsCurrent()) {
|
||||||
const message = error instanceof Error ? error.message : "打开编辑器失败";
|
const message = error instanceof Error ? error.message : "打开编辑器失败";
|
||||||
@@ -514,6 +598,14 @@ function AuthenticatedModelPlatformApp() {
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 检查是否有缓存的会话,有则直接复用,不触发自动打开
|
||||||
|
const cached = sessionCacheRef.current.get(selected.script_id);
|
||||||
|
if (cached) {
|
||||||
|
setEditSession(cached.session);
|
||||||
|
editSessionRef.current = cached.session;
|
||||||
|
setEmbeddedJupyterUrl(cached.jupyterUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
void openScriptEditor(selected, false);
|
void openScriptEditor(selected, false);
|
||||||
}, [
|
}, [
|
||||||
activePage, editBusy, editSession?.script_id, editorOpenError?.scriptId,
|
activePage, editBusy, editSession?.script_id, editorOpenError?.scriptId,
|
||||||
@@ -538,6 +630,8 @@ function AuthenticatedModelPlatformApp() {
|
|||||||
setEmbeddedJupyterUrl(null);
|
setEmbeddedJupyterUrl(null);
|
||||||
setEditSession(null);
|
setEditSession(null);
|
||||||
editSessionRef.current = null;
|
editSessionRef.current = null;
|
||||||
|
// 从缓存中删除
|
||||||
|
if (scriptId) sessionCacheRef.current.delete(scriptId);
|
||||||
if (closeTabFlag && scriptId) {
|
if (closeTabFlag && scriptId) {
|
||||||
void closeTab(scriptId);
|
void closeTab(scriptId);
|
||||||
}
|
}
|
||||||
@@ -557,21 +651,6 @@ function AuthenticatedModelPlatformApp() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换文件时结束编辑
|
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@@ -682,7 +761,10 @@ function AuthenticatedModelPlatformApp() {
|
|||||||
lastCreated = await api.uploadScript(file, uploadParentPath);
|
lastCreated = await api.uploadScript(file, uploadParentPath);
|
||||||
}
|
}
|
||||||
await load(true);
|
await load(true);
|
||||||
if (lastCreated) selectScript(lastCreated.script_id);
|
if (lastCreated) {
|
||||||
|
openTab(lastCreated.script_id);
|
||||||
|
selectScript(lastCreated.script_id);
|
||||||
|
}
|
||||||
setToast({
|
setToast({
|
||||||
tone: "success",
|
tone: "success",
|
||||||
message: `${files.length} 个文件已上传到${uploadParentPath ? ` ${uploadParentPath}` : "当前目录"}`,
|
message: `${files.length} 个文件已上传到${uploadParentPath ? ` ${uploadParentPath}` : "当前目录"}`,
|
||||||
@@ -729,7 +811,18 @@ function AuthenticatedModelPlatformApp() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await api.deleteScript(script.script_id);
|
await api.deleteScript(script.script_id);
|
||||||
if (selectedIdRef.current === script.script_id) selectScript(null);
|
// 从标签栏删除该文件,并选中相邻的文件
|
||||||
|
setOpenTabIds((current) => {
|
||||||
|
const index = current.indexOf(script.script_id);
|
||||||
|
const newTabs = current.filter((id) => id !== script.script_id);
|
||||||
|
// 如果删除的是当前选中的文件,选中相邻的
|
||||||
|
if (selectedIdRef.current === script.script_id) {
|
||||||
|
const nextId = newTabs[index] ?? newTabs[index - 1] ?? null;
|
||||||
|
setSelectedId(nextId);
|
||||||
|
selectedIdRef.current = nextId;
|
||||||
|
}
|
||||||
|
return newTabs;
|
||||||
|
});
|
||||||
await load(true);
|
await load(true);
|
||||||
setToast({ tone: "success", message: `${script.script_name} 已删除` });
|
setToast({ tone: "success", message: `${script.script_name} 已删除` });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -842,10 +935,10 @@ function AuthenticatedModelPlatformApp() {
|
|||||||
<section className="editor-area">
|
<section className="editor-area">
|
||||||
{selected ? (
|
{selected ? (
|
||||||
<ScriptWorkspace
|
<ScriptWorkspace
|
||||||
key={selected.script_id}
|
|
||||||
script={selected}
|
script={selected}
|
||||||
editSession={editSession?.script_id === selected.script_id ? editSession : null}
|
sessionCache={sessionCacheRef.current}
|
||||||
jupyterUrl={editSession?.script_id === selected.script_id ? embeddedJupyterUrl : null}
|
editSession={editSession}
|
||||||
|
jupyterUrl={embeddedJupyterUrl}
|
||||||
editBusy={editBusy}
|
editBusy={editBusy}
|
||||||
openError={editorOpenError?.scriptId === selected.script_id ? editorOpenError.message : null}
|
openError={editorOpenError?.scriptId === selected.script_id ? editorOpenError.message : null}
|
||||||
latestVersion={latestVersion}
|
latestVersion={latestVersion}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
ScriptType,
|
ScriptType,
|
||||||
} from "../../services/api";
|
} from "../../services/api";
|
||||||
import { scriptIcon } from "./WorkspaceTree";
|
import { scriptIcon } from "./WorkspaceTree";
|
||||||
import type { MouseEvent as ReactMouseEvent } from "react";
|
import type { MouseEvent as ReactMouseEvent, RefObject } from "react";
|
||||||
import { useRef, useEffect } from "react";
|
import { useRef, useEffect } from "react";
|
||||||
|
|
||||||
type ToastState = {
|
type ToastState = {
|
||||||
@@ -14,8 +14,16 @@ type ToastState = {
|
|||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 缓存的会话类型(多 iframe 共存方案)
|
||||||
|
interface CachedSession {
|
||||||
|
session: ActiveEditSession;
|
||||||
|
jupyterUrl: string;
|
||||||
|
lastActiveTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
type ScriptWorkspaceProps = {
|
type ScriptWorkspaceProps = {
|
||||||
script: ScriptItem;
|
script: ScriptItem;
|
||||||
|
sessionCache: Map<string, CachedSession>;
|
||||||
editSession: ActiveEditSession | null;
|
editSession: ActiveEditSession | null;
|
||||||
jupyterUrl: string | null;
|
jupyterUrl: string | null;
|
||||||
editBusy: boolean;
|
editBusy: boolean;
|
||||||
@@ -91,6 +99,7 @@ function confineJupyterFrame(frame: HTMLIFrameElement): void {
|
|||||||
|
|
||||||
export function ScriptWorkspace({
|
export function ScriptWorkspace({
|
||||||
script,
|
script,
|
||||||
|
sessionCache,
|
||||||
editSession,
|
editSession,
|
||||||
jupyterUrl,
|
jupyterUrl,
|
||||||
editBusy,
|
editBusy,
|
||||||
@@ -108,7 +117,9 @@ export function ScriptWorkspace({
|
|||||||
}: ScriptWorkspaceProps) {
|
}: ScriptWorkspaceProps) {
|
||||||
const tabbarRef = useRef<HTMLDivElement | null>(null);
|
const tabbarRef = useRef<HTMLDivElement | null>(null);
|
||||||
const isNotebook = script.script_type === "notebook";
|
const isNotebook = script.script_type === "notebook";
|
||||||
const isEditing = editSession?.session_status === "active";
|
// 判断当前选中的文件是否正在编辑(需要同时检查 session_status 和 script_id)
|
||||||
|
const isEditing = editSession?.session_status === "active"
|
||||||
|
&& editSession?.script_id === script.script_id;
|
||||||
|
|
||||||
const scroll = (direction: "left" | "right") => {
|
const scroll = (direction: "left" | "right") => {
|
||||||
const tabbar = tabbarRef.current;
|
const tabbar = tabbarRef.current;
|
||||||
@@ -232,34 +243,55 @@ export function ScriptWorkspace({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`editor-canvas${isEditing && jupyterUrl ? " is-embedded" : ""}`}>
|
{/* 多 iframe 共存方案:为每个缓存的 session 渲染独立的 iframe */}
|
||||||
{isEditing && jupyterUrl ? (
|
<div className={`editor-canvas ${sessionCache.size > 0 ? 'is-embedded' : ''}`} style={sessionCache.size > 0 ? { position: 'relative', minHeight: '0', height: '100%' } : undefined}>
|
||||||
<section className="embedded-jupyter">
|
{/* 渲染所有缓存的 iframe */}
|
||||||
|
{Array.from(sessionCache.entries()).map(([scriptId, cached]) => {
|
||||||
|
// 当前选中的脚本就是激活的
|
||||||
|
const isActive = scriptId === script.script_id;
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
key={scriptId}
|
||||||
|
className="embedded-jupyter"
|
||||||
|
style={{
|
||||||
|
position: isActive ? 'relative' : 'absolute',
|
||||||
|
visibility: isActive ? 'visible' : 'hidden',
|
||||||
|
pointerEvents: isActive ? 'auto' : 'none',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="embedded-jupyter__status">
|
<div className="embedded-jupyter__status">
|
||||||
<span>
|
<span>
|
||||||
<i />
|
<i />
|
||||||
Workspace Jupyter Server
|
Workspace Jupyter Server
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{isNotebook ? "Notebook Session · Python 3 Kernel" : "Jupyter 文本编辑器"}
|
{cached.session.session_status === "active" ? "Notebook Session · Python 3 Kernel" : "Jupyter 文本编辑器"}
|
||||||
</span>
|
</span>
|
||||||
<code title={editSession.runtime_id}>
|
<code title={cached.session.runtime_id}>
|
||||||
Runtime {editSession.runtime_id.slice(-8)}
|
Runtime {cached.session.runtime_id.slice(-8)}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<iframe
|
<iframe
|
||||||
key={`${editSession.edit_session_id}:${jupyterUrl}`}
|
src={cached.jupyterUrl}
|
||||||
src={jupyterUrl}
|
title={`${scriptId} Jupyter 编辑器`}
|
||||||
title={`${script.script_name} Jupyter 编辑器`}
|
|
||||||
allow="clipboard-read; clipboard-write"
|
allow="clipboard-read; clipboard-write"
|
||||||
sandbox="allow-same-origin allow-scripts allow-forms allow-downloads allow-modals"
|
sandbox="allow-same-origin allow-scripts allow-forms allow-downloads allow-modals"
|
||||||
onLoad={(event) => confineJupyterFrame(event.currentTarget)}
|
onLoad={(event) => confineJupyterFrame(event.currentTarget)}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
) : isNotebook ? (
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* 当前脚本没有缓存时的状态显示 */}
|
||||||
|
{!sessionCache.has(script.script_id) ? (
|
||||||
|
isNotebook ? (
|
||||||
<section
|
<section
|
||||||
className={`editor-opening-state${openError ? " has-error" : ""}`}
|
className={`editor-opening-state${openError ? " has-error" : ""}`}
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
|
style={{ display: 'block' }}
|
||||||
>
|
>
|
||||||
<div className="editor-opening-state__icon">
|
<div className="editor-opening-state__icon">
|
||||||
{openError
|
{openError
|
||||||
@@ -365,7 +397,7 @@ export function ScriptWorkspace({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)) : null}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user