update: data source
This commit is contained in:
@@ -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 } })),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user