cosmos-explorer/src/Explorer/Explorer.tsx

1175 lines
43 KiB
TypeScript
Raw Normal View History

import { IChoiceGroupProps } from "@fluentui/react";
import * as ko from "knockout";
2021-03-11 05:02:55 +00:00
import React from "react";
2021-01-20 15:15:01 +00:00
import _ from "underscore";
2021-03-11 05:02:55 +00:00
import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants";
2021-01-20 15:15:01 +00:00
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility";
2021-03-11 05:02:55 +00:00
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
import * as Logger from "../Common/Logger";
import { QueriesClient } from "../Common/QueriesClient";
import { configContext } from "../ConfigContext";
2021-03-11 05:02:55 +00:00
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
import { useSidePanel } from "../hooks/useSidePanel";
2021-07-09 05:32:22 +01:00
import { useTabs } from "../hooks/useTabs";
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
2021-03-11 05:02:55 +00:00
import { ExplorerSettings } from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
2021-05-25 20:46:52 +01:00
import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName, getUploadName } from "../Utils/APITypeUtils";
2021-05-25 20:46:52 +01:00
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import {
get as getWorkspace,
listByDatabaseAccount,
listConnectionInfo,
start,
} from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
2021-03-11 05:02:55 +00:00
import { stringToBlob } from "../Utils/BlobUtils";
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
2021-03-11 05:02:55 +00:00
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
2021-03-11 05:02:55 +00:00
import * as ComponentRegisterer from "./ComponentRegisterer";
2021-05-20 15:54:26 +01:00
import { DialogProps, TextFieldProps, useDialog } from "./Controls/Dialog";
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
2021-05-28 21:20:59 +01:00
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
Fix bug publish screenshot (#762) [Preview this branch](https://cosmos-explorer-preview.azurewebsites.net/pull/762?feature.someFeatureFlagYouMightNeed=true) The main change in this PR fixes the snapshot functionality in the Publish pane-related components. Because the code cell outputs are now rendered in their own iframes for security reasons, a single snapshot of the notebook is no longer possible: each cell output takes its own snapshot and the snapshots are collated on the main notebook snapshot. - Move the snapshot functionality to notebook components: this removes the reference of the notebook DOM node that we must pass to the Publish pane via explorer. - Add slice in the state and actions in notebook redux for notebook snapshot requests and result - Add post robot message to take snapshots and receive results - Add logic in `NotebookRenderer` to wait for all output snapshots done before taking the main one collating. - Use `zustand` to share snapshot between Redux world and React world. This solves the issue of keeping the `PanelContainer` component generic, while being able to update its children (`PublishPanel` component) with the new snapshot. Additional changes: - Add `local()` in `@font-face` to check if font is already installed before downloading the font (must be done for Safari, but not Edge/Chrome) - Add "Export output to image" menu item in notebook cell, since each cell output can take its own snapshot (which can be downloaded) ![image](https://user-images.githubusercontent.com/21954022/117454706-b5f16600-af46-11eb-8535-6bf99f3d9170.png)
2021-05-11 19:24:05 +01:00
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
2021-01-20 15:15:01 +00:00
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
import type NotebookManager from "./Notebook/NotebookManager";
import type { NotebookPaneContent } from "./Notebook/NotebookManager";
2021-01-20 15:15:01 +00:00
import { NotebookUtil } from "./Notebook/NotebookUtil";
2021-07-06 21:21:23 +01:00
import { useNotebook } from "./Notebook/useNotebook";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import { AddDatabasePanel } from "./Panes/AddDatabasePanel/AddDatabasePanel";
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane/BrowseQueriesPane";
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
import { GitHubReposPanel } from "./Panes/GitHubReposPanel/GitHubReposPanel";
import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane";
import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel";
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
2021-03-11 05:02:55 +00:00
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
2021-07-09 05:32:22 +01:00
import TabsBase from "./Tabs/TabsBase";
2021-03-11 05:02:55 +00:00
import TerminalTab from "./Tabs/TerminalTab";
import Database from "./Tree/Database";
import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
2021-01-20 15:15:01 +00:00
import StoredProcedure from "./Tree/StoredProcedure";
2021-06-18 19:25:08 +01:00
import { useDatabases } from "./useDatabases";
2021-06-24 19:56:33 +01:00
import { useSelectedNode } from "./useSelectedNode";
2021-01-20 15:15:01 +00:00
BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
var tmp = ComponentRegisterer;
export default class Explorer {
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
public queriesClient: QueriesClient;
public tableDataClient: TableDataClient;
// Resource Tree
private resourceTree: ResourceTreeAdapter;
// Resource Token
public resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken;
2021-01-20 15:15:01 +00:00
// Tabs
public isTabsContentExpanded: ko.Observable<boolean>;
public gitHubOAuthService: GitHubOAuthService;
2021-01-20 15:15:01 +00:00
// Notebooks
public notebookManager?: NotebookManager;
2021-01-20 15:15:01 +00:00
private _isInitializingNotebooks: boolean;
private notebookToImport: {
name: string;
content: string;
};
private static readonly MaxNbDatabasesToAutoExpand = 5;
2021-07-09 05:32:22 +01:00
constructor() {
2021-01-20 15:15:01 +00:00
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree,
});
this._isInitializingNotebooks = false;
2021-07-06 21:21:23 +01:00
useNotebook.subscribe(
() => this.refreshCommandBarButtons(),
(state) => state.isNotebooksEnabledForAccount
);
2021-01-20 15:15:01 +00:00
this.queriesClient = new QueriesClient(this);
2021-06-24 19:56:33 +01:00
useSelectedNode.subscribe(() => {
2021-01-20 15:15:01 +00:00
// Make sure switching tabs restores tabs display
this.isTabsContentExpanded(false);
});
this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => {
if (userContext.features.enableFixedCollectionWithSharedThroughput) {
2021-01-20 15:15:01 +00:00
return true;
}
if (!userContext.databaseAccount) {
2021-01-20 15:15:01 +00:00
return false;
}
return isCapabilityEnabled("EnableMongo");
2021-01-20 15:15:01 +00:00
});
2021-07-09 05:32:22 +01:00
useTabs.subscribe(
(openedTabs: TabsBase[]) => {
if (openedTabs.length === 0) {
useSelectedNode.getState().setSelectedNode(undefined);
useCommandBar.getState().setContextButtons([]);
}
},
(state) => state.openedTabs
);
2021-01-20 15:15:01 +00:00
this.isTabsContentExpanded = ko.observable(false);
document.addEventListener(
"contextmenu",
function (e) {
e.preventDefault();
},
false
);
$(function () {
$(document.body).click(() => $(".commandDropdownContainer").hide());
});
2021-03-15 03:53:16 +00:00
switch (userContext.apiType) {
case "Tables":
this.tableDataClient = new TablesAPIDataClient();
break;
case "Cassandra":
this.tableDataClient = new CassandraAPIDataClient();
break;
}
2021-01-20 15:15:01 +00:00
this._initSettings();
TelemetryProcessor.traceSuccess(
Action.InitializeDataExplorer,
{ dataExplorerArea: Constants.Areas.ResourceTree },
startKey
);
2021-07-06 21:21:23 +01:00
useNotebook.subscribe(
async () => {
if (!this.notebookManager) {
const NotebookManager = await (
await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager")
).default;
this.notebookManager = new NotebookManager();
this.notebookManager.initialize({
container: this,
resourceTree: this.resourceTree,
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
refreshNotebookList: () => this.refreshNotebookList(),
});
}
2021-01-20 15:15:01 +00:00
2021-07-06 21:21:23 +01:00
this.refreshCommandBarButtons();
this.refreshNotebookList();
},
(state) => state.isNotebookEnabled
);
2021-01-20 15:15:01 +00:00
this.resourceTree = new ResourceTreeAdapter(this);
this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this);
// Override notebook server parameters from URL parameters
if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) {
2021-07-06 21:21:23 +01:00
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl,
authToken: userContext.features.notebookServerToken,
});
}
2021-01-20 15:15:01 +00:00
if (userContext.features.notebookBasePath) {
2021-07-06 21:21:23 +01:00
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
}
2021-01-20 15:15:01 +00:00
if (userContext.features.livyEndpoint) {
2021-07-06 21:21:23 +01:00
useNotebook.getState().setSparkClusterConnectionInfo({
userName: undefined,
password: undefined,
endpoints: [
{
endpoint: userContext.features.livyEndpoint,
kind: DataModels.SparkClusterEndpointKind.Livy,
},
],
});
}
2021-05-19 19:32:12 +01:00
if (configContext.enableSchemaAnalyzer) {
userContext.features.enableSchemaAnalyzer = true;
}
2021-07-06 21:21:23 +01:00
this.refreshExplorer();
2021-01-20 15:15:01 +00:00
}
public openEnableSynapseLinkDialog(): void {
const addSynapseLinkDialogProps: DialogProps = {
linkProps: {
linkText: "Learn more",
linkUrl: "https://aka.ms/cosmosdb-synapselink",
},
isModal: true,
title: `Enable Azure Synapse Link on your Cosmos DB account`,
subText: `Enable Azure Synapse Link to perform near real time analytical analytics on this account, without impacting the performance of your transactional workloads.
Azure Synapse Link brings together Cosmos Db Analytical Store and Synapse Analytics`,
primaryButtonText: "Enable Azure Synapse Link",
secondaryButtonText: "Cancel",
onPrimaryButtonClick: async () => {
const startTime = TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink);
const clearInProgressMessage = logConsoleProgress(
2021-01-20 15:15:01 +00:00
"Enabling Azure Synapse Link for this account. This may take a few minutes before you can enable analytical store for this account."
);
2021-07-06 21:21:23 +01:00
useNotebook.getState().setIsSynapseLinkUpdating(true);
2021-05-20 15:54:26 +01:00
useDialog.getState().closeDialog();
2021-01-20 15:15:01 +00:00
try {
2021-05-28 21:20:19 +01:00
await update(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, {
2021-05-25 20:46:52 +01:00
properties: {
enableAnalyticalStorage: true,
},
});
clearInProgressMessage();
logConsoleInfo("Enabled Azure Synapse Link for this account");
TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, {}, startTime);
2021-05-25 20:46:52 +01:00
userContext.databaseAccount.properties.enableAnalyticalStorage = true;
2021-01-20 15:15:01 +00:00
} catch (error) {
clearInProgressMessage();
logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`);
TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, {}, startTime);
2021-01-20 15:15:01 +00:00
} finally {
2021-07-06 21:21:23 +01:00
useNotebook.getState().setIsSynapseLinkUpdating(false);
2021-01-20 15:15:01 +00:00
}
},
onSecondaryButtonClick: () => {
2021-05-20 15:54:26 +01:00
useDialog.getState().closeDialog();
2021-01-20 15:15:01 +00:00
TelemetryProcessor.traceCancel(Action.EnableAzureSynapseLink);
},
};
2021-05-20 15:54:26 +01:00
useDialog.getState().openDialog(addSynapseLinkDialogProps);
2021-01-20 15:15:01 +00:00
TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink);
// TODO: return result
}
2021-07-09 05:32:22 +01:00
public async refreshDatabaseForResourceToken(): Promise<void> {
2021-06-11 01:02:07 +01:00
const databaseId = userContext.parsedResourceToken?.databaseId;
const collectionId = userContext.parsedResourceToken?.collectionId;
2021-01-20 15:15:01 +00:00
if (!databaseId || !collectionId) {
2021-07-09 05:32:22 +01:00
return;
2021-01-20 15:15:01 +00:00
}
2021-07-09 05:32:22 +01:00
const collection: DataModels.Collection = await readCollection(databaseId, collectionId);
const resourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection);
useDatabases.setState({ resourceTokenCollection });
useSelectedNode.getState().setSelectedNode(resourceTokenCollection);
2021-01-20 15:15:01 +00:00
}
2021-07-09 05:32:22 +01:00
public async refreshAllDatabases(): Promise<void> {
2021-01-20 15:15:01 +00:00
const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, {
dataExplorerArea: Constants.Areas.ResourceTree,
});
2021-07-09 05:32:22 +01:00
try {
const databases: DataModels.Database[] = await readDatabases();
TelemetryProcessor.traceSuccess(
Action.LoadDatabases,
{
dataExplorerArea: Constants.Areas.ResourceTree,
},
startKey
);
const currentDatabases = useDatabases.getState().databases;
const deltaDatabases = this.getDeltaDatabases(databases, currentDatabases);
let updatedDatabases = currentDatabases.filter(
(database) => !deltaDatabases.toDelete.some((deletedDatabase) => deletedDatabase.id() === database.id())
);
updatedDatabases = [...updatedDatabases, ...deltaDatabases.toAdd].sort((db1, db2) =>
db1.id().localeCompare(db2.id())
);
useDatabases.setState({ databases: updatedDatabases });
await this.refreshAndExpandNewDatabases(deltaDatabases.toAdd, currentDatabases);
} catch (error) {
const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure(
Action.LoadDatabases,
{
dataExplorerArea: Constants.Areas.ResourceTree,
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
logConsoleError(`Error while refreshing databases: ${errorMessage}`);
}
2021-01-20 15:15:01 +00:00
}
public onRefreshDatabasesKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.onRefreshResourcesClick(source, null);
return false;
}
return true;
};
public onRefreshResourcesClick = (source: any, event: MouseEvent): void => {
const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, {
description: "Refresh button clicked",
dataExplorerArea: Constants.Areas.ResourceTree,
});
userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases();
2021-01-20 15:15:01 +00:00
this.refreshNotebookList();
};
// Facade
public provideFeedbackEmail = () => {
2021-04-14 00:13:24 +01:00
window.open(Constants.Urls.feedbackEmail, "_blank");
2021-01-20 15:15:01 +00:00
};
public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise<void> {
if (!databaseAccount) {
throw new Error("No database account specified");
}
if (this._isInitializingNotebooks) {
return;
}
this._isInitializingNotebooks = true;
await this.ensureNotebookWorkspaceRunning();
const connectionInfo = await listConnectionInfo(
userContext.subscriptionId,
userContext.resourceGroup,
databaseAccount.name,
"default"
);
2021-01-20 15:15:01 +00:00
2021-07-06 21:21:23 +01:00
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
});
this.refreshNotebookList();
2021-01-20 15:15:01 +00:00
this._isInitializingNotebooks = false;
}
public resetNotebookWorkspace() {
2021-07-06 21:21:23 +01:00
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
2021-01-20 15:15:01 +00:00
handleError(
"Attempt to reset notebook workspace, but notebook is not enabled",
"Explorer/resetNotebookWorkspace"
);
return;
}
2021-02-25 00:41:28 +00:00
2021-01-20 15:15:01 +00:00
const resetConfirmationDialogProps: DialogProps = {
isModal: true,
title: "Reset Workspace",
subText: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?",
primaryButtonText: "OK",
secondaryButtonText: "Cancel",
onPrimaryButtonClick: this._resetNotebookWorkspace,
2021-05-20 15:54:26 +01:00
onSecondaryButtonClick: () => useDialog.getState().closeDialog(),
2021-01-20 15:15:01 +00:00
};
2021-05-20 15:54:26 +01:00
useDialog.getState().openDialog(resetConfirmationDialogProps);
2021-01-20 15:15:01 +00:00
}
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
);
2021-01-20 15:15:01 +00:00
return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default");
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace");
return false;
}
}
private async ensureNotebookWorkspaceRunning() {
if (!userContext.databaseAccount) {
2021-01-20 15:15:01 +00:00
return;
}
let clearMessage;
try {
const notebookWorkspace = await getWorkspace(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
2021-01-20 15:15:01 +00:00
"default"
);
if (
notebookWorkspace &&
notebookWorkspace.properties &&
notebookWorkspace.properties.status &&
notebookWorkspace.properties.status.toLowerCase() === "stopped"
) {
clearMessage = NotificationConsoleUtils.logConsoleProgress("Initializing notebook workspace");
await start(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default");
2021-01-20 15:15:01 +00:00
}
} catch (error) {
handleError(error, "Explorer/ensureNotebookWorkspaceRunning", "Failed to initialize notebook workspace");
} finally {
clearMessage && clearMessage();
}
}
private _resetNotebookWorkspace = async () => {
2021-05-20 15:54:26 +01:00
useDialog.getState().closeDialog();
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
2021-01-20 15:15:01 +00:00
try {
await this.notebookManager?.notebookClient.resetWorkspace();
logConsoleInfo("Successfully reset notebook workspace");
2021-01-20 15:15:01 +00:00
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
} catch (error) {
logConsoleError(`Failed to reset notebook workspace: ${error}`);
2021-01-20 15:15:01 +00:00
TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, {
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
throw error;
} finally {
clearInProgressMessage();
2021-01-20 15:15:01 +00:00
}
};
private getDeltaDatabases(
2021-07-09 05:32:22 +01:00
updatedDatabaseList: DataModels.Database[],
databases: ViewModels.Database[]
): {
toAdd: ViewModels.Database[];
toDelete: ViewModels.Database[];
} {
2021-01-20 15:15:01 +00:00
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
const databaseExists = _.some(
2021-06-18 19:25:08 +01:00
databases,
2021-01-20 15:15:01 +00:00
(existingDatabase: ViewModels.Database) => existingDatabase.id() === database.id
);
return !databaseExists;
});
const databasesToAdd: ViewModels.Database[] = newDatabases.map(
(newDatabase: DataModels.Database) => new Database(this, newDatabase)
);
2021-07-09 05:32:22 +01:00
const databasesToDelete: ViewModels.Database[] = [];
databases.forEach((database: ViewModels.Database) => {
2021-01-20 15:15:01 +00:00
const databasePresentInUpdatedList = _.some(
updatedDatabaseList,
(db: DataModels.Database) => db.id === database.id()
);
if (!databasePresentInUpdatedList) {
databasesToDelete.push(database);
}
});
return { toAdd: databasesToAdd, toDelete: databasesToDelete };
}
2021-07-09 05:32:22 +01:00
private async refreshAndExpandNewDatabases(
newDatabases: ViewModels.Database[],
databases: ViewModels.Database[]
): Promise<void> {
// we reload collections for all databases so the resource tree reflects any collection-level changes
// i.e addition of stored procedures, etc.
// If the user has a lot of databases, only load expanded databases.
const databasesToLoad =
databases.length <= Explorer.MaxNbDatabasesToAutoExpand
? databases
: databases.filter((db) => db.isDatabaseExpanded() || db.id() === Constants.SavedQueries.DatabaseName);
const startKey: number = TelemetryProcessor.traceStart(Action.LoadCollections, {
dataExplorerArea: Constants.Areas.ResourceTree,
});
try {
await Promise.all(
databasesToLoad.map(async (database: ViewModels.Database) => {
await database.loadCollections();
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id());
if (isNewDatabase) {
database.expandDatabase();
}
useTabs
.getState()
.refreshActiveTab((tab) => tab.collection && tab.collection.getDatabase().id() === database.id());
TelemetryProcessor.traceSuccess(
Action.LoadCollections,
{ dataExplorerArea: Constants.Areas.ResourceTree },
startKey
);
})
);
} catch (error) {
TelemetryProcessor.traceFailure(
Action.LoadCollections,
{
dataExplorerArea: Constants.Areas.ResourceTree,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
}
2021-01-20 15:15:01 +00:00
}
2021-07-09 05:32:22 +01:00
private _initSettings() {
if (!ExplorerSettings.hasSettingsDefined()) {
ExplorerSettings.createDefaultSettings();
}
2021-01-20 15:15:01 +00:00
}
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
2021-07-06 21:21:23 +01:00
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
2021-01-20 15:15:01 +00:00
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);
promise
.then(() => this.resourceTree.triggerRender())
.catch((reason: any) => this.showOkModalDialog("Unable to upload file", 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;
2021-07-06 21:21:23 +01:00
if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) {
2021-01-20 15:15:01 +00:00
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;
2021-07-06 21:21:23 +01:00
if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) {
2021-01-20 15:15:01 +00:00
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,
Fix bug publish screenshot (#762) [Preview this branch](https://cosmos-explorer-preview.azurewebsites.net/pull/762?feature.someFeatureFlagYouMightNeed=true) The main change in this PR fixes the snapshot functionality in the Publish pane-related components. Because the code cell outputs are now rendered in their own iframes for security reasons, a single snapshot of the notebook is no longer possible: each cell output takes its own snapshot and the snapshots are collated on the main notebook snapshot. - Move the snapshot functionality to notebook components: this removes the reference of the notebook DOM node that we must pass to the Publish pane via explorer. - Add slice in the state and actions in notebook redux for notebook snapshot requests and result - Add post robot message to take snapshots and receive results - Add logic in `NotebookRenderer` to wait for all output snapshots done before taking the main one collating. - Use `zustand` to share snapshot between Redux world and React world. This solves the issue of keeping the `PanelContainer` component generic, while being able to update its children (`PublishPanel` component) with the new snapshot. Additional changes: - Add `local()` in `@font-face` to check if font is already installed before downloading the font (must be done for Safari, but not Edge/Chrome) - Add "Export output to image" menu item in notebook cell, since each cell output can take its own snapshot (which can be downloaded) ![image](https://user-images.githubusercontent.com/21954022/117454706-b5f16600-af46-11eb-8535-6bf99f3d9170.png)
2021-05-11 19:24:05 +01:00
notebookContentRef?: string,
onTakeSnapshot?: (request: SnapshotRequest) => void,
onClosePanel?: () => void
): Promise<void> {
2021-01-20 15:15:01 +00:00
if (this.notebookManager) {
Fix bug publish screenshot (#762) [Preview this branch](https://cosmos-explorer-preview.azurewebsites.net/pull/762?feature.someFeatureFlagYouMightNeed=true) The main change in this PR fixes the snapshot functionality in the Publish pane-related components. Because the code cell outputs are now rendered in their own iframes for security reasons, a single snapshot of the notebook is no longer possible: each cell output takes its own snapshot and the snapshots are collated on the main notebook snapshot. - Move the snapshot functionality to notebook components: this removes the reference of the notebook DOM node that we must pass to the Publish pane via explorer. - Add slice in the state and actions in notebook redux for notebook snapshot requests and result - Add post robot message to take snapshots and receive results - Add logic in `NotebookRenderer` to wait for all output snapshots done before taking the main one collating. - Use `zustand` to share snapshot between Redux world and React world. This solves the issue of keeping the `PanelContainer` component generic, while being able to update its children (`PublishPanel` component) with the new snapshot. Additional changes: - Add `local()` in `@font-face` to check if font is already installed before downloading the font (must be done for Safari, but not Edge/Chrome) - Add "Export output to image" menu item in notebook cell, since each cell output can take its own snapshot (which can be downloaded) ![image](https://user-images.githubusercontent.com/21954022/117454706-b5f16600-af46-11eb-8535-6bf99f3d9170.png)
2021-05-11 19:24:05 +01:00
await this.notebookManager.openPublishNotebookPane(
name,
content,
notebookContentRef,
onTakeSnapshot,
onClosePanel
);
2021-01-20 15:15:01 +00:00
}
}
public copyNotebook(name: string, content: string): void {
this.notebookManager?.openCopyNotebookPane(name, content);
2021-01-20 15:15:01 +00:00
}
public showOkModalDialog(title: string, msg: string): void {
2021-05-20 15:54:26 +01:00
useDialog.getState().openDialog({
2021-01-20 15:15:01 +00:00
isModal: true,
title,
subText: msg,
primaryButtonText: "Close",
secondaryButtonText: undefined,
2021-05-20 15:54:26 +01:00
onPrimaryButtonClick: () => {
useDialog.getState().closeDialog();
},
2021-01-20 15:15:01 +00:00
onSecondaryButtonClick: undefined,
});
}
public showOkCancelModalDialog(
title: string,
msg: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
isPrimaryButtonDisabled?: boolean
): void {
2021-05-20 15:54:26 +01:00
useDialog.getState().openDialog({
2021-01-20 15:15:01 +00:00
isModal: true,
title,
subText: msg,
primaryButtonText: okLabel,
secondaryButtonText: cancelLabel,
onPrimaryButtonClick: () => {
2021-05-20 15:54:26 +01:00
useDialog.getState().closeDialog();
2021-01-20 15:15:01 +00:00
onOk && onOk();
},
onSecondaryButtonClick: () => {
2021-05-20 15:54:26 +01:00
useDialog.getState().closeDialog();
2021-01-20 15:15:01 +00:00
onCancel && onCancel();
},
choiceGroupProps,
textFieldProps,
primaryButtonDisabled: isPrimaryButtonDisabled,
});
}
/**
* 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}`);
}
2021-07-09 05:32:22 +01:00
const notebookTabs = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.NotebookV2,
(tab) =>
(tab as NotebookV2Tab).notebookPath &&
FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path)
) as NotebookV2Tab[];
2021-01-20 15:15:01 +00:00
let notebookTab = notebookTabs && notebookTabs[0];
if (notebookTab) {
2021-07-09 05:32:22 +01:00
useTabs.getState().activateTab(notebookTab);
2021-01-20 15:15:01 +00:00
} else {
const options: NotebookTabOptions = {
account: userContext.databaseAccount,
tabKind: ViewModels.CollectionTabKind.NotebookV2,
node: null,
title: notebookContentItem.name,
tabPath: notebookContentItem.path,
collection: null,
masterKey: userContext.masterKey || "",
isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null,
container: this,
notebookContentItem,
};
try {
const NotebookTabV2 = await import(/* webpackChunkName: "NotebookV2Tab" */ "./Tabs/NotebookV2Tab");
notebookTab = new NotebookTabV2.default(options);
2021-07-09 05:32:22 +01:00
useTabs.getState().activateNewTab(notebookTab);
2021-01-20 15:15:01 +00:00
} catch (reason) {
console.error("Import NotebookV2Tab failed!", reason);
return false;
}
}
return true;
}
public renameNotebook(notebookFile: NotebookContentItem): void {
2021-07-06 21:21:23 +01:00
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
2021-01-20 15:15:01 +00:00
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
2021-07-09 05:32:22 +01:00
const openedNotebookTabs = useTabs
.getState()
.getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => {
2021-01-20 15:15:01 +00:00
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path);
2021-07-09 05:32:22 +01:00
});
2021-01-20 15:15:01 +00:00
if (openedNotebookTabs.length > 0) {
this.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)
}
notebookFile={notebookFile}
/>
);
2021-01-20 15:15:01 +00:00
}
}
public onCreateDirectory(parent: NotebookContentItem): void {
2021-07-06 21:21:23 +01:00
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
2021-01-20 15:15:01 +00:00
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)
}
notebookFile={parent}
/>
);
2021-01-20 15:15:01 +00:00
}
public readFile(notebookFile: NotebookContentItem): Promise<string> {
2021-07-06 21:21:23 +01:00
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
2021-01-20 15:15:01 +00:00
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> {
2021-07-06 21:21:23 +01:00
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
2021-01-20 15:15:01 +00:00
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: any) => {
logConsoleError(`Could not download notebook ${getErrorMessage(error)}`);
2021-01-20 15:15:01 +00:00
clearMessage();
}
);
}
private refreshNotebookList = async (): Promise<void> => {
2021-07-06 21:21:23 +01:00
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
2021-01-20 15:15:01 +00:00
return;
}
await this.resourceTree.initialize();
this.notebookManager?.refreshPinnedRepos();
if (this.notebookToImport) {
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
}
};
public deleteNotebookFile(item: NotebookContentItem): Promise<void> {
2021-07-06 21:21:23 +01:00
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
2021-01-20 15:15:01 +00:00
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
2021-07-09 05:32:22 +01:00
const openedNotebookTabs = useTabs
.getState()
.getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => {
2021-01-20 15:15:01 +00:00
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path);
2021-07-09 05:32:22 +01:00
});
2021-01-20 15:15:01 +00:00
if (openedNotebookTabs.length > 0) {
this.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) {
2021-05-20 15:54:26 +01:00
useDialog.getState().openDialog({
2021-01-20 15:15:01 +00:00
isModal: true,
title: "Unable to delete file",
subText: "Directory is not empty.",
primaryButtonText: "Close",
secondaryButtonText: undefined,
2021-05-20 15:54:26 +01:00
onPrimaryButtonClick: () => useDialog.getState().closeDialog(),
2021-01-20 15:15:01 +00:00
onSecondaryButtonClick: undefined,
});
return Promise.reject();
}
return this.notebookManager?.notebookContentClient.deleteContentItem(item).then(
() => logConsoleInfo(`Successfully deleted: ${item.path}`),
(reason: any) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`)
2021-01-20 15:15:01 +00:00
);
}
/**
* This creates a new notebook file, then opens the notebook
*/
public onNewNotebookClicked(parent?: NotebookContentItem): void {
2021-07-06 21:21:23 +01:00
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
2021-01-20 15:15:01 +00:00
const error = "Attempt to create new notebook, but notebook is not enabled";
handleError(error, "Explorer/onNewNotebookClicked");
throw new Error(error);
}
parent = parent || this.resourceTree.myNotebooksContentRoot;
const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`);
2021-01-20 15:15:01 +00:00
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, {
dataExplorerArea: Constants.Areas.Notebook,
});
this.notebookManager?.notebookContentClient
.createNewNotebookFile(parent)
.then((newFile: NotebookContentItem) => {
logConsoleInfo(`Successfully created: ${newFile.name}`);
2021-01-20 15:15:01 +00:00
TelemetryProcessor.traceSuccess(
Action.CreateNewNotebook,
{
dataExplorerArea: Constants.Areas.Notebook,
},
startKey
);
return this.openNotebook(newFile);
})
.then(() => this.resourceTree.triggerRender())
.catch((error: any) => {
const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`;
logConsoleError(errorMessage);
2021-01-20 15:15:01 +00:00
TelemetryProcessor.traceFailure(
Action.CreateNewNotebook,
{
dataExplorerArea: Constants.Areas.Notebook,
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
})
.finally(clearInProgressMessage);
2021-01-20 15:15:01 +00:00
}
public refreshContentItem(item: NotebookContentItem): Promise<void> {
2021-07-06 21:21:23 +01:00
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
2021-01-20 15:15:01 +00:00
const error = "Attempt to refresh notebook list, but notebook is not enabled";
handleError(error, "Explorer/refreshContentItem");
return Promise.reject(new Error(error));
}
return this.notebookManager?.notebookContentClient.updateItemChildren(item);
}
public openNotebookTerminal(kind: ViewModels.TerminalKind) {
let title: string;
switch (kind) {
case ViewModels.TerminalKind.Default:
title = "Terminal";
break;
case ViewModels.TerminalKind.Mongo:
title = "Mongo Shell";
break;
case ViewModels.TerminalKind.Cassandra:
title = "Cassandra Shell";
break;
default:
throw new Error("Terminal kind: ${kind} not supported");
}
2021-07-09 05:32:22 +01:00
const terminalTabs: TerminalTab[] = useTabs
.getState()
.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle() === title) as TerminalTab[];
2021-01-20 15:15:01 +00:00
let index = 1;
if (terminalTabs.length > 0) {
index = terminalTabs[terminalTabs.length - 1].index + 1;
}
const newTab = new TerminalTab({
account: userContext.databaseAccount,
tabKind: ViewModels.CollectionTabKind.Terminal,
node: null,
title: `${title} ${index}`,
tabPath: `${title} ${index}`,
collection: null,
isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null,
container: this,
kind: kind,
index: index,
});
2021-01-20 15:15:01 +00:00
2021-07-09 05:32:22 +01:00
useTabs.getState().activateNewTab(newTab);
2021-01-20 15:15:01 +00:00
}
public async openGallery(
selectedTab?: GalleryTabKind,
notebookUrl?: string,
galleryItem?: IGalleryItem,
isFavorite?: boolean
) {
const title = "Gallery";
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
2021-07-09 05:32:22 +01:00
const galleryTab = useTabs
.getState()
.getTabs(ViewModels.CollectionTabKind.Gallery)
.find((tab) => tab.tabTitle() == title);
2021-01-20 15:15:01 +00:00
if (galleryTab instanceof GalleryTab) {
2021-07-09 05:32:22 +01:00
useTabs.getState().activateTab(galleryTab);
2021-01-20 15:15:01 +00:00
} else {
2021-07-09 05:32:22 +01:00
useTabs.getState().activateNewTab(
new GalleryTab(
{
tabKind: ViewModels.CollectionTabKind.Gallery,
title,
tabPath: title,
onLoadStartKey: null,
isTabsContentExpanded: ko.observable(true),
},
{
account: userContext.databaseAccount,
container: this,
junoClient: this.notebookManager?.junoClient,
selectedTab: selectedTab || GalleryTabKind.PublicGallery,
notebookUrl,
galleryItem,
isFavorite,
}
)
);
2021-01-20 15:15:01 +00:00
}
}
public async onNewCollectionClicked(databaseId?: string): Promise<void> {
if (userContext.apiType === "Cassandra") {
useSidePanel
.getState()
.openSidePanel(
"Add Table",
<CassandraAddCollectionPane explorer={this} cassandraApiClient={new CassandraAPIDataClient()} />
);
} else {
await useDatabases.getState().loadDatabaseOffers();
useSidePanel
.getState()
.openSidePanel("New " + getCollectionName(), <AddCollectionPanel explorer={this} databaseId={databaseId} />);
2021-01-20 15:15:01 +00:00
}
}
private refreshCommandBarButtons(): void {
2021-07-09 05:32:22 +01:00
const activeTab = useTabs.getState().activeTab;
2021-01-20 15:15:01 +00:00
if (activeTab) {
activeTab.onActivate(); // TODO only update tabs buttons?
} else {
2021-05-28 21:20:59 +01:00
useCommandBar.getState().setContextButtons([]);
2021-01-20 15:15:01 +00:00
}
}
private _openSetupNotebooksPaneForQuickstart(): void {
const title = "Enable Notebooks (Preview)";
const description =
"You have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
this.openSetupNotebooksPanel(title, description);
2021-01-20 15:15:01 +00:00
}
public async handleOpenFileAction(path: string): Promise<void> {
2021-07-06 21:21:23 +01:00
if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
2021-01-20 15:15:01 +00:00
this._openSetupNotebooksPaneForQuickstart();
}
// 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 {
2021-06-24 19:56:33 +01:00
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
}
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
useSidePanel
.getState()
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
}
public openAddDatabasePane(): void {
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={this} />);
}
public openBrowseQueriesPanel(): void {
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this} />);
}
public openSaveQueryPanel(): void {
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this} />);
}
public openUploadFilePanel(parent?: NotebookContentItem): void {
parent = parent || this.resourceTree.myNotebooksContentRoot;
useSidePanel
.getState()
.openSidePanel(
"Upload file to notebook server",
<UploadFilePane uploadFile={(name: string, content: string) => this.uploadFile(name, content, parent)} />
);
}
public openGitHubReposPanel(header: string, junoClient?: JunoClient): void {
useSidePanel
.getState()
.openSidePanel(
header,
<GitHubReposPanel
explorer={this}
gitHubClientProp={this.notebookManager.gitHubClient}
junoClientProp={junoClient}
/>
);
}
public openSetupNotebooksPanel(title: string, description: string): void {
useSidePanel
.getState()
.openSidePanel(title, <SetupNoteBooksPanel explorer={this} panelTitle={title} panelDescription={description} />);
}
2021-07-06 21:21:23 +01:00
public async refreshExplorer(): Promise<void> {
userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
2021-07-09 05:32:22 +01:00
: this.refreshAllDatabases();
2021-07-06 21:21:23 +01:00
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
const isNotebookEnabled: boolean =
userContext.authType !== AuthType.ResourceToken &&
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
userContext.features.enableNotebooks);
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed());
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
isNotebookEnabled,
dataExplorerArea: Constants.Areas.Notebook,
});
if (isNotebookEnabled) {
await this.initNotebooks(userContext.databaseAccount);
} else if (this.notebookToImport) {
// if notebooks is not enabled but the user is trying to do a quickstart setup with notebooks, open the SetupNotebooksPane
this._openSetupNotebooksPaneForQuickstart();
}
}
2021-01-20 15:15:01 +00:00
}