From 56699ccb1b13d88d66cc0832afa2404754e94055 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Fri, 30 Jul 2021 16:23:36 -0700 Subject: [PATCH] Fix new resource tree (#962) --- src/Common/ResourceTreeContainer.tsx | 6 +- src/Explorer/Explorer.tsx | 27 +++++--- .../Notebook/NotebookContentClient.ts | 31 +++++++-- src/Explorer/Notebook/useNotebook.ts | 30 ++++++--- .../CopyNotebookPane/CopyNotebookPane.tsx | 7 +- src/Explorer/Tree/ResourceTree.tsx | 67 ++++++++++++------- src/Platform/Hosted/extractFeatures.ts | 4 +- 7 files changed, 117 insertions(+), 55 deletions(-) diff --git a/src/Common/ResourceTreeContainer.tsx b/src/Common/ResourceTreeContainer.tsx index ca41610da..18a769b12 100644 --- a/src/Common/ResourceTreeContainer.tsx +++ b/src/Common/ResourceTreeContainer.tsx @@ -54,10 +54,10 @@ export const ResourceTreeContainer: FunctionComponent {userContext.authType === AuthType.ResourceToken ? ( - ) : userContext.features.enableReactResourceTree ? ( - - ) : ( + ) : userContext.features.enableKoResourceTree ? (
+ ) : ( + )}
{/* Collections Window - End */} diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index a4cd403f0..d854278c2 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -537,17 +537,22 @@ export default class Explorer { } } - public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise { + public uploadFile( + name: string, + content: string, + parent: NotebookContentItem, + isGithubTree?: boolean + ): Promise { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to upload notebook, but notebook is not enabled"; handleError(error, "Explorer/uploadFile"); throw new Error(error); } - const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent); + const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent, isGithubTree); promise .then(() => this.resourceTree.triggerRender()) - .catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", reason)); + .catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", getErrorMessage(reason))); return promise; } @@ -672,7 +677,7 @@ export default class Explorer { return true; } - public renameNotebook(notebookFile: NotebookContentItem): void { + public renameNotebook(notebookFile: NotebookContentItem, isGithubTree?: boolean): void { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to rename notebook, but notebook is not enabled"; handleError(error, "Explorer/renameNotebook"); @@ -705,7 +710,7 @@ export default class Explorer { paneTitle="Rename Notebook" defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")} onSubmit={(notebookFile: NotebookContentItem, input: string): Promise => - this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input) + this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input, isGithubTree) } notebookFile={notebookFile} /> @@ -713,7 +718,7 @@ export default class Explorer { } } - public onCreateDirectory(parent: NotebookContentItem): void { + public onCreateDirectory(parent: NotebookContentItem, isGithubTree?: boolean): void { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to create notebook directory, but notebook is not enabled"; handleError(error, "Explorer/onCreateDirectory"); @@ -735,7 +740,7 @@ export default class Explorer { submitButtonLabel="Create" defaultInput="" onSubmit={(notebookFile: NotebookContentItem, input: string): Promise => - this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input) + this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input, isGithubTree) } notebookFile={parent} /> @@ -804,7 +809,7 @@ export default class Explorer { } }; - public deleteNotebookFile(item: NotebookContentItem): Promise { + public deleteNotebookFile(item: NotebookContentItem, isGithubTree?: boolean): Promise { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to delete notebook file, but notebook is not enabled"; handleError(error, "Explorer/deleteNotebookFile"); @@ -837,7 +842,7 @@ export default class Explorer { return Promise.reject(); } - return this.notebookManager?.notebookContentClient.deleteContentItem(item).then( + return this.notebookManager?.notebookContentClient.deleteContentItem(item, isGithubTree).then( () => logConsoleInfo(`Successfully deleted: ${item.path}`), (reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`) ); @@ -846,7 +851,7 @@ export default class Explorer { /** * This creates a new notebook file, then opens the notebook */ - public onNewNotebookClicked(parent?: NotebookContentItem): void { + public onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): void { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to create new notebook, but notebook is not enabled"; handleError(error, "Explorer/onNewNotebookClicked"); @@ -861,7 +866,7 @@ export default class Explorer { }); this.notebookManager?.notebookContentClient - .createNewNotebookFile(parent) + .createNewNotebookFile(parent, isGithubTree) .then((newFile: NotebookContentItem) => { logConsoleInfo(`Successfully created: ${newFile.name}`); TelemetryProcessor.traceSuccess( diff --git a/src/Explorer/Notebook/NotebookContentClient.ts b/src/Explorer/Notebook/NotebookContentClient.ts index 3599c009c..5ca408c2a 100644 --- a/src/Explorer/Notebook/NotebookContentClient.ts +++ b/src/Explorer/Notebook/NotebookContentClient.ts @@ -36,7 +36,7 @@ export class NotebookContentClient { * * @param parent parent folder */ - public createNewNotebookFile(parent: NotebookContentItem): Promise { + public createNewNotebookFile(parent: NotebookContentItem, isGithubTree?: boolean): Promise { if (!parent || parent.type !== NotebookContentItemType.Directory) { throw new Error(`Parent must be a directory: ${parent}`); } @@ -57,6 +57,8 @@ export class NotebookContentClient { const notebookFile = xhr.response; const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type); + useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree); + // TODO: delete when ResourceTreeAdapter is removed if (parent.children) { item.parent = parent; parent.children.push(item); @@ -66,9 +68,9 @@ export class NotebookContentClient { }); } - public async deleteContentItem(item: NotebookContentItem): Promise { + public async deleteContentItem(item: NotebookContentItem, isGithubTree?: boolean): Promise { const path = await this.deleteNotebookFile(item.path); - useNotebook.getState().deleteNotebookItem(item); + useNotebook.getState().deleteNotebookItem(item, isGithubTree); // TODO: Delete once old resource tree is removed if (!path || path !== item.path) { @@ -91,7 +93,8 @@ export class NotebookContentClient { public async uploadFileAsync( name: string, content: string, - parent: NotebookContentItem + parent: NotebookContentItem, + isGithubTree?: boolean ): Promise { if (!parent || parent.type !== NotebookContentItemType.Directory) { throw new Error(`Parent must be a directory: ${parent}`); @@ -115,6 +118,8 @@ export class NotebookContentClient { .then((xhr: AjaxResponse) => { const notebookFile = xhr.response; const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type); + useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree); + // TODO: delete when ResourceTreeAdapter is removed if (parent.children) { item.parent = parent; parent.children.push(item); @@ -137,7 +142,11 @@ export class NotebookContentClient { * @param sourcePath * @param targetName is not prefixed with path */ - public renameNotebook(item: NotebookContentItem, targetName: string): Promise { + public renameNotebook( + item: NotebookContentItem, + targetName: string, + isGithubTree?: boolean + ): Promise { const sourcePath = item.path; // Match extension if (sourcePath.indexOf(".") !== -1) { @@ -163,6 +172,9 @@ export class NotebookContentClient { item.name = notebookFile.name; item.path = notebookFile.path; item.timestamp = NotebookUtil.getCurrentTimestamp(); + + useNotebook.getState().updateNotebookItem(item, isGithubTree); + return item; }); } @@ -172,7 +184,11 @@ export class NotebookContentClient { * @param parent * @param newDirectoryName basename of the new directory */ - public async createDirectory(parent: NotebookContentItem, newDirectoryName: string): Promise { + public async createDirectory( + parent: NotebookContentItem, + newDirectoryName: string, + isGithubTree?: boolean + ): Promise { if (parent.type !== NotebookContentItemType.Directory) { throw new Error(`Parent is not a directory: ${parent.path}`); } @@ -199,8 +215,11 @@ export class NotebookContentClient { const dir = xhr.response; const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type); + useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree); + // TODO: delete when ResourceTreeAdapter is removed item.parent = parent; parent.children?.push(item); + return item; }); } diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index af1b47477..5a2c97b3e 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -38,8 +38,9 @@ interface NotebookState { setNotebookBasePath: (notebookBasePath: string) => void; refreshNotebooksEnabledStateForAccount: () => Promise; findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem; - updateNotebookItem: (item: NotebookContentItem) => void; - deleteNotebookItem: (item: NotebookContentItem) => void; + insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void; + updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void; + deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void; initializeNotebooksTree: (notebookManager: NotebookManager) => Promise; initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void; } @@ -141,19 +142,30 @@ export const useNotebook: UseStore = create((set, get) => ({ return undefined; }, - updateNotebookItem: (item: NotebookContentItem): void => { - const root = cloneDeep(get().myNotebooksContentRoot); + insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean): void => { + const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); + const parentItem = get().findItem(root, parent); + item.parent = parentItem; + if (parentItem.children) { + parentItem.children.push(item); + } else { + parentItem.children = [item]; + } + isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); + }, + updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => { + const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); const parentItem = get().findItem(root, item.parent); parentItem.children = parentItem.children.filter((child) => child.path !== item.path); parentItem.children.push(item); item.parent = parentItem; - set({ myNotebooksContentRoot: root }); + isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); }, - deleteNotebookItem: (item: NotebookContentItem): void => { - const root = cloneDeep(get().myNotebooksContentRoot); + deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => { + const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); const parentItem = get().findItem(root, item.parent); parentItem.children = parentItem.children.filter((child) => child.path !== item.path); - set({ myNotebooksContentRoot: root }); + isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); }, initializeNotebooksTree: async (notebookManager: NotebookManager): Promise => { const myNotebooksContentRoot = { @@ -216,6 +228,7 @@ export const useNotebook: UseStore = create((set, get) => ({ path: "PsuedoDir", type: NotebookContentItemType.Directory, children: [], + parent: gitHubNotebooksContentRoot, }; pinnedRepo.branches.forEach((branch) => { @@ -223,6 +236,7 @@ export const useNotebook: UseStore = create((set, get) => ({ name: branch.name, path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""), type: NotebookContentItemType.Directory, + parent: repoTreeItem, }); }); diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx index 60cb731b8..3c63c00ae 100644 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx +++ b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx @@ -98,6 +98,7 @@ export const CopyNotebookPane: FunctionComponent = ({ const copyNotebook = async (location: Location): Promise => { let parent: NotebookContentItem; + let isGithubTree: boolean; switch (location.type) { case "MyNotebooks": parent = { @@ -105,21 +106,23 @@ export const CopyNotebookPane: FunctionComponent = ({ path: useNotebook.getState().notebookBasePath, type: NotebookContentItemType.Directory, }; + isGithubTree = false; break; case "GitHub": parent = { - name: ResourceTreeAdapter.GitHubReposTitle, + name: selectedLocation.branch, path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""), type: NotebookContentItemType.Directory, }; + isGithubTree = true; break; default: throw new Error(`Unsupported location type ${location.type}`); } - return container.uploadFile(name, content, parent); + return container.uploadFile(name, content, parent, isGithubTree); }; const onDropDownChange = (_: FormEvent, option?: IDropdownOption): void => { diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index 2130c01bb..b8dae8259 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -1,5 +1,6 @@ import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import * as React from "react"; +import shallow from "zustand/shallow"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import DeleteIcon from "../../../images/delete.svg"; import GalleryIcon from "../../../images/GalleryIcon.svg"; @@ -55,7 +56,16 @@ export const ResourceTree: React.FC = ({ container }: Resourc galleryContentRoot, gitHubNotebooksContentRoot, updateNotebookItem, - } = useNotebook(); + } = useNotebook( + (state) => ({ + isNotebookEnabled: state.isNotebookEnabled, + myNotebooksContentRoot: state.myNotebooksContentRoot, + galleryContentRoot: state.galleryContentRoot, + gitHubNotebooksContentRoot: state.gitHubNotebooksContentRoot, + updateNotebookItem: state.updateNotebookItem, + }), + shallow + ); const { activeTab, refreshActiveTab } = useTabs(); const showScriptNodes = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; const pseudoDirPath = "PsuedoDir"; @@ -166,7 +176,8 @@ export const ResourceTree: React.FC = ({ container }: Resourc mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); } }); - } + }, + true ); gitHubNotebooksTree.contextMenu = [ @@ -202,9 +213,9 @@ export const ResourceTree: React.FC = ({ container }: Resourc }; const buildChildNodes = ( - container: Explorer, item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void + onFileClick: (item: NotebookContentItem) => void, + isGithubTree?: boolean ): TreeNode[] => { if (!item || !item.children) { return []; @@ -212,8 +223,8 @@ export const ResourceTree: React.FC = ({ container }: Resourc return item.children.map((item) => { const result = item.type === NotebookContentItemType.Directory - ? buildNotebookDirectoryNode(item, onFileClick) - : buildNotebookFileNode(item, onFileClick); + ? buildNotebookDirectoryNode(item, onFileClick, isGithubTree) + : buildNotebookFileNode(item, onFileClick, isGithubTree); result.timestamp = item.timestamp; return result; }); @@ -222,7 +233,8 @@ export const ResourceTree: React.FC = ({ container }: Resourc const buildNotebookFileNode = ( item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void + onFileClick: (item: NotebookContentItem) => void, + isGithubTree?: boolean ): TreeNode => { return { label: item.name, @@ -239,17 +251,21 @@ export const ResourceTree: React.FC = ({ container }: Resourc (activeTab as any).notebookPath() === item.path ); }, - contextMenu: createFileContextMenu(container, item), + contextMenu: createFileContextMenu(container, item, isGithubTree), data: item, }; }; - const createFileContextMenu = (container: Explorer, item: NotebookContentItem): TreeNodeMenuItem[] => { + const createFileContextMenu = ( + container: Explorer, + item: NotebookContentItem, + isGithubTree?: boolean + ): TreeNodeMenuItem[] => { let items: TreeNodeMenuItem[] = [ { label: "Rename", iconSrc: NotebookIcon, - onClick: () => container.renameNotebook(item), + onClick: () => container.renameNotebook(item, isGithubTree), }, { label: "Delete", @@ -261,7 +277,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc "Confirm delete", `Are you sure you want to delete "${item.name}"`, "Delete", - () => container.deleteNotebookFile(item), + () => container.deleteNotebookFile(item, isGithubTree), "Cancel", undefined ); @@ -311,12 +327,16 @@ export const ResourceTree: React.FC = ({ container }: Resourc } }; - const createDirectoryContextMenu = (container: Explorer, item: NotebookContentItem): TreeNodeMenuItem[] => { + const createDirectoryContextMenu = ( + container: Explorer, + item: NotebookContentItem, + isGithubTree?: boolean + ): TreeNodeMenuItem[] => { let items: TreeNodeMenuItem[] = [ { label: "Refresh", iconSrc: RefreshIcon, - onClick: () => loadSubitems(item), + onClick: () => loadSubitems(item, isGithubTree), }, { label: "Delete", @@ -328,7 +348,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc "Confirm delete", `Are you sure you want to delete "${item.name}?"`, "Delete", - () => container.deleteNotebookFile(item), + () => container.deleteNotebookFile(item, isGithubTree), "Cancel", undefined ); @@ -337,17 +357,17 @@ export const ResourceTree: React.FC = ({ container }: Resourc { label: "Rename", iconSrc: NotebookIcon, - onClick: () => container.renameNotebook(item), + onClick: () => container.renameNotebook(item, isGithubTree), }, { label: "New Directory", iconSrc: NewNotebookIcon, - onClick: () => container.onCreateDirectory(item), + onClick: () => container.onCreateDirectory(item, isGithubTree), }, { label: "New Notebook", iconSrc: NewNotebookIcon, - onClick: () => container.onNewNotebookClicked(item), + onClick: () => container.onNewNotebookClicked(item, isGithubTree), }, { label: "Upload File", @@ -372,7 +392,8 @@ export const ResourceTree: React.FC = ({ container }: Resourc const buildNotebookDirectoryNode = ( item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void + onFileClick: (item: NotebookContentItem) => void, + isGithubTree?: boolean ): TreeNode => { return { label: item.name, @@ -382,7 +403,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc isLeavesParentsSeparate: true, onClick: () => { if (!item.children) { - loadSubitems(item); + loadSubitems(item, isGithubTree); } }, isSelected: () => { @@ -395,9 +416,9 @@ export const ResourceTree: React.FC = ({ container }: Resourc (activeTab as any).notebookPath() === item.path ); }, - contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item) : undefined, + contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item, isGithubTree) : undefined, data: item, - children: buildChildNodes(container, item, onFileClick), + children: buildChildNodes(item, onFileClick, isGithubTree), }; }; @@ -699,9 +720,9 @@ export const ResourceTree: React.FC = ({ container }: Resourc return traverse(schema); }; - const loadSubitems = async (item: NotebookContentItem): Promise => { + const loadSubitems = async (item: NotebookContentItem, isGithubTree?: boolean): Promise => { const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item); - updateNotebookItem(updatedItem); + updateNotebookItem(updatedItem, isGithubTree); }; const dataRootNode = buildDataTree(); diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 4a85dca8d..c7ea359ab 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -16,7 +16,7 @@ export type Features = { readonly enableTtl: boolean; readonly executeSproc: boolean; readonly enableAadDataPlane: boolean; - readonly enableReactResourceTree: boolean; + readonly enableKoResourceTree: boolean; readonly hostedDataExplorer: boolean; readonly junoEndpoint?: string; readonly livyEndpoint?: string; @@ -58,7 +58,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear enableSDKoperations: "true" === get("enablesdkoperations"), enableSpark: "true" === get("enablespark"), enableTtl: "true" === get("enablettl"), - enableReactResourceTree: "true" === get("enablereactresourcetree"), + enableKoResourceTree: "true" === get("enablekoresourcetree"), executeSproc: "true" === get("dataexplorerexecutesproc"), hostedDataExplorer: "true" === get("hosteddataexplorerenabled"), junoEndpoint: get("junoendpoint"),