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