This reverts commit b023250e67
.
This commit is contained in:
parent
81a5b7cb6d
commit
298197b1b8
|
@ -68,6 +68,10 @@ export interface OpenPane extends DataExplorerAction {
|
||||||
paneKind: PaneKind | string;
|
paneKind: PaneKind | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OpenSampleNotebook extends DataExplorerAction {
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The types of actions that the DataExplorer supports performing upon opening.
|
* The types of actions that the DataExplorer supports performing upon opening.
|
||||||
*/
|
*/
|
||||||
|
@ -76,4 +80,5 @@ export enum ActionType {
|
||||||
OpenCollectionTab,
|
OpenCollectionTab,
|
||||||
OpenPane,
|
OpenPane,
|
||||||
TransmitCachedData,
|
TransmitCachedData,
|
||||||
|
OpenSampleNotebook,
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||||
"computedProperties": [Function],
|
"computedProperties": [Function],
|
||||||
"conflictResolutionPolicy": [Function],
|
"conflictResolutionPolicy": [Function],
|
||||||
"container": Explorer {
|
"container": Explorer {
|
||||||
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
@ -45,8 +47,10 @@ exports[`SettingsComponent renders 1`] = `
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
},
|
},
|
||||||
|
"refreshNotebookList": [Function],
|
||||||
"resourceTree": ResourceTreeAdapter {
|
"resourceTree": ResourceTreeAdapter {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
|
"copyNotebook": [Function],
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -103,6 +107,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||||
"computedProperties": [Function],
|
"computedProperties": [Function],
|
||||||
"conflictResolutionPolicy": [Function],
|
"conflictResolutionPolicy": [Function],
|
||||||
"container": Explorer {
|
"container": Explorer {
|
||||||
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
@ -119,8 +125,10 @@ exports[`SettingsComponent renders 1`] = `
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
},
|
},
|
||||||
|
"refreshNotebookList": [Function],
|
||||||
"resourceTree": ResourceTreeAdapter {
|
"resourceTree": ResourceTreeAdapter {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
|
"copyNotebook": [Function],
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -216,6 +224,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||||
"computedProperties": [Function],
|
"computedProperties": [Function],
|
||||||
"conflictResolutionPolicy": [Function],
|
"conflictResolutionPolicy": [Function],
|
||||||
"container": Explorer {
|
"container": Explorer {
|
||||||
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
@ -232,8 +242,10 @@ exports[`SettingsComponent renders 1`] = `
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
},
|
},
|
||||||
|
"refreshNotebookList": [Function],
|
||||||
"resourceTree": ResourceTreeAdapter {
|
"resourceTree": ResourceTreeAdapter {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
|
"copyNotebook": [Function],
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -259,6 +271,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||||
}
|
}
|
||||||
explorer={
|
explorer={
|
||||||
Explorer {
|
Explorer {
|
||||||
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
@ -275,8 +289,10 @@ exports[`SettingsComponent renders 1`] = `
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
},
|
},
|
||||||
|
"refreshNotebookList": [Function],
|
||||||
"resourceTree": ResourceTreeAdapter {
|
"resourceTree": ResourceTreeAdapter {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
|
"copyNotebook": [Function],
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Link } from "@fluentui/react/lib/Link";
|
||||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
|
@ -15,7 +16,7 @@ import shallow from "zustand/shallow";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import { Areas, ConnectionStatusType, HttpStatusCodes, PoolIdType } from "../Common/Constants";
|
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook, PoolIdType } from "../Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { QueriesClient } from "../Common/QueriesClient";
|
import { QueriesClient } from "../Common/QueriesClient";
|
||||||
|
@ -31,23 +32,34 @@ import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"
|
||||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { isAccountNewerThanThresholdInMs, userContext } from "../UserContext";
|
import { isAccountNewerThanThresholdInMs, userContext } from "../UserContext";
|
||||||
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||||
|
import { stringToBlob } from "../Utils/BlobUtils";
|
||||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
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 { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||||
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
|
import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||||
import { useSidePanel } from "../hooks/useSidePanel";
|
import { useSidePanel } from "../hooks/useSidePanel";
|
||||||
import { useTabs } from "../hooks/useTabs";
|
import { useTabs } from "../hooks/useTabs";
|
||||||
import "./ComponentRegisterer";
|
import "./ComponentRegisterer";
|
||||||
import { DialogProps, useDialog } from "./Controls/Dialog";
|
import { DialogProps, useDialog } from "./Controls/Dialog";
|
||||||
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
||||||
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { NotebookContentItem } from "./Notebook/NotebookContentItem";
|
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
||||||
|
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
|
||||||
|
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
||||||
import type NotebookManager from "./Notebook/NotebookManager";
|
import type NotebookManager from "./Notebook/NotebookManager";
|
||||||
|
import { NotebookPaneContent } from "./Notebook/NotebookManager";
|
||||||
|
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||||
import { useNotebook } from "./Notebook/useNotebook";
|
import { useNotebook } from "./Notebook/useNotebook";
|
||||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
||||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
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 { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
|
||||||
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
||||||
|
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
|
||||||
import TabsBase from "./Tabs/TabsBase";
|
import TabsBase from "./Tabs/TabsBase";
|
||||||
import TerminalTab from "./Tabs/TerminalTab";
|
import TerminalTab from "./Tabs/TerminalTab";
|
||||||
import Database from "./Tree/Database";
|
import Database from "./Tree/Database";
|
||||||
|
@ -75,6 +87,7 @@ export default class Explorer {
|
||||||
// Notebooks
|
// Notebooks
|
||||||
public notebookManager?: NotebookManager;
|
public notebookManager?: NotebookManager;
|
||||||
|
|
||||||
|
private _isInitializingNotebooks: boolean;
|
||||||
private notebookToImport: {
|
private notebookToImport: {
|
||||||
name: string;
|
name: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
@ -86,6 +99,7 @@ export default class Explorer {
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||||
});
|
});
|
||||||
|
this._isInitializingNotebooks = false;
|
||||||
|
|
||||||
this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
|
this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
|
||||||
useNotebook.subscribe(
|
useNotebook.subscribe(
|
||||||
|
@ -191,10 +205,12 @@ export default class Explorer {
|
||||||
container: this,
|
container: this,
|
||||||
resourceTree: this.resourceTree,
|
resourceTree: this.resourceTree,
|
||||||
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
|
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
|
||||||
|
refreshNotebookList: () => this.refreshNotebookList(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.refreshCommandBarButtons();
|
this.refreshCommandBarButtons();
|
||||||
|
this.refreshNotebookList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public openEnableSynapseLinkDialog(): void {
|
public openEnableSynapseLinkDialog(): void {
|
||||||
|
@ -357,6 +373,7 @@ export default class Explorer {
|
||||||
userContext.authType === AuthType.ResourceToken
|
userContext.authType === AuthType.ResourceToken
|
||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
: this.refreshAllDatabases();
|
: this.refreshAllDatabases();
|
||||||
|
this.refreshNotebookList();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Facade
|
// Facade
|
||||||
|
@ -364,6 +381,19 @@ export default class Explorer {
|
||||||
window.open(Constants.Urls.feedbackEmail, "_blank");
|
window.open(Constants.Urls.feedbackEmail, "_blank");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise<void> {
|
||||||
|
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<void> {
|
public async allocateContainer(poolId: PoolIdType, mode?: string): Promise<void> {
|
||||||
const shouldUseNotebookStates = poolId === PoolIdType.DefaultPoolId ? true : false;
|
const shouldUseNotebookStates = poolId === PoolIdType.DefaultPoolId ? true : false;
|
||||||
const notebookServerInfo = shouldUseNotebookStates
|
const notebookServerInfo = shouldUseNotebookStates
|
||||||
|
@ -442,6 +472,8 @@ export default class Explorer {
|
||||||
? useNotebook.getState().setIsAllocating(false)
|
? useNotebook.getState().setIsAllocating(false)
|
||||||
: useQueryCopilot.getState().setIsAllocatingContainer(false);
|
: useQueryCopilot.getState().setIsAllocatingContainer(false);
|
||||||
this.refreshCommandBarButtons();
|
this.refreshCommandBarButtons();
|
||||||
|
this.refreshNotebookList();
|
||||||
|
this._isInitializingNotebooks = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -478,6 +510,104 @@ export default class Explorer {
|
||||||
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
|
.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<boolean> {
|
||||||
|
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(
|
private getDeltaDatabases(
|
||||||
updatedDatabaseList: DataModels.Database[],
|
updatedDatabaseList: DataModels.Database[],
|
||||||
databases: ViewModels.Database[],
|
databases: ViewModels.Database[],
|
||||||
|
@ -566,6 +696,406 @@ export default class Explorer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public uploadFile(
|
||||||
|
name: string,
|
||||||
|
content: string,
|
||||||
|
parent: NotebookContentItem,
|
||||||
|
isGithubTree?: boolean,
|
||||||
|
): Promise<NotebookContentItem> {
|
||||||
|
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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<void> {
|
||||||
|
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<boolean> {
|
||||||
|
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",
|
||||||
|
<StringInputPane
|
||||||
|
closePanel={() => {
|
||||||
|
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<NotebookContentItem> =>
|
||||||
|
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",
|
||||||
|
<StringInputPane
|
||||||
|
closePanel={() => {
|
||||||
|
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<NotebookContentItem> =>
|
||||||
|
this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input, isGithubTree)
|
||||||
|
}
|
||||||
|
notebookFile={parent}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readFile(notebookFile: NotebookContentItem): Promise<string> {
|
||||||
|
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<void> {
|
||||||
|
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<void> => {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<p>{Notebook.newNotebookModalContent1}</p>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
{Notebook.newNotebookModalContent2}
|
||||||
|
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
|
||||||
|
{Notebook.learnMore}
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
// TODO: Delete this function when ResourceTreeAdapter is removed.
|
||||||
public async refreshContentItem(item: NotebookContentItem): Promise<void> {
|
public async refreshContentItem(item: NotebookContentItem): Promise<void> {
|
||||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
||||||
|
@ -722,6 +1252,32 @@ export default class Explorer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async handleOpenFileAction(path: string): Promise<void> {
|
||||||
|
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 {
|
public openUploadItemsPanePane(): void {
|
||||||
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
|
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
|
||||||
}
|
}
|
||||||
|
@ -731,6 +1287,54 @@ export default class Explorer {
|
||||||
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
|
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
<UploadFilePane uploadFile={(name: string, content: string) => this.uploadFile(name, content, parent)} />,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDownloadModalConent(fileName: string): JSX.Element {
|
||||||
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>{Notebook.galleryNotebookDownloadContent1}</p>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
{Notebook.galleryNotebookDownloadContent2}
|
||||||
|
<Link href={Notebook.cosmosNotebookGitDocumentationUrl} target="_blank">
|
||||||
|
{Notebook.learnMore}
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <p> Download {fileName} from gallery as a copy to your notebooks to run and/or edit the notebook. </p>;
|
||||||
|
}
|
||||||
|
|
||||||
public async refreshExplorer(): Promise<void> {
|
public async refreshExplorer(): Promise<void> {
|
||||||
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
||||||
userContext.authType === AuthType.ResourceToken
|
userContext.authType === AuthType.ResourceToken
|
||||||
|
@ -755,6 +1359,10 @@ export default class Explorer {
|
||||||
dataExplorerArea: Constants.Areas.Notebook,
|
dataExplorerArea: Constants.Areas.Notebook,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
await this.initNotebooks(userContext.databaseAccount);
|
||||||
|
}
|
||||||
|
|
||||||
await this.refreshSampleData();
|
await this.refreshSampleData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,13 +12,15 @@ import * as Logger from "../../Common/Logger";
|
||||||
import { GitHubClient } from "../../GitHub/GitHubClient";
|
import { GitHubClient } from "../../GitHub/GitHubClient";
|
||||||
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
|
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
|
||||||
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
|
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
|
||||||
|
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||||
import { JunoClient } from "../../Juno/JunoClient";
|
import { JunoClient } from "../../Juno/JunoClient";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
import { getFullName } from "../../Utils/UserUtils";
|
import { getFullName } from "../../Utils/UserUtils";
|
||||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
|
||||||
import { useDialog } from "../Controls/Dialog";
|
import { useDialog } from "../Controls/Dialog";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
|
||||||
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||||
import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane";
|
import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane";
|
||||||
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||||
|
@ -38,6 +40,7 @@ export interface NotebookManagerOptions {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
resourceTree: ResourceTreeAdapter;
|
resourceTree: ResourceTreeAdapter;
|
||||||
refreshCommandBarButtons: () => void;
|
refreshCommandBarButtons: () => void;
|
||||||
|
refreshNotebookList: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class NotebookManager {
|
export default class NotebookManager {
|
||||||
|
@ -78,6 +81,10 @@ export default class NotebookManager {
|
||||||
contents.JupyterContentProvider,
|
contents.JupyterContentProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.notebookClient = new NotebookContainerClient(() =>
|
||||||
|
this.params.container.initNotebooks(userContext?.databaseAccount),
|
||||||
|
);
|
||||||
|
|
||||||
this.notebookContentClient = new NotebookContentClient(this.notebookContentProvider);
|
this.notebookContentClient = new NotebookContentClient(this.notebookContentProvider);
|
||||||
|
|
||||||
this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
|
this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
|
||||||
|
@ -99,9 +106,11 @@ export default class NotebookManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.params.refreshCommandBarButtons();
|
this.params.refreshCommandBarButtons();
|
||||||
|
this.params.refreshNotebookList();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.junoClient.subscribeToPinnedRepos((pinnedRepos) => {
|
this.junoClient.subscribeToPinnedRepos((pinnedRepos) => {
|
||||||
|
this.params.resourceTree.initializeGitHubRepos(pinnedRepos);
|
||||||
this.params.resourceTree.triggerRender();
|
this.params.resourceTree.triggerRender();
|
||||||
useNotebook.getState().initializeGitHubRepos(pinnedRepos);
|
useNotebook.getState().initializeGitHubRepos(pinnedRepos);
|
||||||
});
|
});
|
||||||
|
@ -140,6 +149,22 @@ export default class NotebookManager {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public openCopyNotebookPane(name: string, content: string): void {
|
||||||
|
const { container } = this.params;
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
"Copy Notebook",
|
||||||
|
<CopyNotebookPane
|
||||||
|
container={container}
|
||||||
|
junoClient={this.junoClient}
|
||||||
|
gitHubOAuthService={this.gitHubOAuthService}
|
||||||
|
name={name}
|
||||||
|
content={content}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Octokit's error handler uses any
|
// Octokit's error handler uses any
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
private onGitHubClientError = (error: any): void => {
|
private onGitHubClientError = (error: any): void => {
|
||||||
|
|
|
@ -195,5 +195,17 @@ export function handleOpenAction(
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openFile(action: ActionContracts.OpenSampleNotebook, explorer: Explorer) {
|
||||||
|
explorer.handleOpenFileAction(decodeURIComponent(action.path));
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
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<CopyNotebookPanelProps> = ({
|
||||||
|
name,
|
||||||
|
content,
|
||||||
|
container,
|
||||||
|
junoClient,
|
||||||
|
gitHubOAuthService,
|
||||||
|
}: CopyNotebookPanelProps) => {
|
||||||
|
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||||
|
const [isExecuting, setIsExecuting] = useState<boolean>();
|
||||||
|
const [formError, setFormError] = useState<string>("");
|
||||||
|
const [pinnedRepos, setPinnedRepos] = useState<IPinnedRepo[]>();
|
||||||
|
const [selectedLocation, setSelectedLocation] = useState<Location>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
open();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const open = async (): Promise<void> => {
|
||||||
|
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<void> => {
|
||||||
|
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<NotebookContentItem> => {
|
||||||
|
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<HTMLDivElement>, option?: IDropdownOption): void => {
|
||||||
|
setSelectedLocation(option?.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const props: RightPaneFormProps = {
|
||||||
|
formError,
|
||||||
|
isExecuting: isExecuting,
|
||||||
|
submitButtonText: "OK",
|
||||||
|
onSubmit: () => submit(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyNotebookPaneProps: CopyNotebookPaneProps = {
|
||||||
|
name,
|
||||||
|
pinnedRepos,
|
||||||
|
onDropDownChange: onDropDownChange,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RightPaneForm {...props}>
|
||||||
|
<CopyNotebookPaneComponent {...copyNotebookPaneProps} />
|
||||||
|
</RightPaneForm>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,120 @@
|
||||||
|
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<HTMLDivElement>, option?: IDropdownOption) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps> = ({
|
||||||
|
name,
|
||||||
|
pinnedRepos,
|
||||||
|
onDropDownChange,
|
||||||
|
}: CopyNotebookPaneProps) => {
|
||||||
|
const BranchNameWhiteSpace = " ";
|
||||||
|
|
||||||
|
const onRenderDropDownTitle: IRenderFunction<IDropdownOption[]> = (options: IDropdownOption[]): JSX.Element => {
|
||||||
|
return <span>{options.length && options[0].title}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRenderDropDownOption: IRenderFunction<ISelectableOption> = (option: ISelectableOption): JSX.Element => {
|
||||||
|
return <span style={{ whiteSpace: "pre-wrap" }}>{option.text}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="paneMainContent">
|
||||||
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
|
<Stack.Item>
|
||||||
|
<Label htmlFor="notebookName">Name</Label>
|
||||||
|
<Text id="notebookName">{name}</Text>
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Dropdown {...dropDownProps} />
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -17,6 +17,8 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
||||||
addRepoProps={
|
addRepoProps={
|
||||||
Object {
|
Object {
|
||||||
"container": Explorer {
|
"container": Explorer {
|
||||||
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
@ -33,8 +35,10 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
},
|
},
|
||||||
|
"refreshNotebookList": [Function],
|
||||||
"resourceTree": ResourceTreeAdapter {
|
"resourceTree": ResourceTreeAdapter {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
|
"copyNotebook": [Function],
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,6 +7,8 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||||
errorMessage="Could not create directory "
|
errorMessage="Could not create directory "
|
||||||
explorer={
|
explorer={
|
||||||
Explorer {
|
Explorer {
|
||||||
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
@ -23,8 +25,10 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
},
|
},
|
||||||
|
"refreshNotebookList": [Function],
|
||||||
"resourceTree": ResourceTreeAdapter {
|
"resourceTree": ResourceTreeAdapter {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
|
"copyNotebook": [Function],
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,8 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
||||||
databaseId="CopilotSampleDb"
|
databaseId="CopilotSampleDb"
|
||||||
explorer={
|
explorer={
|
||||||
Explorer {
|
Explorer {
|
||||||
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
@ -38,8 +40,10 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
},
|
},
|
||||||
|
"refreshNotebookList": [Function],
|
||||||
"resourceTree": ResourceTreeAdapter {
|
"resourceTree": ResourceTreeAdapter {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
|
"copyNotebook": [Function],
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,9 +25,11 @@ import * as React from "react";
|
||||||
import ConnectIcon from "../../../images/Connect_color.svg";
|
import ConnectIcon from "../../../images/Connect_color.svg";
|
||||||
import ContainersIcon from "../../../images/Containers.svg";
|
import ContainersIcon from "../../../images/Containers.svg";
|
||||||
import LinkIcon from "../../../images/Link_blue.svg";
|
import LinkIcon from "../../../images/Link_blue.svg";
|
||||||
|
import NotebookColorIcon from "../../../images/Notebooks.svg";
|
||||||
import PowerShellIcon from "../../../images/PowerShell.svg";
|
import PowerShellIcon from "../../../images/PowerShell.svg";
|
||||||
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
||||||
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
|
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
|
||||||
|
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
|
||||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
@ -408,6 +410,14 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
heroes.push(launchQuickstartBtn);
|
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());
|
heroes.push(this.getShellCard());
|
||||||
|
@ -483,12 +493,28 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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[] {
|
private createRecentItems(): SplashScreenItem[] {
|
||||||
return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((activity) => {
|
return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((activity) => {
|
||||||
switch (activity.type) {
|
switch (activity.type) {
|
||||||
default: {
|
default: {
|
||||||
throw new Error(`Unknown activity: ${activity}`);
|
const unknownActivity: never = activity;
|
||||||
|
throw new Error(`Unknown activity: ${unknownActivity}`);
|
||||||
}
|
}
|
||||||
|
case MostRecentActivity.Type.OpenNotebook:
|
||||||
|
return this.decorateOpenNotebookActivity(activity);
|
||||||
|
|
||||||
case MostRecentActivity.Type.OpenCollection:
|
case MostRecentActivity.Type.OpenCollection:
|
||||||
return this.decorateOpenCollectionActivity(activity);
|
return this.decorateOpenCollectionActivity(activity);
|
||||||
|
|
|
@ -1,11 +1,31 @@
|
||||||
|
import { stringifyNotebook, toJS } from "@nteract/commutable";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as Q from "q";
|
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 * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils";
|
||||||
import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { useDialog } from "../Controls/Dialog";
|
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 { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
|
||||||
|
import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types";
|
||||||
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
||||||
|
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
||||||
import { useNotebook } from "../Notebook/useNotebook";
|
import { useNotebook } from "../Notebook/useNotebook";
|
||||||
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
|
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
|
||||||
|
|
||||||
|
@ -70,7 +90,275 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||||
return [];
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected buildCommandBarOptions(): void {
|
protected buildCommandBarOptions(): void {
|
||||||
|
@ -94,4 +382,50 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
||||||
sparkClusterConnectionInfo,
|
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);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,42 @@
|
||||||
|
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
||||||
import { SampleDataTree } from "Explorer/Tree/SampleDataTree";
|
import { SampleDataTree } from "Explorer/Tree/SampleDataTree";
|
||||||
import { getItemName } from "Utils/APITypeUtils";
|
import { getItemName } from "Utils/APITypeUtils";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import shallow from "zustand/shallow";
|
import shallow from "zustand/shallow";
|
||||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
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 CollectionIcon from "../../../images/tree-collection.svg";
|
||||||
|
import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants";
|
||||||
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
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 { userContext } from "../../UserContext";
|
||||||
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
|
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
|
||||||
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
|
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
||||||
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
|
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
|
||||||
import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent";
|
import { useDialog } from "../Controls/Dialog";
|
||||||
|
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
||||||
|
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
||||||
|
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
||||||
import { useNotebook } from "../Notebook/useNotebook";
|
import { useNotebook } from "../Notebook/useNotebook";
|
||||||
|
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||||
import TabsBase from "../Tabs/TabsBase";
|
import TabsBase from "../Tabs/TabsBase";
|
||||||
import { useDatabases } from "../useDatabases";
|
import { useDatabases } from "../useDatabases";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
|
@ -26,21 +45,391 @@ import StoredProcedure from "./StoredProcedure";
|
||||||
import Trigger from "./Trigger";
|
import Trigger from "./Trigger";
|
||||||
import UserDefinedFunction from "./UserDefinedFunction";
|
import UserDefinedFunction from "./UserDefinedFunction";
|
||||||
|
|
||||||
|
export const MyNotebooksTitle = "My Notebooks";
|
||||||
|
export const GitHubReposTitle = "GitHub repos";
|
||||||
|
|
||||||
interface ResourceTreeProps {
|
interface ResourceTreeProps {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: ResourceTreeProps): JSX.Element => {
|
export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: ResourceTreeProps): JSX.Element => {
|
||||||
const databases = useDatabases((state) => state.databases);
|
const databases = useDatabases((state) => state.databases);
|
||||||
const { isNotebookEnabled } = useNotebook(
|
const {
|
||||||
|
isNotebookEnabled,
|
||||||
|
myNotebooksContentRoot,
|
||||||
|
galleryContentRoot,
|
||||||
|
gitHubNotebooksContentRoot,
|
||||||
|
updateNotebookItem,
|
||||||
|
} = useNotebook(
|
||||||
(state) => ({
|
(state) => ({
|
||||||
isNotebookEnabled: state.isNotebookEnabled,
|
isNotebookEnabled: state.isNotebookEnabled,
|
||||||
|
myNotebooksContentRoot: state.myNotebooksContentRoot,
|
||||||
|
galleryContentRoot: state.galleryContentRoot,
|
||||||
|
gitHubNotebooksContentRoot: state.gitHubNotebooksContentRoot,
|
||||||
|
updateNotebookItem: state.updateNotebookItem,
|
||||||
}),
|
}),
|
||||||
shallow,
|
shallow,
|
||||||
);
|
);
|
||||||
const { refreshActiveTab } = useTabs();
|
const { activeTab, refreshActiveTab } = useTabs();
|
||||||
const showScriptNodes =
|
const showScriptNodes =
|
||||||
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
|
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 (
|
||||||
|
<Callout {...calloutProps}>
|
||||||
|
<Stack tokens={{ childrenGap: 10, padding: 20 }}>
|
||||||
|
<Text variant="xLarge" block>
|
||||||
|
New gallery
|
||||||
|
</Text>
|
||||||
|
<Text block>
|
||||||
|
Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other
|
||||||
|
contributors.
|
||||||
|
</Text>
|
||||||
|
<Link {...openGalleryProps}>Open gallery</Link>
|
||||||
|
</Stack>
|
||||||
|
</Callout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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",
|
||||||
|
<GitHubReposPanel
|
||||||
|
explorer={container}
|
||||||
|
gitHubClientProp={container.notebookManager.gitHubClient}
|
||||||
|
junoClientProp={container.notebookManager.junoClient}
|
||||||
|
/>,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 buildDataTree = (): TreeNode => {
|
||||||
const databaseTreeNodes: TreeNode[] = databases.map((database: ViewModels.Database) => {
|
const databaseTreeNodes: TreeNode[] = databases.map((database: ViewModels.Database) => {
|
||||||
|
@ -368,6 +757,11 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||||
return traverse(schema);
|
return traverse(schema);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadSubitems = async (item: NotebookContentItem, isGithubTree?: boolean): Promise<void> => {
|
||||||
|
const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item);
|
||||||
|
updateNotebookItem(updatedItem, isGithubTree);
|
||||||
|
};
|
||||||
|
|
||||||
const dataRootNode = buildDataTree();
|
const dataRootNode = buildDataTree();
|
||||||
const isSampleDataEnabled =
|
const isSampleDataEnabled =
|
||||||
useQueryCopilot().copilotEnabled &&
|
useQueryCopilot().copilotEnabled &&
|
||||||
|
@ -381,16 +775,46 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||||
{!isNotebookEnabled && !isSampleDataEnabled && (
|
{!isNotebookEnabled && !isSampleDataEnabled && (
|
||||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||||
)}
|
)}
|
||||||
|
{isNotebookEnabled && !isSampleDataEnabled && (
|
||||||
|
<>
|
||||||
|
<AccordionComponent>
|
||||||
|
<AccordionItemComponent title={"DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
||||||
|
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||||
|
</AccordionItemComponent>
|
||||||
|
</AccordionComponent>
|
||||||
|
|
||||||
|
{/* {buildGalleryCallout()} */}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{!isNotebookEnabled && isSampleDataEnabled && (
|
{!isNotebookEnabled && isSampleDataEnabled && (
|
||||||
<>
|
<>
|
||||||
<AccordionComponent>
|
<AccordionComponent>
|
||||||
<AccordionItemComponent title={"MY DATA"} isExpanded={true}>
|
<AccordionItemComponent title={"MY DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
||||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||||
</AccordionItemComponent>
|
</AccordionItemComponent>
|
||||||
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
|
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
|
||||||
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
||||||
</AccordionItemComponent>
|
</AccordionItemComponent>
|
||||||
</AccordionComponent>
|
</AccordionComponent>
|
||||||
|
|
||||||
|
{/* {buildGalleryCallout()} */}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isNotebookEnabled && isSampleDataEnabled && (
|
||||||
|
<>
|
||||||
|
<AccordionComponent>
|
||||||
|
<AccordionItemComponent title={"MY DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
||||||
|
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||||
|
</AccordionItemComponent>
|
||||||
|
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
|
||||||
|
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
||||||
|
</AccordionItemComponent>
|
||||||
|
<AccordionItemComponent title={"NOTEBOOKS"}>
|
||||||
|
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
|
||||||
|
</AccordionItemComponent>
|
||||||
|
</AccordionComponent>
|
||||||
|
|
||||||
|
{/* {buildGalleryCallout()} */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,21 +1,42 @@
|
||||||
|
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
||||||
import { getItemName } from "Utils/APITypeUtils";
|
import { getItemName } from "Utils/APITypeUtils";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
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 CollectionIcon from "../../../images/tree-collection.svg";
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
|
import { Areas } from "../../Common/Constants";
|
||||||
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
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 { userContext } from "../../UserContext";
|
||||||
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
|
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
|
||||||
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
|
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
||||||
import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent";
|
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
|
||||||
|
import { useDialog } from "../Controls/Dialog";
|
||||||
|
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
||||||
|
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
||||||
|
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
||||||
import { useNotebook } from "../Notebook/useNotebook";
|
import { useNotebook } from "../Notebook/useNotebook";
|
||||||
|
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||||
import TabsBase from "../Tabs/TabsBase";
|
import TabsBase from "../Tabs/TabsBase";
|
||||||
import { useDatabases } from "../useDatabases";
|
import { useDatabases } from "../useDatabases";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
|
@ -25,8 +46,19 @@ import Trigger from "./Trigger";
|
||||||
import UserDefinedFunction from "./UserDefinedFunction";
|
import UserDefinedFunction from "./UserDefinedFunction";
|
||||||
|
|
||||||
export class ResourceTreeAdapter implements ReactAdapter {
|
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<number>;
|
public parameters: ko.Observable<number>;
|
||||||
|
|
||||||
|
public galleryContentRoot: NotebookContentItem;
|
||||||
|
public myNotebooksContentRoot: NotebookContentItem;
|
||||||
|
public gitHubNotebooksContentRoot: NotebookContentItem;
|
||||||
|
|
||||||
public constructor(private container: Explorer) {
|
public constructor(private container: Explorer) {
|
||||||
this.parameters = ko.observable(Date.now());
|
this.parameters = ko.observable(Date.now());
|
||||||
|
|
||||||
|
@ -44,10 +76,112 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||||
this.triggerRender();
|
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 {
|
public renderComponent(): JSX.Element {
|
||||||
const dataRootNode = this.buildDataTree();
|
const dataRootNode = this.buildDataTree();
|
||||||
|
const notebooksRootNode = this.buildNotebooksTrees();
|
||||||
|
|
||||||
|
if (useNotebook.getState().isNotebookEnabled) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AccordionComponent>
|
||||||
|
<AccordionItemComponent title={ResourceTreeAdapter.DataTitle} isExpanded={!this.gitHubNotebooksContentRoot}>
|
||||||
|
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||||
|
</AccordionItemComponent>
|
||||||
|
<AccordionItemComponent title={ResourceTreeAdapter.NotebooksTitle}>
|
||||||
|
<TreeComponent className="notebookResourceTree" rootNode={notebooksRootNode} />
|
||||||
|
</AccordionItemComponent>
|
||||||
|
</AccordionComponent>
|
||||||
|
|
||||||
|
{/* {this.galleryContentRoot && this.buildGalleryCallout()} */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
|
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async initialize(): Promise<void[]> {
|
||||||
|
const refreshTasks: Promise<void>[] = [];
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private buildDataTree(): TreeNode {
|
private buildDataTree(): TreeNode {
|
||||||
const databaseTreeNodes: TreeNode[] = useDatabases.getState().databases.map((database: ViewModels.Database) => {
|
const databaseTreeNodes: TreeNode[] = useDatabases.getState().databases.map((database: ViewModels.Database) => {
|
||||||
|
@ -370,6 +504,365 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||||
return traverse(schema);
|
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 (
|
||||||
|
<Callout {...calloutProps}>
|
||||||
|
<Stack tokens={{ childrenGap: 10, padding: 20 }}>
|
||||||
|
<Text variant="xLarge" block>
|
||||||
|
New gallery
|
||||||
|
</Text>
|
||||||
|
<Text block>
|
||||||
|
Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other
|
||||||
|
contributors.
|
||||||
|
</Text>
|
||||||
|
<Link {...openGalleryProps}>Open gallery</Link>
|
||||||
|
</Stack>
|
||||||
|
</Callout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
<GitHubReposPanel
|
||||||
|
explorer={this.container}
|
||||||
|
gitHubClientProp={this.container.notebookManager.gitHubClient}
|
||||||
|
junoClientProp={this.container.notebookManager.junoClient}
|
||||||
|
/>,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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() {
|
public triggerRender() {
|
||||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -245,6 +245,7 @@ export function downloadItem(
|
||||||
},
|
},
|
||||||
"Cancel",
|
"Cancel",
|
||||||
undefined,
|
undefined,
|
||||||
|
container.getDownloadModalConent(name),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export async function downloadNotebookItem(
|
export async function downloadNotebookItem(
|
||||||
|
@ -277,6 +278,7 @@ export async function downloadNotebookItem(
|
||||||
metadata.untrusted = true;
|
metadata.untrusted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await container.importAndOpenContent(data.name, JSON.stringify(notebook));
|
||||||
logConsoleInfo(`Successfully downloaded ${data.name} to ${useNotebook.getState().notebookFolderName}`);
|
logConsoleInfo(`Successfully downloaded ${data.name} to ${useNotebook.getState().notebookFolderName}`);
|
||||||
|
|
||||||
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
|
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
|
||||||
|
|
Loading…
Reference in New Issue