refactor: uiStore.ts
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
import { type FormEvent, useEffect, useMemo, useRef } from "react";
|
||||
|
||||
import { useAuth } from "../../context/AuthContext";
|
||||
import { CreateFolderModal } from "../../components/platform/CreateFolderModal";
|
||||
import { CreateScriptModal } from "../../components/platform/CreateScriptModal";
|
||||
import Icon from "../../components/common/Icon";
|
||||
import { PublishModal } from "../../components/platform/PublishModal";
|
||||
import { ScriptExplorer } from "../../components/platform/ScriptExplorer";
|
||||
import { TreeContextMenu } from "../../components/platform/TreeContextMenu";
|
||||
import { VersionReceiptModal } from "../../components/platform/VersionReceiptModal";
|
||||
|
||||
import { getSessionCache, useScriptWorkspaceStore } from "./state/scriptWorkspaceStore";
|
||||
import { useUiStore } from "./state/uiStore";
|
||||
import { ScriptWorkspace } from "./ScriptWorkspace";
|
||||
|
||||
export default function ScriptsPage() {
|
||||
const { currentWorkspace, user } = useAuth();
|
||||
const uploadInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
// store state
|
||||
const scripts = useScriptWorkspaceStore((s) => s.scripts);
|
||||
const directories = useScriptWorkspaceStore((s) => s.directories);
|
||||
const selectedId = useScriptWorkspaceStore((s) => s.selectedId);
|
||||
const openTabIds = useScriptWorkspaceStore((s) => s.openTabIds);
|
||||
const keyword = useScriptWorkspaceStore((s) => s.keyword);
|
||||
const loading = useScriptWorkspaceStore((s) => s.loading);
|
||||
const refreshing = useScriptWorkspaceStore((s) => s.refreshing);
|
||||
const editSession = useScriptWorkspaceStore((s) => s.editSession);
|
||||
const embeddedJupyterUrl = useScriptWorkspaceStore((s) => s.embeddedJupyterUrl);
|
||||
const editBusy = useScriptWorkspaceStore((s) => s.editBusy);
|
||||
const editorOpenError = useScriptWorkspaceStore((s) => s.editorOpenError);
|
||||
const latestVersion = useScriptWorkspaceStore((s) => s.latestVersion);
|
||||
const latestVersionLoading = useScriptWorkspaceStore((s) => s.latestVersionLoading);
|
||||
|
||||
// store actions
|
||||
const setKeyword = useScriptWorkspaceStore((s) => s.setKeyword);
|
||||
const load = useScriptWorkspaceStore((s) => s.load);
|
||||
const reset = useScriptWorkspaceStore((s) => s.reset);
|
||||
const selectScript = useScriptWorkspaceStore((s) => s.selectScript);
|
||||
const openTab = useScriptWorkspaceStore((s) => s.openTab);
|
||||
const closeTab = useScriptWorkspaceStore((s) => s.closeTab);
|
||||
const switchTab = useScriptWorkspaceStore((s) => s.switchTab);
|
||||
const openScriptEditor = useScriptWorkspaceStore((s) => s.openScriptEditor);
|
||||
const endEditing = useScriptWorkspaceStore((s) => s.endEditing);
|
||||
const loadLatestVersion = useScriptWorkspaceStore((s) => s.loadLatestVersion);
|
||||
const createScript = useScriptWorkspaceStore((s) => s.createScript);
|
||||
const uploadScripts = useScriptWorkspaceStore((s) => s.uploadScripts);
|
||||
const createFolder = useScriptWorkspaceStore((s) => s.createFolder);
|
||||
const deleteScript = useScriptWorkspaceStore((s) => s.deleteScript);
|
||||
const deleteDirectory = useScriptWorkspaceStore((s) => s.deleteDirectory);
|
||||
const openPublishDialog = useScriptWorkspaceStore((s) => s.openPublishDialog);
|
||||
const submitPublish = useScriptWorkspaceStore((s) => s.submitPublish);
|
||||
|
||||
// ui store
|
||||
const pushToast = useUiStore((s) => s.pushToast);
|
||||
const createDialog = useUiStore((s) => s.createDialog);
|
||||
const setCreateForm = useUiStore((s) => s.setCreateForm);
|
||||
const closeCreateDialog = useUiStore((s) => s.closeCreateDialog);
|
||||
const folderDialog = useUiStore((s) => s.folderDialog);
|
||||
const setFolderName = useUiStore((s) => s.setFolderName);
|
||||
const closeFolderDialog = useUiStore((s) => s.closeFolderDialog);
|
||||
const contextMenu = useUiStore((s) => s.contextMenu);
|
||||
const closeContextMenu = useUiStore((s) => s.closeContextMenu);
|
||||
const showContextMenu = useUiStore((s) => s.showContextMenu);
|
||||
const upload = useUiStore((s) => s.upload);
|
||||
const openCreateDialog = useUiStore((s) => s.openCreateDialog);
|
||||
const openFolderDialog = useUiStore((s) => s.openFolderDialog);
|
||||
const chooseUpload = useUiStore((s) => s.chooseUpload);
|
||||
const publish = useUiStore((s) => s.publish);
|
||||
const setReleaseNote = useUiStore((s) => s.setReleaseNote);
|
||||
const setPublishVisibility = useUiStore((s) => s.setPublishVisibility);
|
||||
const closePublishDialog = useUiStore((s) => s.closePublishDialog);
|
||||
const clearPublishedVersion = useUiStore((s) => s.clearPublishedVersion);
|
||||
|
||||
const workspaceId = currentWorkspace?.workspace_id;
|
||||
|
||||
// 1) 初始加载 + workspace 切换时重置
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
useEffect(() => {
|
||||
reset();
|
||||
void load();
|
||||
}, [reset, load, workspaceId]);
|
||||
|
||||
// 2) 选中文件变更时加载最新版本
|
||||
useEffect(() => {
|
||||
if (!selectedId) {
|
||||
useScriptWorkspaceStore.setState({ latestVersion: null });
|
||||
return;
|
||||
}
|
||||
let ignore = false;
|
||||
useScriptWorkspaceStore.setState({ latestVersionLoading: true });
|
||||
void loadLatestVersion(selectedId).then(() => {
|
||||
if (!ignore) {
|
||||
useScriptWorkspaceStore.setState({ latestVersionLoading: false });
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
}, [selectedId, loadLatestVersion]);
|
||||
|
||||
const selected = useMemo(
|
||||
() => scripts.find((item) => item.script_id === selectedId) ?? null,
|
||||
[scripts, selectedId],
|
||||
);
|
||||
|
||||
const filteredScripts = useMemo(() => {
|
||||
const normalized = keyword.trim().toLocaleLowerCase();
|
||||
if (!normalized) return scripts;
|
||||
return scripts.filter((item) =>
|
||||
item.script_name.toLocaleLowerCase().includes(normalized),
|
||||
);
|
||||
}, [keyword, scripts]);
|
||||
|
||||
// 3) 自动打开 notebook 编辑器
|
||||
useEffect(() => {
|
||||
if (
|
||||
!selected
|
||||
|| selected.script_type !== "notebook"
|
||||
|| editBusy
|
||||
|| (editSession?.script_id === selected.script_id && embeddedJupyterUrl)
|
||||
|| editorOpenError?.scriptId === selected.script_id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const cached = getSessionCache().get(selected.script_id);
|
||||
if (cached) {
|
||||
useScriptWorkspaceStore.setState({
|
||||
editSession: cached.session,
|
||||
embeddedJupyterUrl: cached.jupyterUrl,
|
||||
});
|
||||
return;
|
||||
}
|
||||
void openScriptEditor(selected, false);
|
||||
}, [
|
||||
selected,
|
||||
editBusy,
|
||||
editSession?.script_id,
|
||||
embeddedJupyterUrl,
|
||||
editorOpenError?.scriptId,
|
||||
openScriptEditor,
|
||||
]);
|
||||
|
||||
// 4) context menu 外部点击关闭
|
||||
useEffect(() => {
|
||||
if (!contextMenu) return;
|
||||
const close = () => closeContextMenu();
|
||||
const closeOnEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") close();
|
||||
};
|
||||
window.addEventListener("pointerdown", close);
|
||||
window.addEventListener("blur", close);
|
||||
window.addEventListener("resize", close);
|
||||
window.addEventListener("scroll", close, true);
|
||||
window.addEventListener("keydown", closeOnEscape);
|
||||
return () => {
|
||||
window.removeEventListener("pointerdown", close);
|
||||
window.removeEventListener("blur", close);
|
||||
window.removeEventListener("resize", close);
|
||||
window.removeEventListener("scroll", close, true);
|
||||
window.removeEventListener("keydown", closeOnEscape);
|
||||
};
|
||||
}, [contextMenu, closeContextMenu]);
|
||||
|
||||
// 5) handlers
|
||||
const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files ?? []);
|
||||
event.target.value = "";
|
||||
if (files.length === 0) return;
|
||||
void uploadScripts(files, upload.parentPath);
|
||||
};
|
||||
|
||||
const handleCreateSubmit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
void createScript(createDialog.form);
|
||||
};
|
||||
|
||||
const handleFolderSubmit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
void createFolder(folderDialog.name, folderDialog.parentPath);
|
||||
};
|
||||
|
||||
const handlePublishSubmit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
void submitPublish(publish.releaseNote, publish.visibility);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="workspace-layout">
|
||||
<ScriptExplorer
|
||||
scripts={scripts}
|
||||
filteredScripts={filteredScripts}
|
||||
directories={directories}
|
||||
user={user}
|
||||
selectedId={selectedId}
|
||||
loading={loading}
|
||||
refreshing={refreshing}
|
||||
uploading={upload.uploading}
|
||||
keyword={keyword}
|
||||
onKeywordChange={setKeyword}
|
||||
onRefresh={() => void load(true)}
|
||||
onUpload={() => chooseUpload("")}
|
||||
onOpenCreateDialog={(parentPath, scriptType) =>
|
||||
openCreateDialog(parentPath, scriptType)}
|
||||
onOpenFolderDialog={(parentPath) => openFolderDialog(parentPath)}
|
||||
onChooseUpload={(parentPath) => chooseUpload(parentPath)}
|
||||
onContextMenu={showContextMenu}
|
||||
onSelect={openTab}
|
||||
uploadInputRef={uploadInputRef}
|
||||
onHandleUpload={handleUpload}
|
||||
/>
|
||||
|
||||
<section className="editor-area">
|
||||
{selected ? (
|
||||
<ScriptWorkspace
|
||||
script={selected}
|
||||
sessionCache={getSessionCache()}
|
||||
editSession={editSession}
|
||||
jupyterUrl={embeddedJupyterUrl}
|
||||
editBusy={editBusy}
|
||||
openError={
|
||||
editorOpenError?.scriptId === selected.script_id
|
||||
? editorOpenError.message
|
||||
: null
|
||||
}
|
||||
latestVersion={latestVersion}
|
||||
versionsLoading={latestVersionLoading}
|
||||
openTabs={openTabIds.map((id) => {
|
||||
const s = scripts.find((item) => item.script_id === id);
|
||||
return {
|
||||
scriptId: id,
|
||||
scriptName: s?.script_name ?? "未知",
|
||||
scriptType: s?.script_type ?? "notebook",
|
||||
};
|
||||
})}
|
||||
onOpenEditor={() => void openScriptEditor(selected)}
|
||||
onEndEditing={() => void endEditing()}
|
||||
onClose={(scriptId, event) => void closeTab(scriptId, event)}
|
||||
onSwitchTab={switchTab}
|
||||
onNewTab={() => openCreateDialog("")}
|
||||
onPublish={() => openPublishDialog(selected)}
|
||||
onInfo={(t) => pushToast(t)}
|
||||
/>
|
||||
) : (
|
||||
<div className="welcome-panel">
|
||||
<div className="welcome-panel__visual">
|
||||
<Icon name="script" size={42} />
|
||||
</div>
|
||||
<span className="welcome-panel__label">构建脚本工作台</span>
|
||||
<h2>创建你的第一个模型脚本</h2>
|
||||
<p>
|
||||
通过 Notebook 完成数据探索,或使用 Python 脚本构建可调度的处理任务。
|
||||
</p>
|
||||
<button className="primary-button" onClick={() => openCreateDialog("")}>
|
||||
<Icon name="plus" size={17} />
|
||||
新建构建脚本
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<CreateScriptModal
|
||||
open={createDialog.open}
|
||||
creating={createDialog.creating}
|
||||
form={createDialog.form}
|
||||
scripts={scripts.map((s) => ({
|
||||
script_type: s.script_type,
|
||||
script_name: s.script_name,
|
||||
}))}
|
||||
onFormChange={setCreateForm}
|
||||
onSubmit={handleCreateSubmit}
|
||||
onClose={closeCreateDialog}
|
||||
/>
|
||||
|
||||
<CreateFolderModal
|
||||
open={folderDialog.open}
|
||||
parentPath={folderDialog.parentPath}
|
||||
name={folderDialog.name}
|
||||
busy={folderDialog.busy}
|
||||
onNameChange={setFolderName}
|
||||
onSubmit={handleFolderSubmit}
|
||||
onClose={closeFolderDialog}
|
||||
/>
|
||||
|
||||
<TreeContextMenu
|
||||
contextMenu={contextMenu}
|
||||
onOpenScript={(scriptId) => {
|
||||
selectScript(scriptId);
|
||||
closeContextMenu();
|
||||
}}
|
||||
onRemoveScript={(s) => void deleteScript(s)}
|
||||
onOpenCreateDialog={(parentPath, scriptType) =>
|
||||
openCreateDialog(parentPath, scriptType)}
|
||||
onOpenFolderDialog={(parentPath) => openFolderDialog(parentPath)}
|
||||
onChooseUpload={(parentPath) => chooseUpload(parentPath)}
|
||||
onRemoveDirectory={(p) => void deleteDirectory(p)}
|
||||
onClose={closeContextMenu}
|
||||
/>
|
||||
|
||||
<PublishModal
|
||||
publishTarget={publish.target}
|
||||
releaseNote={publish.releaseNote}
|
||||
publishVisibility={publish.visibility}
|
||||
publishing={publish.publishing}
|
||||
onReleaseNoteChange={setReleaseNote}
|
||||
onPublishVisibilityChange={setPublishVisibility}
|
||||
onSubmit={handlePublishSubmit}
|
||||
onClose={closePublishDialog}
|
||||
/>
|
||||
|
||||
<VersionReceiptModal
|
||||
publishedVersion={publish.publishedVersion}
|
||||
onClose={clearPublishedVersion}
|
||||
onCopy={(message) => pushToast({ tone: "success", message })}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user