diff --git a/src/Contracts/ActionContracts.ts b/src/Contracts/ActionContracts.ts index f8fc956e6..cf4b66ed6 100644 --- a/src/Contracts/ActionContracts.ts +++ b/src/Contracts/ActionContracts.ts @@ -68,10 +68,6 @@ export interface OpenPane extends DataExplorerAction { paneKind: PaneKind | string; } -export interface OpenSampleNotebook extends DataExplorerAction { - path: string; -} - /** * The types of actions that the DataExplorer supports performing upon opening. */ @@ -80,5 +76,4 @@ export enum ActionType { OpenCollectionTab, OpenPane, TransmitCachedData, - OpenSampleNotebook, } diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index ab7abac11..b08834dd6 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -29,8 +29,6 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -47,10 +45,8 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, }, @@ -107,8 +103,6 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -125,10 +119,8 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, }, @@ -224,8 +216,6 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -242,10 +232,8 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, }, @@ -271,8 +259,6 @@ exports[`SettingsComponent renders 1`] = ` } explorer={ Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -289,10 +275,8 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, } diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 4af478475..368168a4a 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -1,4 +1,3 @@ -import { Link } from "@fluentui/react/lib/Link"; import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { sendMessage } from "Common/MessageHandler"; import { Platform, configContext } from "ConfigContext"; @@ -16,7 +15,7 @@ import shallow from "zustand/shallow"; import { AuthType } from "../AuthType"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import * as Constants from "../Common/Constants"; -import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook, PoolIdType } from "../Common/Constants"; +import { Areas, ConnectionStatusType, HttpStatusCodes, PoolIdType } from "../Common/Constants"; import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils"; import * as Logger from "../Common/Logger"; import { QueriesClient } from "../Common/QueriesClient"; @@ -32,34 +31,23 @@ import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants" import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import { isAccountNewerThanThresholdInMs, userContext } from "../UserContext"; import { getCollectionName, getUploadName } from "../Utils/APITypeUtils"; -import { stringToBlob } from "../Utils/BlobUtils"; import { isCapabilityEnabled } from "../Utils/CapabilityUtils"; -import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; -import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; -import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces"; import { useSidePanel } from "../hooks/useSidePanel"; import { useTabs } from "../hooks/useTabs"; import "./ComponentRegisterer"; import { DialogProps, useDialog } from "./Controls/Dialog"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter"; -import * as FileSystemUtil from "./Notebook/FileSystemUtil"; -import { SnapshotRequest } from "./Notebook/NotebookComponent/types"; -import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; +import { NotebookContentItem } from "./Notebook/NotebookContentItem"; import type NotebookManager from "./Notebook/NotebookManager"; -import { NotebookPaneContent } from "./Notebook/NotebookManager"; -import { NotebookUtil } from "./Notebook/NotebookUtil"; import { useNotebook } from "./Notebook/useNotebook"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; -import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; -import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; -import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import TabsBase from "./Tabs/TabsBase"; import TerminalTab from "./Tabs/TerminalTab"; import Database from "./Tree/Database"; @@ -87,7 +75,6 @@ export default class Explorer { // Notebooks public notebookManager?: NotebookManager; - private _isInitializingNotebooks: boolean; private notebookToImport: { name: string; content: string; @@ -99,7 +86,6 @@ export default class Explorer { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { dataExplorerArea: Constants.Areas.ResourceTree, }); - this._isInitializingNotebooks = false; this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id); useNotebook.subscribe( @@ -205,12 +191,10 @@ export default class Explorer { container: this, resourceTree: this.resourceTree, refreshCommandBarButtons: () => this.refreshCommandBarButtons(), - refreshNotebookList: () => this.refreshNotebookList(), }); } this.refreshCommandBarButtons(); - this.refreshNotebookList(); } public openEnableSynapseLinkDialog(): void { @@ -373,7 +357,6 @@ export default class Explorer { userContext.authType === AuthType.ResourceToken ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(); - this.refreshNotebookList(); }; // Facade @@ -381,19 +364,6 @@ export default class Explorer { window.open(Constants.Urls.feedbackEmail, "_blank"); }; - public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise { - if (!databaseAccount) { - throw new Error("No database account specified"); - } - - if (this._isInitializingNotebooks) { - return; - } - this._isInitializingNotebooks = true; - this.refreshNotebookList(); - this._isInitializingNotebooks = false; - } - public async allocateContainer(poolId: PoolIdType, mode?: string): Promise { const shouldUseNotebookStates = poolId === PoolIdType.DefaultPoolId ? true : false; const notebookServerInfo = shouldUseNotebookStates @@ -472,8 +442,6 @@ export default class Explorer { ? useNotebook.getState().setIsAllocating(false) : useQueryCopilot.getState().setIsAllocatingContainer(false); this.refreshCommandBarButtons(); - this.refreshNotebookList(); - this._isInitializingNotebooks = false; } } } @@ -510,104 +478,6 @@ export default class Explorer { .then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo)); } - public resetNotebookWorkspace(): void { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) { - handleError( - "Attempt to reset notebook workspace, but notebook is not enabled", - "Explorer/resetNotebookWorkspace", - ); - return; - } - const dialogContent = useNotebook.getState().isPhoenixNotebooks - ? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?" - : "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?"; - - const resetConfirmationDialogProps: DialogProps = { - isModal: true, - title: "Reset Workspace", - subText: dialogContent, - primaryButtonText: "OK", - secondaryButtonText: "Cancel", - onPrimaryButtonClick: this._resetNotebookWorkspace, - onSecondaryButtonClick: () => useDialog.getState().closeDialog(), - }; - useDialog.getState().openDialog(resetConfirmationDialogProps); - } - - private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise { - if (!databaseAccount) { - return false; - } - try { - const { value: workspaces } = await listByDatabaseAccount( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - ); - return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default"); - } catch (error) { - Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace"); - return false; - } - } - - private _resetNotebookWorkspace = async () => { - useDialog.getState().closeDialog(); - const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace"); - let connectionStatus: ContainerConnectionInfo; - try { - const notebookServerInfo = useNotebook.getState().notebookServerInfo; - if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) { - const error = "No server endpoint detected"; - Logger.logError(error, "NotebookContainerClient/resetWorkspace"); - logConsoleError(error); - return; - } - TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, { - dataExplorerArea: Areas.Notebook, - }); - if (useNotebook.getState().isPhoenixNotebooks) { - useTabs.getState().closeAllNotebookTabs(true); - connectionStatus = { - status: ConnectionStatusType.Connecting, - }; - useNotebook.getState().setConnectionInfo(connectionStatus); - } - const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace(); - if (connectionInfo?.status !== HttpStatusCodes.OK) { - throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`); - } - if (!connectionInfo?.data?.phoenixServiceUrl) { - throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`); - } - if (useNotebook.getState().isPhoenixNotebooks) { - await this.setNotebookInfo(true, connectionInfo, connectionStatus); - useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - } - logConsoleInfo("Successfully reset notebook workspace"); - TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, { - dataExplorerArea: Areas.Notebook, - }); - } catch (error) { - logConsoleError(`Failed to reset notebook workspace: ${error}`); - TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, { - dataExplorerArea: Areas.Notebook, - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }); - if (useNotebook.getState().isPhoenixNotebooks) { - connectionStatus = { - status: ConnectionStatusType.Failed, - }; - useNotebook.getState().resetContainerConnection(connectionStatus); - useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - } - throw error; - } finally { - clearInProgressMessage(); - } - }; - private getDeltaDatabases( updatedDatabaseList: DataModels.Database[], databases: ViewModels.Database[], @@ -696,406 +566,6 @@ export default class Explorer { } } - 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, isGithubTree); - promise - .then(() => this.resourceTree.triggerRender()) - .catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", getErrorMessage(reason))); - return promise; - } - - public async importAndOpen(path: string): Promise { - const name = NotebookUtil.getName(path); - const item = NotebookUtil.createNotebookContentItem(name, path, "file"); - const parent = this.resourceTree.myNotebooksContentRoot; - - if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) { - const existingItem = _.find(parent.children, (node) => node.name === name); - if (existingItem) { - return this.openNotebook(existingItem); - } - - const content = await this.readFile(item); - const uploadedItem = await this.uploadFile(name, content, parent); - return this.openNotebook(uploadedItem); - } - - return Promise.resolve(false); - } - - public async importAndOpenContent(name: string, content: string): Promise { - const parent = this.resourceTree.myNotebooksContentRoot; - - if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) { - if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) { - this.notebookToImport = undefined; // we don't want to try opening this notebook again - } - - const existingItem = _.find(parent.children, (node) => node.name === name); - if (existingItem) { - return this.openNotebook(existingItem); - } - - const uploadedItem = await this.uploadFile(name, content, parent); - return this.openNotebook(uploadedItem); - } - - this.notebookToImport = { name, content }; // we'll try opening this notebook later on - return Promise.resolve(false); - } - - public async publishNotebook( - name: string, - content: NotebookPaneContent, - notebookContentRef?: string, - onTakeSnapshot?: (request: SnapshotRequest) => void, - onClosePanel?: () => void, - ): Promise { - if (this.notebookManager) { - await this.notebookManager.openPublishNotebookPane( - name, - content, - notebookContentRef, - onTakeSnapshot, - onClosePanel, - ); - } - } - - public copyNotebook(name: string, content: string): void { - this.notebookManager?.openCopyNotebookPane(name, content); - } - - /** - * Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree. - * Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder. - * Manually creating the missing folders between the root and its parent dir would break the UX: expanding a folder - * will not fetch its content if the children array exists (and has only one child which was manually created). - * Fetching the intermediate folders possibly involves a few chained async calls which isn't ideal. - * - * @param name - * @param path - */ - public createNotebookContentItemFile(name: string, path: string): NotebookContentItem { - return NotebookUtil.createNotebookContentItem(name, path, "file"); - } - - public async openNotebook(notebookContentItem: NotebookContentItem): Promise { - if (!notebookContentItem || !notebookContentItem.path) { - throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`); - } - if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) { - await this.allocateContainer(PoolIdType.DefaultPoolId); - } - - const notebookTabs = useTabs - .getState() - .getTabs( - ViewModels.CollectionTabKind.NotebookV2, - (tab) => - (tab as NotebookV2Tab).notebookPath && - FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path), - ) as NotebookV2Tab[]; - let notebookTab = notebookTabs && notebookTabs[0]; - - if (notebookTab) { - useTabs.getState().activateTab(notebookTab); - } else { - const options: NotebookTabOptions = { - account: userContext.databaseAccount, - tabKind: ViewModels.CollectionTabKind.NotebookV2, - node: undefined, - title: notebookContentItem.name, - tabPath: notebookContentItem.path, - collection: undefined, - masterKey: userContext.masterKey || "", - isTabsContentExpanded: ko.observable(true), - onLoadStartKey: undefined, - container: this, - notebookContentItem, - }; - - try { - const NotebookTabV2 = await import(/* webpackChunkName: "NotebookV2Tab" */ "./Tabs/NotebookV2Tab"); - notebookTab = new NotebookTabV2.default(options); - useTabs.getState().activateNewTab(notebookTab); - } catch (reason) { - console.error("Import NotebookV2Tab failed!", reason); - return false; - } - } - - return true; - } - - 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"); - throw new Error(error); - } - - // Don't delete if tab is open to avoid accidental deletion - const openedNotebookTabs = useTabs - .getState() - .getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => { - return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path); - }); - if (openedNotebookTabs.length > 0) { - useDialog - .getState() - .showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); - } else { - useSidePanel.getState().openSidePanel( - "Rename Notebook", - { - useSidePanel.getState().closeSidePanel(); - this.resourceTree.triggerRender(); - }} - inputLabel="Enter new notebook name" - submitButtonLabel="Rename" - errorMessage="Could not rename notebook" - inProgressMessage="Renaming notebook to" - successMessage="Renamed notebook to" - paneTitle="Rename Notebook" - defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")} - onSubmit={(notebookFile: NotebookContentItem, input: string): Promise => - this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input, isGithubTree) - } - notebookFile={notebookFile} - />, - ); - } - } - - 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"); - throw new Error(error); - } - - useSidePanel.getState().openSidePanel( - "Create new directory", - { - useSidePanel.getState().closeSidePanel(); - this.resourceTree.triggerRender(); - }} - errorMessage="Could not create directory " - inProgressMessage="Creating directory " - successMessage="Created directory " - inputLabel="Enter new directory name" - paneTitle="Create new directory" - submitButtonLabel="Create" - defaultInput="" - onSubmit={(notebookFile: NotebookContentItem, input: string): Promise => - this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input, isGithubTree) - } - notebookFile={parent} - />, - ); - } - - public readFile(notebookFile: NotebookContentItem): Promise { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to read file, but notebook is not enabled"; - handleError(error, "Explorer/downloadFile"); - throw new Error(error); - } - - return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path); - } - - public downloadFile(notebookFile: NotebookContentItem): Promise { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to download file, but notebook is not enabled"; - handleError(error, "Explorer/downloadFile"); - throw new Error(error); - } - - const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Downloading ${notebookFile.path}`); - - return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then( - (content: string) => { - const blob = stringToBlob(content, "text/plain"); - if (navigator.msSaveBlob) { - // for IE and Edge - navigator.msSaveBlob(blob, notebookFile.name); - } else { - const downloadLink: HTMLAnchorElement = document.createElement("a"); - const url = URL.createObjectURL(blob); - downloadLink.href = url; - downloadLink.target = "_self"; - downloadLink.download = notebookFile.name; - - // for some reason, FF displays the download prompt only when - // the link is added to the dom so we add and remove it - document.body.appendChild(downloadLink); - downloadLink.click(); - downloadLink.remove(); - } - - clearMessage(); - }, - (error) => { - logConsoleError(`Could not download notebook ${getErrorMessage(error)}`); - clearMessage(); - }, - ); - } - - private refreshNotebookList = async (): Promise => { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - return; - } - - await this.resourceTree.initialize(); - await useNotebook.getState().initializeNotebooksTree(this.notebookManager); - - this.notebookManager?.refreshPinnedRepos(); - if (this.notebookToImport) { - this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); - } - }; - - 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"); - throw new Error(error); - } - - // Don't delete if tab is open to avoid accidental deletion - const openedNotebookTabs = useTabs - .getState() - .getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => { - return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path); - }); - if (openedNotebookTabs.length > 0) { - useDialog - .getState() - .showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); - return Promise.reject(); - } - - if (item.type === NotebookContentItemType.Directory && item.children && item.children.length > 0) { - useDialog.getState().openDialog({ - isModal: true, - title: "Unable to delete file", - subText: "Directory is not empty.", - primaryButtonText: "Close", - secondaryButtonText: undefined, - onPrimaryButtonClick: () => useDialog.getState().closeDialog(), - onSecondaryButtonClick: undefined, - }); - return Promise.reject(); - } - - return this.notebookManager?.notebookContentClient.deleteContentItem(item, isGithubTree).then( - () => logConsoleInfo(`Successfully deleted: ${item.path}`), - (reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`), - ); - } - - /** - * This creates a new notebook file, then opens the notebook - */ - public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to create new notebook, but notebook is not enabled"; - handleError(error, "Explorer/onNewNotebookClicked"); - throw new Error(error); - } - if (useNotebook.getState().isPhoenixNotebooks) { - if (isGithubTree) { - await this.allocateContainer(PoolIdType.DefaultPoolId); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - } else { - useDialog.getState().showOkCancelModalDialog( - Notebook.newNotebookModalTitle, - undefined, - "Create", - async () => { - await this.allocateContainer(PoolIdType.DefaultPoolId); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - }, - "Cancel", - undefined, - this.getNewNoteWarningText(), - ); - } - } else { - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - } - } - - private getNewNoteWarningText(): JSX.Element { - return ( - <> -

{Notebook.newNotebookModalContent1}

-
-

- {Notebook.newNotebookModalContent2} - - {Notebook.learnMore} - -

- - ); - } - - private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void { - const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`); - const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, { - dataExplorerArea: Constants.Areas.Notebook, - }); - - this.notebookManager?.notebookContentClient - .createNewNotebookFile(parent, isGithubTree) - .then((newFile: NotebookContentItem) => { - logConsoleInfo(`Successfully created: ${newFile.name}`); - TelemetryProcessor.traceSuccess( - Action.CreateNewNotebook, - { - dataExplorerArea: Constants.Areas.Notebook, - }, - startKey, - ); - return this.openNotebook(newFile); - }) - .then(() => this.resourceTree.triggerRender()) - .catch((error) => { - const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`; - logConsoleError(errorMessage); - TelemetryProcessor.traceFailure( - Action.CreateNewNotebook, - { - dataExplorerArea: Constants.Areas.Notebook, - error: errorMessage, - errorStack: getErrorStack(error), - }, - startKey, - ); - }) - .finally(clearInProgressMessage); - } - // TODO: Delete this function when ResourceTreeAdapter is removed. public async refreshContentItem(item: NotebookContentItem): Promise { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { @@ -1252,32 +722,6 @@ export default class Explorer { } } - public async handleOpenFileAction(path: string): Promise { - if (useNotebook.getState().isPhoenixNotebooks === undefined) { - await useNotebook.getState().getPhoenixStatus(); - } - if (useNotebook.getState().isPhoenixNotebooks) { - await this.allocateContainer(PoolIdType.DefaultPoolId); - } - - // We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb - // when launching a notebook quickstart from Portal. In future we should just use gallery id and use Juno to fetch instead of directly - // calling GitHub. For now convert this url to a raw url and download content. - const gitHubInfo = fromContentUri(path); - if (gitHubInfo) { - const rawUrl = toRawContentUri(gitHubInfo.owner, gitHubInfo.repo, gitHubInfo.branch, gitHubInfo.path); - const response = await fetch(rawUrl); - if (response.status === Constants.HttpStatusCodes.OK) { - this.notebookToImport = { - name: NotebookUtil.getName(path), - content: await response.text(), - }; - - this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); - } - } - } - public openUploadItemsPanePane(): void { useSidePanel.getState().openSidePanel("Upload " + getUploadName(), ); } @@ -1287,54 +731,6 @@ export default class Explorer { .openSidePanel("Input parameters", ); } - public openUploadFilePanel(parent?: NotebookContentItem): void { - if (useNotebook.getState().isPhoenixNotebooks) { - useDialog.getState().showOkCancelModalDialog( - Notebook.newNotebookUploadModalTitle, - undefined, - "Upload", - async () => { - await this.allocateContainer(PoolIdType.DefaultPoolId); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.uploadFilePanel(parent); - }, - "Cancel", - undefined, - this.getNewNoteWarningText(), - ); - } else { - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.uploadFilePanel(parent); - } - } - - private uploadFilePanel(parent?: NotebookContentItem): void { - useSidePanel - .getState() - .openSidePanel( - "Upload file to notebook server", - this.uploadFile(name, content, parent)} />, - ); - } - - public getDownloadModalConent(fileName: string): JSX.Element { - if (useNotebook.getState().isPhoenixNotebooks) { - return ( - <> -

{Notebook.galleryNotebookDownloadContent1}

-
-

- {Notebook.galleryNotebookDownloadContent2} - - {Notebook.learnMore} - -

- - ); - } - return

Download {fileName} from gallery as a copy to your notebooks to run and/or edit the notebook.

; - } - public async refreshExplorer(): Promise { if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") { userContext.authType === AuthType.ResourceToken @@ -1359,10 +755,6 @@ export default class Explorer { dataExplorerArea: Constants.Areas.Notebook, }); - if (useNotebook.getState().isPhoenixNotebooks) { - await this.initNotebooks(userContext.databaseAccount); - } - await this.refreshSampleData(); } diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx index 3ccbefcaf..45afe6061 100644 --- a/src/Explorer/Notebook/NotebookManager.tsx +++ b/src/Explorer/Notebook/NotebookManager.tsx @@ -12,15 +12,13 @@ import * as Logger from "../../Common/Logger"; import { GitHubClient } from "../../GitHub/GitHubClient"; import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider"; import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService"; -import { useSidePanel } from "../../hooks/useSidePanel"; import { JunoClient } from "../../Juno/JunoClient"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import { userContext } from "../../UserContext"; import { getFullName } from "../../Utils/UserUtils"; +import { useSidePanel } from "../../hooks/useSidePanel"; import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; -import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane"; import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; @@ -40,7 +38,6 @@ export interface NotebookManagerOptions { container: Explorer; resourceTree: ResourceTreeAdapter; refreshCommandBarButtons: () => void; - refreshNotebookList: () => void; } export default class NotebookManager { @@ -81,10 +78,6 @@ export default class NotebookManager { contents.JupyterContentProvider, ); - this.notebookClient = new NotebookContainerClient(() => - this.params.container.initNotebooks(userContext?.databaseAccount), - ); - this.notebookContentClient = new NotebookContentClient(this.notebookContentProvider); this.gitHubOAuthService.getTokenObservable().subscribe((token) => { @@ -106,11 +99,9 @@ export default class NotebookManager { } this.params.refreshCommandBarButtons(); - this.params.refreshNotebookList(); }); this.junoClient.subscribeToPinnedRepos((pinnedRepos) => { - this.params.resourceTree.initializeGitHubRepos(pinnedRepos); this.params.resourceTree.triggerRender(); useNotebook.getState().initializeGitHubRepos(pinnedRepos); }); @@ -149,22 +140,6 @@ export default class NotebookManager { ); } - public openCopyNotebookPane(name: string, content: string): void { - const { container } = this.params; - useSidePanel - .getState() - .openSidePanel( - "Copy Notebook", - , - ); - } - // Octokit's error handler uses any // eslint-disable-next-line @typescript-eslint/no-explicit-any private onGitHubClientError = (error: any): void => { diff --git a/src/Explorer/OpenActions/OpenActions.tsx b/src/Explorer/OpenActions/OpenActions.tsx index f3ef288c8..e2059cdba 100644 --- a/src/Explorer/OpenActions/OpenActions.tsx +++ b/src/Explorer/OpenActions/OpenActions.tsx @@ -195,17 +195,5 @@ export function handleOpenAction( return true; } - if ( - action.actionType === ActionContracts.ActionType.OpenSampleNotebook || - action.actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenSampleNotebook] - ) { - openFile(action as ActionContracts.OpenSampleNotebook, explorer); - return true; - } - return false; } - -function openFile(action: ActionContracts.OpenSampleNotebook, explorer: Explorer) { - explorer.handleOpenFileAction(decodeURIComponent(action.path)); -} diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx deleted file mode 100644 index 0f7927b3b..000000000 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { IDropdownOption } from "@fluentui/react"; -import React, { FormEvent, FunctionComponent, useEffect, useState } from "react"; -import { HttpStatusCodes, PoolIdType } from "../../../Common/Constants"; -import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils"; -import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; -import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient"; -import * as GitHubUtils from "../../../Utils/GitHubUtils"; -import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; -import { useSidePanel } from "../../../hooks/useSidePanel"; -import Explorer from "../../Explorer"; -import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem"; -import { useNotebook } from "../../Notebook/useNotebook"; -import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; -import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent"; - -interface Location { - type: "MyNotebooks" | "GitHub"; - - // GitHub - owner?: string; - repo?: string; - branch?: string; -} -export interface CopyNotebookPanelProps { - name: string; - content: string; - container: Explorer; - junoClient: JunoClient; - gitHubOAuthService: GitHubOAuthService; -} - -export const CopyNotebookPane: FunctionComponent = ({ - name, - content, - container, - junoClient, - gitHubOAuthService, -}: CopyNotebookPanelProps) => { - const closeSidePanel = useSidePanel((state) => state.closeSidePanel); - const [isExecuting, setIsExecuting] = useState(); - const [formError, setFormError] = useState(""); - const [pinnedRepos, setPinnedRepos] = useState(); - const [selectedLocation, setSelectedLocation] = useState(); - - useEffect(() => { - open(); - }, []); - - const open = async (): Promise => { - if (gitHubOAuthService.isLoggedIn()) { - const response = await junoClient.getPinnedRepos(gitHubOAuthService.getTokenObservable()()?.scope); - if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { - handleError(`Received HTTP ${response.status} when fetching pinned repos`, "CopyNotebookPaneAdapter/submit"); - } - - if (response.data?.length > 0) { - setPinnedRepos(response.data); - } - } - }; - - const submit = async (): Promise => { - let destination: string = selectedLocation?.type; - let clearMessage: () => void; - setIsExecuting(true); - - try { - if (!selectedLocation) { - throw new Error(`No location selected`); - } - - if (selectedLocation.type === "GitHub") { - destination = `${destination} - ${GitHubUtils.toRepoFullName( - selectedLocation.owner, - selectedLocation.repo, - )} - ${selectedLocation.branch}`; - } else if (selectedLocation.type === "MyNotebooks" && useNotebook.getState().isPhoenixNotebooks) { - destination = useNotebook.getState().notebookFolderName; - } - - clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`); - - const notebookContentItem = await copyNotebook(selectedLocation); - if (!notebookContentItem) { - throw new Error(`Failed to upload ${name}`); - } - - NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${name} to ${destination}`); - closeSidePanel(); - } catch (error) { - const errorMessage = getErrorMessage(error); - setFormError(`Failed to copy ${name} to ${destination}`); - handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError); - } finally { - clearMessage && clearMessage(); - setIsExecuting(false); - } - }; - - const copyNotebook = async (location: Location): Promise => { - let parent: NotebookContentItem; - let isGithubTree: boolean; - switch (location.type) { - case "MyNotebooks": - parent = { - name: useNotebook.getState().notebookFolderName, - path: useNotebook.getState().notebookBasePath, - type: NotebookContentItemType.Directory, - }; - isGithubTree = false; - if (useNotebook.getState().isPhoenixNotebooks) { - await container.allocateContainer(PoolIdType.DefaultPoolId); - } - break; - - case "GitHub": - parent = { - 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, isGithubTree); - }; - - const onDropDownChange = (_: FormEvent, option?: IDropdownOption): void => { - setSelectedLocation(option?.data); - }; - - const props: RightPaneFormProps = { - formError, - isExecuting: isExecuting, - submitButtonText: "OK", - onSubmit: () => submit(), - }; - - const copyNotebookPaneProps: CopyNotebookPaneProps = { - name, - pinnedRepos, - onDropDownChange: onDropDownChange, - }; - - return ( - - - - ); -}; diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx deleted file mode 100644 index 5cd0cfdc1..000000000 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { - Dropdown, - IDropdownOption, - IDropdownProps, - IRenderFunction, - ISelectableOption, - Label, - SelectableOptionMenuItemType, - Stack, - Text, -} from "@fluentui/react"; -import { GitHubReposTitle } from "Explorer/Tree/ResourceTree"; -import React, { FormEvent, FunctionComponent } from "react"; -import { IPinnedRepo } from "../../../Juno/JunoClient"; -import * as GitHubUtils from "../../../Utils/GitHubUtils"; -import { useNotebook } from "../../Notebook/useNotebook"; - -interface Location { - type: "MyNotebooks" | "GitHub"; - - // GitHub - owner?: string; - repo?: string; - branch?: string; -} - -export interface CopyNotebookPaneProps { - name: string; - pinnedRepos: IPinnedRepo[]; - onDropDownChange: (_: FormEvent, option?: IDropdownOption) => void; -} - -export const CopyNotebookPaneComponent: FunctionComponent = ({ - name, - pinnedRepos, - onDropDownChange, -}: CopyNotebookPaneProps) => { - const BranchNameWhiteSpace = " "; - - const onRenderDropDownTitle: IRenderFunction = (options: IDropdownOption[]): JSX.Element => { - return {options.length && options[0].title}; - }; - - const onRenderDropDownOption: IRenderFunction = (option: ISelectableOption): JSX.Element => { - return {option.text}; - }; - - const getDropDownOptions = (): IDropdownOption[] => { - const options: IDropdownOption[] = []; - options.push({ - key: "MyNotebooks-Item", - text: useNotebook.getState().notebookFolderName, - title: useNotebook.getState().notebookFolderName, - data: { - type: "MyNotebooks", - } as Location, - }); - - if (pinnedRepos && pinnedRepos.length > 0) { - options.push({ - key: "GitHub-Header-Divider", - text: undefined, - itemType: SelectableOptionMenuItemType.Divider, - }); - - options.push({ - key: "GitHub-Header", - text: GitHubReposTitle, - itemType: SelectableOptionMenuItemType.Header, - }); - - pinnedRepos.forEach((pinnedRepo) => { - const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); - options.push({ - key: `GitHub-Repo-${repoFullName}`, - text: repoFullName, - disabled: true, - }); - - pinnedRepo.branches.forEach((branch) => - options.push({ - key: `GitHub-Repo-${repoFullName}-${branch.name}`, - text: `${BranchNameWhiteSpace}${branch.name}`, - title: `${repoFullName} - ${branch.name}`, - data: { - type: "GitHub", - owner: pinnedRepo.owner, - repo: pinnedRepo.name, - branch: branch.name, - } as Location, - }), - ); - }); - } - - return options; - }; - const dropDownProps: IDropdownProps = { - label: "Location", - ariaLabel: "Location", - placeholder: "Select an option", - onRenderTitle: onRenderDropDownTitle, - onRenderOption: onRenderDropDownOption, - options: getDropDownOptions(), - onChange: onDropDownChange, - }; - - return ( -
- - - - {name} - - - - -
- ); -}; diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index 4a3a8942e..aa2137c42 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -17,8 +17,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` addRepoProps={ Object { "container": Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -35,10 +33,8 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, }, diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index 8054abe19..566808f4b 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -7,8 +7,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` errorMessage="Could not create directory " explorer={ Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -25,10 +23,8 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, } diff --git a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap index 26b52ff90..6d875fe2e 100644 --- a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap +++ b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap @@ -22,8 +22,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] = databaseId="CopilotSampleDb" explorer={ Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -40,10 +38,8 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] = "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, } diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index f4ebb9cd0..e9477e088 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -25,11 +25,9 @@ import * as React from "react"; import ConnectIcon from "../../../images/Connect_color.svg"; import ContainersIcon from "../../../images/Containers.svg"; import LinkIcon from "../../../images/Link_blue.svg"; -import NotebookColorIcon from "../../../images/Notebooks.svg"; import PowerShellIcon from "../../../images/PowerShell.svg"; import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg"; import QuickStartIcon from "../../../images/Quickstart_Lightning.svg"; -import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; import * as Constants from "../../Common/Constants"; import { userContext } from "../../UserContext"; @@ -410,14 +408,6 @@ export class SplashScreen extends React.Component { }, }; heroes.push(launchQuickstartBtn); - } else if (useNotebook.getState().isPhoenixNotebooks) { - const newNotebookBtn = { - iconSrc: NotebookColorIcon, - title: "New notebook", - description: "Visualize your data stored in Azure Cosmos DB", - onClick: () => this.container.onNewNotebookClicked(), - }; - heroes.push(newNotebookBtn); } heroes.push(this.getShellCard()); @@ -493,28 +483,12 @@ export class SplashScreen extends React.Component { }; } - private decorateOpenNotebookActivity({ name, path }: MostRecentActivity.OpenNotebookItem) { - return { - info: path, - iconSrc: NotebookIcon, - title: name, - description: "Notebook", - onClick: () => { - const notebookItem = this.container.createNotebookContentItemFile(name, path); - notebookItem && this.container.openNotebook(notebookItem); - }, - }; - } - private createRecentItems(): SplashScreenItem[] { return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((activity) => { switch (activity.type) { default: { - const unknownActivity: never = activity; - throw new Error(`Unknown activity: ${unknownActivity}`); + throw new Error(`Unknown activity: ${activity}`); } - case MostRecentActivity.Type.OpenNotebook: - return this.decorateOpenNotebookActivity(activity); case MostRecentActivity.Type.OpenCollection: return this.decorateOpenCollectionActivity(activity); diff --git a/src/Explorer/Tabs/NotebookV2Tab.ts b/src/Explorer/Tabs/NotebookV2Tab.ts index 92e0e6958..fadb43258 100644 --- a/src/Explorer/Tabs/NotebookV2Tab.ts +++ b/src/Explorer/Tabs/NotebookV2Tab.ts @@ -1,31 +1,11 @@ -import { stringifyNotebook, toJS } from "@nteract/commutable"; import * as ko from "knockout"; import * as Q from "q"; -import { userContext } from "UserContext"; -import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg"; -import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; -import CutIcon from "../../../images/notebook/Notebook-cut.svg"; -import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg"; -import PasteIcon from "../../../images/notebook/Notebook-paste.svg"; -import RestartIcon from "../../../images/notebook/Notebook-restart.svg"; -import RunAllIcon from "../../../images/notebook/Notebook-run-all.svg"; -import RunIcon from "../../../images/notebook/Notebook-run.svg"; -import { default as InterruptKernelIcon, default as KillKernelIcon } from "../../../images/notebook/Notebook-stop.svg"; -import SaveIcon from "../../../images/save-cosmos.svg"; -import { useNotebookSnapshotStore } from "../../hooks/useNotebookSnapshotStore"; -import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils"; import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { useDialog } from "../Controls/Dialog"; -import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory"; -import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2"; -import * as CdbActions from "../Notebook/NotebookComponent/actions"; import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter"; -import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types"; import { NotebookContentItem } from "../Notebook/NotebookContentItem"; -import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase"; @@ -90,275 +70,7 @@ export default class NotebookTabV2 extends NotebookTabBase { } protected getTabsButtons(): CommandButtonComponentProps[] { - const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs(); - const isNotebookUntrusted = this.notebookComponentAdapter.isNotebookUntrusted(); - - const runBtnTooltip = isNotebookUntrusted ? NotebookUtil.UntrustedNotebookRunHint : undefined; - - const saveLabel = "Save"; - const copyToLabel = "Copy to ..."; - const publishLabel = "Publish to gallery"; - const kernelLabel = "No Kernel"; - const runLabel = "Run"; - const runActiveCellLabel = "Run Active Cell"; - const runAllLabel = "Run All"; - const interruptKernelLabel = "Interrupt Kernel"; - const killKernelLabel = "Halt Kernel"; - const restartKernelLabel = "Restart Kernel"; - const clearLabel = "Clear outputs"; - const newCellLabel = "New Cell"; - const cellTypeLabel = "Cell Type"; - const codeLabel = "Code"; - const markdownLabel = "Markdown"; - const rawLabel = "Raw"; - const copyLabel = "Copy"; - const cutLabel = "Cut"; - const pasteLabel = "Paste"; - const cellCodeType = "code"; - const cellMarkdownType = "markdown"; - const cellRawType = "raw"; - - const saveButtonChildren = []; - if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - saveButtonChildren.push({ - iconName: copyToLabel, - onCommandClick: () => this.copyNotebook(), - commandButtonLabel: copyToLabel, - hasPopup: false, - disabled: false, - ariaLabel: copyToLabel, - }); - } - - if (userContext.features.publicGallery) { - saveButtonChildren.push({ - iconName: "PublishContent", - onCommandClick: async () => await this.publishToGallery(), - commandButtonLabel: publishLabel, - hasPopup: false, - disabled: false, - ariaLabel: publishLabel, - }); - } - - let buttons: CommandButtonComponentProps[] = [ - { - iconSrc: SaveIcon, - iconAlt: saveLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookSave(), - commandButtonLabel: saveLabel, - hasPopup: false, - disabled: false, - ariaLabel: saveLabel, - children: saveButtonChildren.length && [ - { - iconName: "Save", - onCommandClick: () => this.notebookComponentAdapter.notebookSave(), - commandButtonLabel: saveLabel, - hasPopup: false, - disabled: false, - ariaLabel: saveLabel, - }, - ...saveButtonChildren, - ], - }, - { - iconSrc: null, - iconAlt: kernelLabel, - onCommandClick: () => {}, - commandButtonLabel: null, - hasPopup: false, - disabled: availableKernels.length < 1, - isDropdown: true, - dropdownPlaceholder: kernelLabel, - dropdownSelectedKey: this.notebookComponentAdapter.getSelectedKernelName(), //this.currentKernelName, - dropdownWidth: 100, - children: availableKernels.map( - (kernel: KernelSpecsDisplay) => - ({ - iconSrc: null, - iconAlt: kernel.displayName, - onCommandClick: () => this.notebookComponentAdapter.notebookChangeKernel(kernel.name), - commandButtonLabel: kernel.displayName, - dropdownItemKey: kernel.name, - hasPopup: false, - disabled: false, - ariaLabel: kernel.displayName, - }) as CommandButtonComponentProps, - ), - ariaLabel: kernelLabel, - }, - { - iconSrc: RunIcon, - iconAlt: runLabel, - onCommandClick: () => { - this.notebookComponentAdapter.notebookRunAndAdvance(); - this.traceTelemetry(Action.ExecuteCell); - }, - commandButtonLabel: runLabel, - tooltipText: runBtnTooltip, - ariaLabel: runLabel, - hasPopup: false, - disabled: isNotebookUntrusted, - children: [ - { - iconSrc: RunIcon, - iconAlt: runActiveCellLabel, - onCommandClick: () => { - this.notebookComponentAdapter.notebookRunAndAdvance(); - this.traceTelemetry(Action.ExecuteCell); - }, - commandButtonLabel: runActiveCellLabel, - hasPopup: false, - disabled: false, - ariaLabel: runActiveCellLabel, - }, - { - iconSrc: RunAllIcon, - iconAlt: runAllLabel, - onCommandClick: () => { - this.notebookComponentAdapter.notebookRunAll(); - this.traceTelemetry(Action.ExecuteAllCells); - }, - commandButtonLabel: runAllLabel, - hasPopup: false, - disabled: false, - ariaLabel: runAllLabel, - }, - { - iconSrc: InterruptKernelIcon, - iconAlt: interruptKernelLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookInterruptKernel(), - commandButtonLabel: interruptKernelLabel, - hasPopup: false, - disabled: false, - ariaLabel: interruptKernelLabel, - }, - { - iconSrc: KillKernelIcon, - iconAlt: killKernelLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookKillKernel(), - commandButtonLabel: killKernelLabel, - hasPopup: false, - disabled: false, - ariaLabel: killKernelLabel, - }, - { - iconSrc: RestartIcon, - iconAlt: restartKernelLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookRestartKernel(), - commandButtonLabel: restartKernelLabel, - hasPopup: false, - disabled: false, - ariaLabel: restartKernelLabel, - }, - ], - }, - { - iconSrc: ClearAllOutputsIcon, - iconAlt: clearLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookClearAllOutputs(), - commandButtonLabel: clearLabel, - hasPopup: false, - disabled: false, - ariaLabel: clearLabel, - }, - { - iconSrc: NewCellIcon, - iconAlt: newCellLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookInsertBelow(), - commandButtonLabel: newCellLabel, - ariaLabel: newCellLabel, - hasPopup: false, - disabled: false, - }, - CommandBarComponentButtonFactory.createDivider(), - { - iconSrc: null, - iconAlt: null, - onCommandClick: () => {}, - commandButtonLabel: null, - ariaLabel: cellTypeLabel, - hasPopup: false, - disabled: false, - isDropdown: true, - dropdownPlaceholder: cellTypeLabel, - dropdownSelectedKey: this.notebookComponentAdapter.getActiveCellTypeStr(), - dropdownWidth: 110, - children: [ - { - iconSrc: null, - iconAlt: null, - onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellCodeType), - commandButtonLabel: codeLabel, - ariaLabel: codeLabel, - dropdownItemKey: cellCodeType, - hasPopup: false, - disabled: false, - }, - { - iconSrc: null, - iconAlt: null, - onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellMarkdownType), - commandButtonLabel: markdownLabel, - ariaLabel: markdownLabel, - dropdownItemKey: cellMarkdownType, - hasPopup: false, - disabled: false, - }, - { - iconSrc: null, - iconAlt: null, - onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellRawType), - commandButtonLabel: rawLabel, - ariaLabel: rawLabel, - dropdownItemKey: cellRawType, - hasPopup: false, - disabled: false, - }, - ], - }, - { - iconSrc: CopyIcon, - iconAlt: copyLabel, - onCommandClick: () => this.notebookComponentAdapter.notebokCopy(), - commandButtonLabel: copyLabel, - ariaLabel: copyLabel, - hasPopup: false, - disabled: false, - children: [ - { - iconSrc: CopyIcon, - iconAlt: copyLabel, - onCommandClick: () => this.notebookComponentAdapter.notebokCopy(), - commandButtonLabel: copyLabel, - ariaLabel: copyLabel, - hasPopup: false, - disabled: false, - }, - { - iconSrc: CutIcon, - iconAlt: cutLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookCut(), - commandButtonLabel: cutLabel, - ariaLabel: cutLabel, - hasPopup: false, - disabled: false, - }, - { - iconSrc: PasteIcon, - iconAlt: pasteLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookPaste(), - commandButtonLabel: pasteLabel, - ariaLabel: pasteLabel, - hasPopup: false, - disabled: false, - }, - ], - }, - // TODO: Uncomment when undo/redo is reimplemented in nteract - ]; - return buttons; + return []; } protected buildCommandBarOptions(): void { @@ -382,50 +94,4 @@ export default class NotebookTabV2 extends NotebookTabBase { sparkClusterConnectionInfo, ); } - - private publishToGallery = async () => { - TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, { - source: Source.CommandBarMenu, - }); - - const notebookReduxStore = NotebookTabV2.clientManager.getStore(); - const unsubscribe = notebookReduxStore.subscribe(() => { - const cdbState = (notebookReduxStore.getState() as CdbAppState).cdb; - useNotebookSnapshotStore.setState({ - snapshot: cdbState.notebookSnapshot?.imageSrc, - error: cdbState.notebookSnapshotError, - }); - }); - - const notebookContent = this.notebookComponentAdapter.getContent(); - const notebookContentRef = this.notebookComponentAdapter.contentRef; - const onPanelClose = (): void => { - unsubscribe(); - useNotebookSnapshotStore.setState({ - snapshot: undefined, - error: undefined, - }); - notebookReduxStore.dispatch(CdbActions.takeNotebookSnapshot(undefined)); - }; - - await this.container.publishNotebook( - notebookContent.name, - notebookContent.content, - notebookContentRef, - (request: SnapshotRequest) => notebookReduxStore.dispatch(CdbActions.takeNotebookSnapshot(request)), - onPanelClose, - ); - }; - - private copyNotebook = () => { - const notebookContent = this.notebookComponentAdapter.getContent(); - let content: string; - if (typeof notebookContent.content === "string") { - content = notebookContent.content; - } else { - content = stringifyNotebook(toJS(notebookContent.content)); - } - - this.container.copyNotebook(notebookContent.name, content); - }; } diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index b5f759534..0933eac7b 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -1,42 +1,23 @@ -import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { SampleDataTree } from "Explorer/Tree/SampleDataTree"; import { getItemName } from "Utils/APITypeUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import * as React from "react"; import shallow from "zustand/shallow"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; -import GalleryIcon from "../../../images/GalleryIcon.svg"; -import DeleteIcon from "../../../images/delete.svg"; -import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; -import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; -import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; -import FileIcon from "../../../images/notebook/file-cosmos.svg"; -import PublishIcon from "../../../images/notebook/publish_content.svg"; -import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; -import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; -import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; -import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { isServerlessAccount } from "../../Utils/CapabilityUtils"; -import * as GitHubUtils from "../../Utils/GitHubUtils"; -import { useSidePanel } from "../../hooks/useSidePanel"; import { useTabs } from "../../hooks/useTabs"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; -import { useDialog } from "../Controls/Dialog"; -import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; +import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; -import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; -import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; -import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import TabsBase from "../Tabs/TabsBase"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; @@ -45,391 +26,21 @@ import StoredProcedure from "./StoredProcedure"; import Trigger from "./Trigger"; import UserDefinedFunction from "./UserDefinedFunction"; -export const MyNotebooksTitle = "My Notebooks"; -export const GitHubReposTitle = "GitHub repos"; - interface ResourceTreeProps { container: Explorer; } export const ResourceTree: React.FC = ({ container }: ResourceTreeProps): JSX.Element => { const databases = useDatabases((state) => state.databases); - const { - isNotebookEnabled, - myNotebooksContentRoot, - galleryContentRoot, - gitHubNotebooksContentRoot, - updateNotebookItem, - } = useNotebook( + const { isNotebookEnabled } = useNotebook( (state) => ({ isNotebookEnabled: state.isNotebookEnabled, - myNotebooksContentRoot: state.myNotebooksContentRoot, - galleryContentRoot: state.galleryContentRoot, - gitHubNotebooksContentRoot: state.gitHubNotebooksContentRoot, - updateNotebookItem: state.updateNotebookItem, }), shallow, ); - const { activeTab, refreshActiveTab } = useTabs(); + const { refreshActiveTab } = useTabs(); const showScriptNodes = configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin"); - const pseudoDirPath = "PsuedoDir"; - - const buildGalleryCallout = (): JSX.Element => { - if ( - LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) && - LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed) - ) { - return undefined; - } - - const calloutProps: ICalloutProps = { - calloutMaxWidth: 350, - ariaLabel: "New gallery", - role: "alertdialog", - gapSpace: 0, - target: ".galleryHeader", - directionalHint: DirectionalHint.leftTopEdge, - onDismiss: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - }, - setInitialFocus: true, - }; - - const openGalleryProps: ILinkProps = { - onClick: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - container.openGallery(); - }, - }; - - return ( - - - - New gallery - - - Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other - contributors. - - Open gallery - - - ); - }; - - const buildNotebooksTree = (): TreeNode => { - const notebooksTree: TreeNode = { - label: undefined, - isExpanded: true, - children: [], - }; - - if (!useNotebook.getState().isPhoenixNotebooks) { - notebooksTree.children.push(buildNotebooksTemporarilyDownTree()); - } else { - if (galleryContentRoot) { - notebooksTree.children.push(buildGalleryNotebooksTree()); - } - - if ( - myNotebooksContentRoot && - useNotebook.getState().isPhoenixNotebooks && - useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected - ) { - notebooksTree.children.push(buildMyNotebooksTree()); - } - if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - // collapse all other notebook nodes - notebooksTree.children.forEach((node) => (node.isExpanded = false)); - notebooksTree.children.push(buildGitHubNotebooksTree(true)); - } - } - return notebooksTree; - }; - - const buildNotebooksTemporarilyDownTree = (): TreeNode => { - return { - label: Notebook.temporarilyDownMsg, - className: "clickDisabled", - }; - }; - - const buildGalleryNotebooksTree = (): TreeNode => { - return { - label: "Gallery", - iconSrc: GalleryIcon, - className: "notebookHeader galleryHeader", - onClick: () => container.openGallery(), - isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery, - }; - }; - - const buildMyNotebooksTree = (): TreeNode => { - const myNotebooksTree: TreeNode = buildNotebookDirectoryNode( - myNotebooksContentRoot, - (item: NotebookContentItem) => { - container.openNotebook(item); - }, - ); - - myNotebooksTree.isExpanded = true; - myNotebooksTree.isAlphaSorted = true; - // Remove "Delete" menu item from context menu - myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); - return myNotebooksTree; - }; - - const buildGitHubNotebooksTree = (isConnected: boolean): TreeNode => { - const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode( - gitHubNotebooksContentRoot, - (item: NotebookContentItem) => { - container.openNotebook(item); - }, - true, - ); - const manageGitContextMenu: TreeNodeMenuItem[] = [ - { - label: "Manage GitHub settings", - onClick: () => - useSidePanel - .getState() - .openSidePanel( - "Manage GitHub settings", - , - ), - }, - { - label: "Disconnect from GitHub", - onClick: () => { - TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { - dataExplorerArea: Areas.Notebook, - }); - container.notebookManager?.gitHubOAuthService.logout(); - }, - }, - ]; - gitHubNotebooksTree.contextMenu = manageGitContextMenu; - gitHubNotebooksTree.isExpanded = true; - gitHubNotebooksTree.isAlphaSorted = true; - - return gitHubNotebooksTree; - }; - - const buildChildNodes = ( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - isGithubTree?: boolean, - ): TreeNode[] => { - if (!item || !item.children) { - return []; - } else { - return item.children.map((item) => { - const result = - item.type === NotebookContentItemType.Directory - ? buildNotebookDirectoryNode(item, onFileClick, isGithubTree) - : buildNotebookFileNode(item, onFileClick, isGithubTree); - result.timestamp = item.timestamp; - return result; - }); - } - }; - - const buildNotebookFileNode = ( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - isGithubTree?: boolean, - ): TreeNode => { - return { - label: item.name, - iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon, - className: "notebookHeader", - onClick: () => onFileClick(item), - isSelected: () => { - return ( - activeTab && - activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && - /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. - NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. - */ - (activeTab as any).notebookPath() === item.path - ); - }, - contextMenu: createFileContextMenu(container, item, isGithubTree), - data: item, - }; - }; - - const createFileContextMenu = ( - container: Explorer, - item: NotebookContentItem, - isGithubTree?: boolean, - ): TreeNodeMenuItem[] => { - let items: TreeNodeMenuItem[] = [ - { - label: "Rename", - iconSrc: NotebookIcon, - onClick: () => container.renameNotebook(item, isGithubTree), - }, - { - label: "Delete", - iconSrc: DeleteIcon, - onClick: () => { - useDialog - .getState() - .showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}"`, - "Delete", - () => container.deleteNotebookFile(item, isGithubTree), - "Cancel", - undefined, - ); - }, - }, - { - label: "Copy to ...", - iconSrc: CopyIcon, - onClick: () => copyNotebook(container, item), - }, - { - label: "Download", - iconSrc: NotebookIcon, - onClick: () => container.downloadFile(item), - }, - ]; - - if (item.type === NotebookContentItemType.Notebook && userContext.features.publicGallery) { - items.push({ - label: "Publish to gallery", - iconSrc: PublishIcon, - onClick: async () => { - TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, { - source: Source.ResourceTreeMenu, - }); - - const content = await container.readFile(item); - if (content) { - await container.publishNotebook(item.name, content); - } - }, - }); - } - - // "Copy to ..." isn't needed if github locations are not available - if (!container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - items = items.filter((item) => item.label !== "Copy to ..."); - } - - return items; - }; - - const copyNotebook = async (container: Explorer, item: NotebookContentItem) => { - const content = await container.readFile(item); - if (content) { - container.copyNotebook(item.name, content); - } - }; - - const createDirectoryContextMenu = ( - container: Explorer, - item: NotebookContentItem, - isGithubTree?: boolean, - ): TreeNodeMenuItem[] => { - let items: TreeNodeMenuItem[] = [ - { - label: "Refresh", - iconSrc: RefreshIcon, - onClick: () => loadSubitems(item, isGithubTree), - }, - { - label: "Delete", - iconSrc: DeleteIcon, - onClick: () => { - useDialog - .getState() - .showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}?"`, - "Delete", - () => container.deleteNotebookFile(item, isGithubTree), - "Cancel", - undefined, - ); - }, - }, - { - label: "Rename", - iconSrc: NotebookIcon, - onClick: () => container.renameNotebook(item, isGithubTree), - }, - { - label: "New Directory", - iconSrc: NewNotebookIcon, - onClick: () => container.onCreateDirectory(item, isGithubTree), - }, - { - label: "Upload File", - iconSrc: NewNotebookIcon, - onClick: () => container.openUploadFilePanel(item), - }, - ]; - - //disallow renaming of temporary notebook workspace - if (item?.path === useNotebook.getState().notebookBasePath) { - items = items.filter((item) => item.label !== "Rename"); - } - - // For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File" - if (GitHubUtils.fromContentUri(item.path)) { - items = items.filter( - (item) => - item.label !== "Delete" && - item.label !== "Rename" && - item.label !== "New Directory" && - item.label !== "Upload File", - ); - } - - return items; - }; - - const buildNotebookDirectoryNode = ( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - isGithubTree?: boolean, - ): TreeNode => { - return { - label: item.name, - iconSrc: undefined, - className: "notebookHeader", - isAlphaSorted: true, - isLeavesParentsSeparate: true, - onClick: () => { - if (!item.children) { - loadSubitems(item, isGithubTree); - } - }, - isSelected: () => { - return ( - activeTab && - activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && - /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. - NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. - */ - (activeTab as any).notebookPath() === item.path - ); - }, - contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item, isGithubTree) : undefined, - data: item, - children: buildChildNodes(item, onFileClick, isGithubTree), - }; - }; const buildDataTree = (): TreeNode => { const databaseTreeNodes: TreeNode[] = databases.map((database: ViewModels.Database) => { @@ -757,11 +368,6 @@ export const ResourceTree: React.FC = ({ container }: Resourc return traverse(schema); }; - const loadSubitems = async (item: NotebookContentItem, isGithubTree?: boolean): Promise => { - const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item); - updateNotebookItem(updatedItem, isGithubTree); - }; - const dataRootNode = buildDataTree(); const isSampleDataEnabled = useQueryCopilot().copilotEnabled && @@ -775,46 +381,16 @@ export const ResourceTree: React.FC = ({ container }: Resourc {!isNotebookEnabled && !isSampleDataEnabled && ( )} - {isNotebookEnabled && !isSampleDataEnabled && ( - <> - - - - - - - {/* {buildGalleryCallout()} */} - - )} {!isNotebookEnabled && isSampleDataEnabled && ( <> - + - - {/* {buildGalleryCallout()} */} - - )} - {isNotebookEnabled && isSampleDataEnabled && ( - <> - - - - - - - - - - - - - {/* {buildGalleryCallout()} */} )} diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index b22f0f916..67fb54773 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -1,42 +1,21 @@ -import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { getItemName } from "Utils/APITypeUtils"; import * as ko from "knockout"; import * as React from "react"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; -import GalleryIcon from "../../../images/GalleryIcon.svg"; -import DeleteIcon from "../../../images/delete.svg"; -import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; -import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; -import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; -import FileIcon from "../../../images/notebook/file-cosmos.svg"; -import PublishIcon from "../../../images/notebook/publish_content.svg"; -import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; -import { Areas } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; -import { IPinnedRepo } from "../../Juno/JunoClient"; -import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; -import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { isServerlessAccount } from "../../Utils/CapabilityUtils"; -import * as GitHubUtils from "../../Utils/GitHubUtils"; -import { useSidePanel } from "../../hooks/useSidePanel"; import { useTabs } from "../../hooks/useTabs"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; -import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; -import { useDialog } from "../Controls/Dialog"; -import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; +import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; -import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; -import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; -import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import TabsBase from "../Tabs/TabsBase"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; @@ -46,19 +25,8 @@ import Trigger from "./Trigger"; import UserDefinedFunction from "./UserDefinedFunction"; export class ResourceTreeAdapter implements ReactAdapter { - public static readonly MyNotebooksTitle = "My Notebooks"; - public static readonly GitHubReposTitle = "GitHub repos"; - - private static readonly DataTitle = "DATA"; - private static readonly NotebooksTitle = "NOTEBOOKS"; - private static readonly PseudoDirPath = "PsuedoDir"; - public parameters: ko.Observable; - public galleryContentRoot: NotebookContentItem; - public myNotebooksContentRoot: NotebookContentItem; - public gitHubNotebooksContentRoot: NotebookContentItem; - public constructor(private container: Explorer) { this.parameters = ko.observable(Date.now()); @@ -76,111 +44,9 @@ export class ResourceTreeAdapter implements ReactAdapter { this.triggerRender(); } - private traceMyNotebookTreeInfo() { - const myNotebooksTree = this.myNotebooksContentRoot; - if (myNotebooksTree.children) { - // Count 1st generation children (tree is lazy-loaded) - const nodeCounts = { files: 0, notebooks: 0, directories: 0 }; - myNotebooksTree.children.forEach((treeNode) => { - switch ((treeNode as NotebookContentItem).type) { - case NotebookContentItemType.File: - nodeCounts.files++; - break; - case NotebookContentItemType.Directory: - nodeCounts.directories++; - break; - case NotebookContentItemType.Notebook: - nodeCounts.notebooks++; - break; - default: - break; - } - }); - TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts }); - } - } - public renderComponent(): JSX.Element { const dataRootNode = this.buildDataTree(); - const notebooksRootNode = this.buildNotebooksTrees(); - - if (useNotebook.getState().isNotebookEnabled) { - return ( - <> - - - - - - - - - - {/* {this.galleryContentRoot && this.buildGalleryCallout()} */} - - ); - } else { - return ; - } - } - - public async initialize(): Promise { - const refreshTasks: Promise[] = []; - - this.galleryContentRoot = { - name: "Gallery", - path: "Gallery", - type: NotebookContentItemType.File, - }; - this.myNotebooksContentRoot = { - name: useNotebook.getState().notebookFolderName, - path: useNotebook.getState().notebookBasePath, - type: NotebookContentItemType.Directory, - }; - - // Only if notebook server is available we can refresh - if (useNotebook.getState().notebookServerInfo?.notebookServerEndpoint) { - refreshTasks.push( - this.container.refreshContentItem(this.myNotebooksContentRoot).then(() => { - this.triggerRender(); - this.traceMyNotebookTreeInfo(); - }), - ); - } - this.gitHubNotebooksContentRoot = { - name: ResourceTreeAdapter.GitHubReposTitle, - path: ResourceTreeAdapter.PseudoDirPath, - type: NotebookContentItemType.Directory, - }; - - return Promise.all(refreshTasks); - } - - public initializeGitHubRepos(pinnedRepos: IPinnedRepo[]): void { - if (this.gitHubNotebooksContentRoot) { - this.gitHubNotebooksContentRoot.children = []; - pinnedRepos?.forEach((pinnedRepo) => { - const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); - const repoTreeItem: NotebookContentItem = { - name: repoFullName, - path: ResourceTreeAdapter.PseudoDirPath, - type: NotebookContentItemType.Directory, - children: [], - }; - - pinnedRepo.branches.forEach((branch) => { - repoTreeItem.children.push({ - name: branch.name, - path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""), - type: NotebookContentItemType.Directory, - }); - }); - - this.gitHubNotebooksContentRoot.children.push(repoTreeItem); - }); - - this.triggerRender(); - } + return ; } private buildDataTree(): TreeNode { @@ -504,365 +370,6 @@ export class ResourceTreeAdapter implements ReactAdapter { return traverse(schema); } - private buildNotebooksTrees(): TreeNode { - let notebooksTree: TreeNode = { - label: undefined, - isExpanded: true, - children: [], - }; - - if (this.galleryContentRoot) { - notebooksTree.children.push(this.buildGalleryNotebooksTree()); - } - - if (this.myNotebooksContentRoot) { - notebooksTree.children.push(this.buildMyNotebooksTree()); - } - - if (this.gitHubNotebooksContentRoot) { - // collapse all other notebook nodes - notebooksTree.children.forEach((node) => (node.isExpanded = false)); - notebooksTree.children.push(this.buildGitHubNotebooksTree()); - } - - return notebooksTree; - } - - private buildGalleryCallout(): JSX.Element { - if ( - LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) && - LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed) - ) { - return undefined; - } - - const calloutProps: ICalloutProps = { - calloutMaxWidth: 350, - ariaLabel: "New gallery", - role: "alertdialog", - gapSpace: 0, - target: ".galleryHeader", - directionalHint: DirectionalHint.leftTopEdge, - onDismiss: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - this.triggerRender(); - }, - setInitialFocus: true, - }; - - const openGalleryProps: ILinkProps = { - onClick: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - this.container.openGallery(); - this.triggerRender(); - }, - }; - - return ( - - - - New gallery - - - Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other - contributors. - - Open gallery - - - ); - } - - private buildGalleryNotebooksTree(): TreeNode { - return { - label: "Gallery", - iconSrc: GalleryIcon, - className: "notebookHeader galleryHeader", - onClick: () => this.container.openGallery(), - isSelected: () => { - const activeTab = useTabs.getState().activeTab; - return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery; - }, - }; - } - - private buildMyNotebooksTree(): TreeNode { - const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( - this.myNotebooksContentRoot, - (item: NotebookContentItem) => { - this.container.openNotebook(item).then((hasOpened) => { - if (hasOpened) { - mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); - } - }); - }, - true, - true, - ); - - myNotebooksTree.isExpanded = true; - myNotebooksTree.isAlphaSorted = true; - // Remove "Delete" menu item from context menu - myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); - return myNotebooksTree; - } - - private buildGitHubNotebooksTree(): TreeNode { - const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( - this.gitHubNotebooksContentRoot, - (item: NotebookContentItem) => { - this.container.openNotebook(item).then((hasOpened) => { - if (hasOpened) { - mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); - } - }); - }, - true, - true, - ); - - gitHubNotebooksTree.contextMenu = [ - { - label: "Manage GitHub settings", - onClick: () => - useSidePanel - .getState() - .openSidePanel( - "Manage GitHub settings", - , - ), - }, - { - label: "Disconnect from GitHub", - onClick: () => { - TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { - dataExplorerArea: Areas.Notebook, - }); - this.container.notebookManager?.gitHubOAuthService.logout(); - }, - }, - ]; - - gitHubNotebooksTree.isExpanded = true; - gitHubNotebooksTree.isAlphaSorted = true; - - return gitHubNotebooksTree; - } - - private buildChildNodes( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - createDirectoryContextMenu: boolean, - createFileContextMenu: boolean, - ): TreeNode[] { - if (!item || !item.children) { - return []; - } else { - return item.children.map((item) => { - const result = - item.type === NotebookContentItemType.Directory - ? this.buildNotebookDirectoryNode(item, onFileClick, createDirectoryContextMenu, createFileContextMenu) - : this.buildNotebookFileNode(item, onFileClick, createFileContextMenu); - result.timestamp = item.timestamp; - return result; - }); - } - } - - private buildNotebookFileNode( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - createFileContextMenu: boolean, - ): TreeNode { - return { - label: item.name, - iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon, - className: "notebookHeader", - onClick: () => onFileClick(item), - isSelected: () => { - const activeTab = useTabs.getState().activeTab; - return ( - activeTab && - activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && - /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. - NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. - */ - (activeTab as any).notebookPath() === item.path - ); - }, - contextMenu: createFileContextMenu && this.createFileContextMenu(item), - data: item, - }; - } - - private createFileContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] { - let items: TreeNodeMenuItem[] = [ - { - label: "Rename", - iconSrc: NotebookIcon, - onClick: () => this.container.renameNotebook(item), - }, - { - label: "Delete", - iconSrc: DeleteIcon, - onClick: () => { - useDialog - .getState() - .showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}"`, - "Delete", - () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), - "Cancel", - undefined, - ); - }, - }, - { - label: "Copy to ...", - iconSrc: CopyIcon, - onClick: () => this.copyNotebook(item), - }, - { - label: "Download", - iconSrc: NotebookIcon, - onClick: () => this.container.downloadFile(item), - }, - ]; - - if (item.type === NotebookContentItemType.Notebook) { - items.push({ - label: "Publish to gallery", - iconSrc: PublishIcon, - onClick: async () => { - TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, { - source: Source.ResourceTreeMenu, - }); - - const content = await this.container.readFile(item); - if (content) { - await this.container.publishNotebook(item.name, content); - } - }, - }); - } - - // "Copy to ..." isn't needed if github locations are not available - if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - items = items.filter((item) => item.label !== "Copy to ..."); - } - - return items; - } - - private copyNotebook = async (item: NotebookContentItem) => { - const content = await this.container.readFile(item); - if (content) { - this.container.copyNotebook(item.name, content); - } - }; - - private createDirectoryContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] { - let items: TreeNodeMenuItem[] = [ - { - label: "Refresh", - iconSrc: RefreshIcon, - onClick: () => this.container.refreshContentItem(item).then(() => this.triggerRender()), - }, - { - label: "Delete", - iconSrc: DeleteIcon, - onClick: () => { - useDialog - .getState() - .showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}?"`, - "Delete", - () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), - "Cancel", - undefined, - ); - }, - }, - { - label: "Rename", - iconSrc: NotebookIcon, - onClick: () => this.container.renameNotebook(item), - }, - { - label: "New Directory", - iconSrc: NewNotebookIcon, - onClick: () => this.container.onCreateDirectory(item), - }, - { - label: "Upload File", - iconSrc: NewNotebookIcon, - onClick: () => this.container.openUploadFilePanel(item), - }, - ]; - - //disallow renaming of temporary notebook workspace - if (item?.path === useNotebook.getState().notebookBasePath) { - items = items.filter((item) => item.label !== "Rename"); - } - - // For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File" - if (GitHubUtils.fromContentUri(item.path)) { - items = items.filter( - (item) => - item.label !== "Delete" && - item.label !== "Rename" && - item.label !== "New Directory" && - item.label !== "Upload File", - ); - } - - return items; - } - - private buildNotebookDirectoryNode( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - createDirectoryContextMenu: boolean, - createFileContextMenu: boolean, - ): TreeNode { - return { - label: item.name, - iconSrc: undefined, - className: "notebookHeader", - isAlphaSorted: true, - isLeavesParentsSeparate: true, - onClick: () => { - if (!item.children) { - this.container.refreshContentItem(item).then(() => this.triggerRender()); - } - }, - isSelected: () => { - const activeTab = useTabs.getState().activeTab; - return ( - activeTab && - activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && - /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. - NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. - */ - (activeTab as any).notebookPath() === item.path - ); - }, - contextMenu: - createDirectoryContextMenu && item.path !== ResourceTreeAdapter.PseudoDirPath - ? this.createDirectoryContextMenu(item) - : undefined, - data: item, - children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu), - }; - } - public triggerRender() { window.requestAnimationFrame(() => this.parameters(Date.now())); } diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index f11b6df5c..1314b2b3c 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -245,7 +245,6 @@ export function downloadItem( }, "Cancel", undefined, - container.getDownloadModalConent(name), ); } export async function downloadNotebookItem( @@ -278,7 +277,6 @@ export async function downloadNotebookItem( metadata.untrusted = true; } - await container.importAndOpenContent(data.name, JSON.stringify(notebook)); logConsoleInfo(`Successfully downloaded ${data.name} to ${useNotebook.getState().notebookFolderName}`); const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);