update: data source

This commit is contained in:
tao.chen
2026-08-14 11:59:06 +08:00
parent 225a585499
commit 480e1cc638
15 changed files with 432 additions and 559 deletions
@@ -4,6 +4,7 @@ import type {
ActiveEditSession,
LatestVersion,
ScriptItem,
ResourceItem,
StableVersion,
Visibility,
WorkspaceBoundApi,
@@ -55,9 +56,11 @@ export const editSessionHandle: { current: ActiveEditSession | null } = {
};
type State = {
scripts: ScriptItem[];
directories: WorkspaceDirectory[];
selectedId: string | null;
scripts: ScriptItem[];
directories: WorkspaceDirectory[];
dataResources: ResourceItem[];
dataResourcesLoading: boolean;
selectedId: string | null;
openTabIds: string[];
keyword: string;
loading: boolean;
@@ -87,11 +90,12 @@ type State = {
readOnlyRefreshVersion: number;
// actions
setApiOnline: (online: boolean) => void;
setKeyword: (keyword: string) => void;
reset: () => void;
load: (silent?: boolean) => Promise<void>;
selectScript: (id: string | null) => void;
setApiOnline: (online: boolean) => void;
setKeyword: (keyword: string) => void;
reset: () => void;
load: (silent?: boolean) => Promise<void>;
loadDataResources: () => Promise<void>;
selectScript: (id: string | null) => void;
openTab: (id: string) => void;
closeTab: (id: string, event?: { stopPropagation: () => void }) => Promise<void>;
switchTab: (id: string) => void;
@@ -106,6 +110,12 @@ type State = {
loadPreview: (workspaceId: string, filePath: string) => Promise<void>;
createScript: (form: NewScriptForm) => Promise<ScriptItem | null>;
uploadScripts: (files: File[], parentPath: string) => Promise<void>;
uploadDataResource: (file: File, meta: {
resourceName: string;
visibility: Visibility;
description: string;
targetPath: string;
}) => Promise<ResourceItem | null>;
createFolder: (name: string, parentPath: string) => Promise<void>;
deleteScript: (script: ScriptItem) => Promise<void>;
deleteDirectory: (path: string) => Promise<void>;
@@ -136,6 +146,12 @@ function ownedScriptPath(item: ScriptItem) {
function pushToast(tone: "success" | "error" | "info", message: string) {
useUiStore.getState().pushToast({ tone, message });
}
async function sha256Hex(buffer: ArrayBuffer): Promise<string> {
const digest = await crypto.subtle.digest("SHA-256", buffer);
return Array.from(new Uint8Array(digest))
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
}
export const useScriptWorkspaceStore = create<State>((set, get) => {
const setEditSessionState = (
@@ -151,9 +167,11 @@ export const useScriptWorkspaceStore = create<State>((set, get) => {
};
return {
scripts: [],
directories: [],
selectedId: null,
scripts: [],
directories: [],
dataResources: [],
dataResourcesLoading: false,
selectedId: null,
openTabIds: [],
keyword: "",
loading: true,
@@ -196,10 +214,12 @@ export const useScriptWorkspaceStore = create<State>((set, get) => {
editSessionHandle.current = null;
sessionCache.clear();
_pythonEditorOpeningIds.clear();
set({
scripts: [],
directories: [],
selectedId: null,
set({
scripts: [],
directories: [],
dataResources: [],
dataResourcesLoading: false,
selectedId: null,
openTabIds: [],
editSession: null,
embeddedJupyterUrl: null,
@@ -257,6 +277,19 @@ export const useScriptWorkspaceStore = create<State>((set, get) => {
}
},
loadDataResources: async () => {
const api = requireApi();
set({ dataResourcesLoading: true });
try {
const list = await api.listResources();
set({ dataResources: Array.isArray(list) ? list : [] });
} catch {
set({ dataResources: [] });
} finally {
set({ dataResourcesLoading: false });
}
},
loadChildren: async (parentPath) => {
const api = requireApi();
if (get().loadedChildPaths.has(parentPath)) return;
@@ -777,6 +810,47 @@ export const useScriptWorkspaceStore = create<State>((set, get) => {
}
},
uploadDataResource: async (file, meta) => {
const api = requireApi();
const ui = useUiStore.getState();
ui.setDataResourceUploading(true);
try {
const buffer = await file.arrayBuffer();
const hash = await sha256Hex(buffer);
const { upload_id: uploadId } = await api.createResourceUpload({
file_name: file.name,
content_type: file.type || "application/octet-stream",
expected_size_bytes: file.size,
expected_hash: hash,
target_path: meta.targetPath,
});
await api.uploadResourceBytes(
uploadId,
buffer,
file.type || "application/octet-stream",
);
const resource = await api.bindResourceUpload(uploadId, {
resource_name: meta.resourceName,
description: meta.description,
visibility: meta.visibility,
});
ui.closeDataResourceDialog();
pushToast(
"success",
`数据资源 ${resource.resource_name} 上传成功,路径:${resource.jupyter_accessible_path}`,
);
return resource;
} catch (error) {
pushToast(
"error",
error instanceof Error ? error.message : "数据资源上传失败",
);
return null;
} finally {
ui.setDataResourceUploading(false);
}
},
createFolder: async (name, parentPath) => {
const api = requireApi();
const trimmed = name.trim();
@@ -34,6 +34,16 @@ export type FolderDialogState = {
name: string;
busy: boolean;
};
export type DataResourceDialogState = {
open: boolean;
file: File | null;
parentPath: string;
resourceName: string;
visibility: Visibility;
description: string;
uploading: boolean;
targetPath: string;
};
export type PublishDialogState = {
target: ScriptItem | null;
@@ -67,6 +77,7 @@ type UiState = {
workspaceMenuOpen: boolean;
upload: UploadState;
publish: PublishDialogState;
dataResourceDialog: DataResourceDialogState;
// toast
pushToast: (toast: ToastState) => void;
@@ -107,6 +118,14 @@ type UiState = {
setPublishing: (publishing: boolean) => void;
setPublishedVersion: (version: StableVersion) => void;
clearPublishedVersion: () => void;
// data resource dialog
openDataResourceDialog: (file: File, parentPath?: string) => void;
closeDataResourceDialog: () => void;
setDataResourceName: (name: string) => void;
setDataResourceVisibility: (visibility: Visibility) => void;
setDataResourceDescription: (description: string) => void;
setDataResourceTargetPath: (path: string) => void;
setDataResourceUploading: (uploading: boolean) => void;
};
export const useUiStore = create<UiState>((set) => ({
@@ -135,6 +154,16 @@ export const useUiStore = create<UiState>((set) => ({
publishing: false,
publishedVersion: null,
},
dataResourceDialog: {
open: false,
file: null,
parentPath: "",
resourceName: "",
visibility: "workspace",
description: "",
uploading: false,
targetPath: "",
},
pushToast: (toast) => set({ toast }),
dismissToast: () => set({ toast: null }),
@@ -230,4 +259,33 @@ export const useUiStore = create<UiState>((set) => ({
})),
clearPublishedVersion: () =>
set((state) => ({ publish: { ...state.publish, publishedVersion: null } })),
}));
openDataResourceDialog: (file, parentPath = "") =>
set((state) => ({
contextMenu: null,
dataResourceDialog: {
open: true,
file,
parentPath,
resourceName: file.name.replace(/\.[^/.]+$/, ""),
visibility: "workspace",
description: "",
uploading: false,
// 右键"在此处上传"时,把父目录预填进子目录输入框
targetPath: parentPath,
},
})),
closeDataResourceDialog: () =>
set((state) => ({
dataResourceDialog: { ...state.dataResourceDialog, open: false, uploading: false },
})),
setDataResourceName: (name) =>
set((state) => ({ dataResourceDialog: { ...state.dataResourceDialog, resourceName: name } })),
setDataResourceVisibility: (visibility) =>
set((state) => ({ dataResourceDialog: { ...state.dataResourceDialog, visibility } })),
setDataResourceDescription: (description) =>
set((state) => ({ dataResourceDialog: { ...state.dataResourceDialog, description } })),
setDataResourceTargetPath: (path) =>
set((state) => ({ dataResourceDialog: { ...state.dataResourceDialog, targetPath: path } })),
setDataResourceUploading: (uploading) =>
set((state) => ({ dataResourceDialog: { ...state.dataResourceDialog, uploading } })),
}));