Move notebook flags to zustand (#912)

This commit is contained in:
victor-meng 2021-07-06 13:21:23 -07:00 committed by GitHub
parent 98d7bb37d5
commit db34024259
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 316 additions and 347 deletions

View File

@ -16,6 +16,7 @@ import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import Explorer from "./Explorer";
import { useNotebook } from "./Notebook/useNotebook";
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
import StoredProcedure from "./Tree/StoredProcedure";
@ -81,13 +82,13 @@ export const createCollectionContextMenuButton = (
iconSrc: HostedTerminalIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
if (container.isShellEnabled()) {
if (useNotebook.getState().isShellEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
},
label: container.isShellEnabled() ? "Open Mongo Shell" : "New Shell",
label: useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell",
});
}

View File

@ -30,17 +30,9 @@ exports[`SettingsComponent renders 1`] = `
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isSchemaEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function],
@ -57,7 +49,6 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular],
"parameters": [Function],
},
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
@ -115,17 +106,9 @@ exports[`SettingsComponent renders 1`] = `
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isSchemaEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function],
@ -142,7 +125,6 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular],
"parameters": [Function],
},
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],

View File

@ -30,7 +30,6 @@ import {
listConnectionInfo,
start,
} from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { stringToBlob } from "../Utils/BlobUtils";
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
@ -46,6 +45,7 @@ import { NotebookContentItem, NotebookContentItemType } from "./Notebook/Noteboo
import type NotebookManager from "./Notebook/NotebookManager";
import type { NotebookPaneContent } from "./Notebook/NotebookManager";
import { NotebookUtil } from "./Notebook/NotebookUtil";
import { useNotebook } from "./Notebook/useNotebook";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import { AddDatabasePanel } from "./Panes/AddDatabasePanel/AddDatabasePanel";
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane/BrowseQueriesPane";
@ -79,7 +79,6 @@ export interface ExplorerParams {
export default class Explorer {
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
public isAccountReady: ko.Observable<boolean>;
public queriesClient: QueriesClient;
public tableDataClient: TableDataClient;
@ -97,18 +96,9 @@ export default class Explorer {
public isSchemaEnabled: ko.Computed<boolean>;
// Notebooks
public isNotebookEnabled: ko.Observable<boolean>;
public isNotebooksEnabledForAccount: ko.Observable<boolean>;
public notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>;
public sparkClusterConnectionInfo: ko.Observable<DataModels.SparkClusterConnectionInfo>;
public isSynapseLinkUpdating: ko.Observable<boolean>;
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
public notebookManager?: NotebookManager;
public isShellEnabled: ko.Observable<boolean>;
private _isInitializingNotebooks: boolean;
private notebookBasePath: ko.Observable<string>;
private notebookToImport: {
name: string;
content: string;
@ -120,40 +110,11 @@ export default class Explorer {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree,
});
this.isAccountReady = ko.observable<boolean>(false);
this._isInitializingNotebooks = false;
this.isShellEnabled = ko.observable(false);
this.isNotebooksEnabledForAccount = ko.observable(false);
this.isNotebooksEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons());
this.isSynapseLinkUpdating = ko.observable<boolean>(false);
this.isAccountReady.subscribe(async (isAccountReady: boolean) => {
if (isAccountReady) {
userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases(true);
await this._refreshNotebooksEnabledStateForAccount();
this.isNotebookEnabled(
userContext.authType !== AuthType.ResourceToken &&
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
userContext.features.enableNotebooks)
);
this.isShellEnabled(this.isNotebookEnabled() && isPublicInternetAccessAllowed());
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
isNotebookEnabled: this.isNotebookEnabled(),
dataExplorerArea: Constants.Areas.Notebook,
});
if (this.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();
}
}
});
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
useNotebook.subscribe(
() => this.refreshCommandBarButtons(),
(state) => state.isNotebooksEnabledForAccount
);
this.queriesClient = new QueriesClient(this);
this.isSchemaEnabled = ko.computed<boolean>(() => userContext.features.enableSchema);
@ -214,53 +175,44 @@ export default class Explorer {
startKey
);
this.isNotebookEnabled = ko.observable(false);
this.isNotebookEnabled.subscribe(async () => {
if (!this.notebookManager) {
const NotebookManager = await (
await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager")
).default;
this.notebookManager = new NotebookManager();
this.notebookManager.initialize({
container: this,
notebookBasePath: this.notebookBasePath,
resourceTree: this.resourceTree,
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
refreshNotebookList: () => this.refreshNotebookList(),
});
}
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(),
});
}
this.refreshCommandBarButtons();
this.refreshNotebookList();
});
this.refreshCommandBarButtons();
this.refreshNotebookList();
},
(state) => state.isNotebookEnabled
);
this.resourceTree = new ResourceTreeAdapter(this);
this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this);
this.notebookServerInfo = ko.observable<DataModels.NotebookWorkspaceConnectionInfo>({
notebookServerEndpoint: undefined,
authToken: undefined,
});
this.notebookBasePath = ko.observable(Constants.Notebook.defaultBasePath);
this.sparkClusterConnectionInfo = ko.observable<DataModels.SparkClusterConnectionInfo>({
userName: undefined,
password: undefined,
endpoints: [],
});
// Override notebook server parameters from URL parameters
if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) {
this.notebookServerInfo({
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl,
authToken: userContext.features.notebookServerToken,
});
}
if (userContext.features.notebookBasePath) {
this.notebookBasePath(userContext.features.notebookBasePath);
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
}
if (userContext.features.livyEndpoint) {
this.sparkClusterConnectionInfo({
useNotebook.getState().setSparkClusterConnectionInfo({
userName: undefined,
password: undefined,
endpoints: [
@ -275,7 +227,8 @@ export default class Explorer {
if (configContext.enableSchemaAnalyzer) {
userContext.features.enableSchemaAnalyzer = true;
}
this.isAccountReady(true);
this.refreshExplorer();
}
public openEnableSynapseLinkDialog(): void {
@ -296,7 +249,7 @@ export default class Explorer {
const clearInProgressMessage = logConsoleProgress(
"Enabling Azure Synapse Link for this account. This may take a few minutes before you can enable analytical store for this account."
);
this.isSynapseLinkUpdating(true);
useNotebook.getState().setIsSynapseLinkUpdating(true);
useDialog.getState().closeDialog();
try {
@ -315,7 +268,7 @@ export default class Explorer {
logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`);
TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, {}, startTime);
} finally {
this.isSynapseLinkUpdating(false);
useNotebook.getState().setIsSynapseLinkUpdating(false);
}
},
@ -464,18 +417,17 @@ export default class Explorer {
"default"
);
this.notebookServerInfo({
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
});
this.notebookServerInfo.valueHasMutated();
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
public resetNotebookWorkspace() {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookClient) {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
handleError(
"Attempt to reset notebook workspace, but notebook is not enabled",
"Explorer/resetNotebookWorkspace"
@ -659,7 +611,7 @@ export default class Explorer {
}
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
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);
@ -677,7 +629,7 @@ export default class Explorer {
const item = NotebookUtil.createNotebookContentItem(name, path, "file");
const parent = this.resourceTree.myNotebooksContentRoot;
if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) {
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);
@ -694,7 +646,7 @@ export default class Explorer {
public async importAndOpenContent(name: string, content: string): Promise<boolean> {
const parent = this.resourceTree.myNotebooksContentRoot;
if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) {
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
}
@ -837,7 +789,7 @@ export default class Explorer {
}
public renameNotebook(notebookFile: NotebookContentItem): void {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
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);
@ -878,7 +830,7 @@ export default class Explorer {
}
public onCreateDirectory(parent: NotebookContentItem): void {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
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);
@ -908,7 +860,7 @@ export default class Explorer {
}
public readFile(notebookFile: NotebookContentItem): Promise<string> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
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);
@ -918,7 +870,7 @@ export default class Explorer {
}
public downloadFile(notebookFile: NotebookContentItem): Promise<void> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
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);
@ -955,56 +907,8 @@ export default class Explorer {
);
}
private async _refreshNotebooksEnabledStateForAccount(): Promise<void> {
const { databaseAccount, authType } = userContext;
if (
authType === AuthType.EncryptedToken ||
authType === AuthType.ResourceToken ||
authType === AuthType.MasterKey
) {
this.isNotebooksEnabledForAccount(false);
return;
}
const firstWriteLocation =
databaseAccount?.properties?.writeLocations &&
databaseAccount?.properties?.writeLocations[0]?.locationName.toLowerCase();
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
const authorizationHeader = getAuthorizationHeader();
try {
const response = await fetch(disallowedLocationsUri, {
method: "POST",
body: JSON.stringify({
resourceTypes: [Constants.ArmResourceTypes.notebookWorkspaces],
}),
headers: {
[authorizationHeader.header]: authorizationHeader.token,
[Constants.HttpHeaders.contentType]: "application/json",
},
});
if (!response.ok) {
throw new Error("Failed to fetch disallowed locations");
}
const disallowedLocations: string[] = await response.json();
if (!disallowedLocations) {
Logger.logInfo("No disallowed locations found", "Explorer/isNotebooksEnabledForAccount");
this.isNotebooksEnabledForAccount(true);
return;
}
// firstWriteLocation should not be disallowed
const isAccountInAllowedLocation = firstWriteLocation && disallowedLocations.indexOf(firstWriteLocation) === -1;
this.isNotebooksEnabledForAccount(isAccountInAllowedLocation);
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount");
this.isNotebooksEnabledForAccount(false);
}
}
private refreshNotebookList = async (): Promise<void> => {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
return;
}
@ -1016,7 +920,7 @@ export default class Explorer {
};
public deleteNotebookFile(item: NotebookContentItem): Promise<void> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
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);
@ -1057,7 +961,7 @@ export default class Explorer {
* This creates a new notebook file, then opens the notebook
*/
public onNewNotebookClicked(parent?: NotebookContentItem): void {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
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);
@ -1101,7 +1005,7 @@ export default class Explorer {
}
public refreshContentItem(item: NotebookContentItem): Promise<void> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to refresh notebook list, but notebook is not enabled";
handleError(error, "Explorer/refreshContentItem");
return Promise.reject(new Error(error));
@ -1110,10 +1014,6 @@ export default class Explorer {
return this.notebookManager?.notebookContentClient.updateItemChildren(item);
}
public getNotebookBasePath(): string {
return this.notebookBasePath();
}
public openNotebookTerminal(kind: ViewModels.TerminalKind) {
let title: string;
@ -1233,7 +1133,7 @@ export default class Explorer {
}
public async handleOpenFileAction(path: string): Promise<void> {
if (this.isAccountReady() && !(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
this._openSetupNotebooksPaneForQuickstart();
}
@ -1304,4 +1204,29 @@ export default class Explorer {
.getState()
.openSidePanel(title, <SetupNoteBooksPanel explorer={this} panelTitle={title} panelDescription={description} />);
}
public async refreshExplorer(): Promise<void> {
userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases(true);
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();
}
}
}

View File

@ -54,7 +54,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
if (container.tabsManager.activeTab()?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker", container.memoryUsageInfo));
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
}
return (

View File

@ -6,6 +6,7 @@ import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
import NotebookManager from "../../Notebook/NotebookManager";
import { useNotebook } from "../../Notebook/useNotebook";
import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
@ -28,9 +29,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
});
it("Account is not serverless - button should be visible", () => {
@ -71,18 +69,19 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
});
afterEach(() => {
updateUserContext({
portalEnv: "prod",
});
useNotebook.getState().setIsNotebookEnabled(false);
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
});
it("Notebooks is already enabled - button should be hidden", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
@ -90,8 +89,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Account is running on one of the national clouds - button should be hidden", () => {
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
updateUserContext({
portalEnv: "mooncake",
});
@ -102,8 +99,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Notebooks is not enabled but is available - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
@ -113,9 +109,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeDefined();
@ -139,24 +132,25 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isShellEnabled = ko.observable(true);
});
afterAll(() => {
updateUserContext({
apiType: "SQL",
});
useNotebook.getState().setIsShellEnabled(false);
});
beforeEach(() => {
updateUserContext({
apiType: "Mongo",
});
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
useNotebook.getState().setIsShellEnabled(true);
});
mockExplorer.isShellEnabled = ko.observable(true);
afterEach(() => {
useNotebook.getState().setIsNotebookEnabled(false);
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
});
it("Mongo Api not available - button should be hidden", () => {
@ -185,7 +179,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Notebooks is not enabled and is available - button should be hidden", () => {
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
@ -193,7 +187,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
useNotebook.getState().setIsNotebookEnabled(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
@ -203,8 +197,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
@ -214,9 +208,9 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
mockExplorer.isShellEnabled = ko.observable(false);
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
useNotebook.getState().setIsShellEnabled(false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
@ -237,7 +231,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
});
beforeEach(() => {
@ -248,8 +241,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
});
afterEach(() => {
useNotebook.getState().setIsNotebookEnabled(false);
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
});
it("Cassandra Api not available - button should be hidden", () => {
@ -283,7 +279,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Notebooks is not enabled and is available - button should be shown and enabled", () => {
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
@ -291,7 +287,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
useNotebook.getState().setIsNotebookEnabled(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
@ -301,8 +297,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
@ -327,23 +323,17 @@ describe("CommandBarComponentButtonFactory tests", () => {
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.notebookManager = new NotebookManager();
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
});
beforeEach(() => {
mockExplorer.isNotebookEnabled = ko.observable(false);
});
afterEach(() => {
jest.resetAllMocks();
useNotebook.getState().setIsNotebookEnabled(false);
});
it("Notebooks is enabled and GitHubOAuthService is not logged in - connect to github button should be visible", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
useNotebook.getState().setIsNotebookEnabled(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
@ -351,7 +341,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Notebooks is enabled and GitHubOAuthService is logged in - manage github settings button should be visible", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
useNotebook.getState().setIsNotebookEnabled(true);
mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);

View File

@ -28,6 +28,7 @@ import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
import { OpenFullScreen } from "../../OpenFullScreen";
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
@ -63,7 +64,7 @@ export function createStaticCommandBarButtons(
buttons.push(createDivider());
if (container.isNotebookEnabled()) {
if (useNotebook.getState().isNotebookEnabled) {
const newNotebookButton = createNewNotebookButton(container);
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
buttons.push(newNotebookButton);
@ -77,7 +78,7 @@ export function createStaticCommandBarButtons(
buttons.push(createNotebookWorkspaceResetButton(container));
if (
(userContext.apiType === "Mongo" &&
container.isShellEnabled() &&
useNotebook.getState().isShellEnabled &&
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
userContext.apiType === "Cassandra"
) {
@ -139,13 +140,13 @@ export function createContextCommandBarButtons(
const buttons: CommandButtonComponentProps[] = [];
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
const label = container.isShellEnabled() ? "Open Mongo Shell" : "New Shell";
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (container.isShellEnabled()) {
if (useNotebook.getState().isShellEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
@ -270,7 +271,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
onCommandClick: () => container.openEnableSynapseLinkDialog(),
commandButtonLabel: label,
hasPopup: false,
disabled: container.isSynapseLinkUpdating(),
disabled: useNotebook.getState().isSynapseLinkUpdating,
ariaLabel: label,
};
}
@ -450,9 +451,9 @@ function createEnableNotebooksButton(container: Explorer): CommandButtonComponen
onCommandClick: () => container.openSetupNotebooksPanel(label, description),
commandButtonLabel: label,
hasPopup: false,
disabled: !container.isNotebooksEnabledForAccount(),
disabled: !useNotebook.getState().isNotebooksEnabledForAccount,
ariaLabel: label,
tooltipText: container.isNotebooksEnabledForAccount() ? "" : tooltip,
tooltipText: useNotebook.getState().isNotebooksEnabledForAccount ? "" : tooltip,
};
}
@ -476,12 +477,13 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
const title = "Set up workspace";
const description =
"Looks like you have not created a workspace for this account. To proceed and start using features including mongo shell and notebook, we will need to create a default workspace in this account.";
const disableButton = !container.isNotebooksEnabledForAccount() && !container.isNotebookEnabled();
const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (container.isNotebookEnabled()) {
if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
container.openSetupNotebooksPanel(title, description);
@ -502,12 +504,13 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
const title = "Set up workspace";
const description =
"Looks like you have not created a workspace for this account. To proceed and start using features including cassandra shell and notebook, we will need to create a default workspace in this account.";
const disableButton = !container.isNotebooksEnabledForAccount() && !container.isNotebookEnabled();
const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (container.isNotebookEnabled()) {
if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
} else {
container.openSetupNotebooksPanel(title, description);

View File

@ -6,16 +6,14 @@ import {
IDropdownOption,
IDropdownStyles,
} from "@fluentui/react";
import { Observable } from "knockout";
import * as React from "react";
import _ from "underscore";
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
import { StyleConstants } from "../../../Common/Constants";
import { MemoryUsageInfo } from "../../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { MemoryTrackerComponent } from "./MemoryTrackerComponent";
import { MemoryTracker } from "./MemoryTrackerComponent";
/**
* Convert our NavbarButtonConfig to UI Fabric buttons
@ -185,12 +183,9 @@ export const createDivider = (key: string): ICommandBarItemProps => {
};
};
export const createMemoryTracker = (
key: string,
memoryUsageInfo: Observable<MemoryUsageInfo>
): ICommandBarItemProps => {
export const createMemoryTracker = (key: string): ICommandBarItemProps => {
return {
key,
onRender: () => <MemoryTrackerComponent memoryUsageInfo={memoryUsageInfo} />,
onRender: () => <MemoryTracker />,
};
};

View File

@ -1,48 +1,29 @@
import { ProgressIndicator, Spinner, SpinnerSize, Stack } from "@fluentui/react";
import { Observable, Subscription } from "knockout";
import * as React from "react";
import { MemoryUsageInfo } from "../../../Contracts/DataModels";
interface MemoryTrackerProps {
memoryUsageInfo: Observable<MemoryUsageInfo>;
}
export class MemoryTrackerComponent extends React.Component<MemoryTrackerProps> {
private memoryUsageInfoSubscription: Subscription;
public componentDidMount(): void {
this.memoryUsageInfoSubscription = this.props.memoryUsageInfo.subscribe(() => {
this.forceUpdate();
});
}
public componentWillUnmount(): void {
this.memoryUsageInfoSubscription && this.memoryUsageInfoSubscription.dispose();
}
public render(): JSX.Element {
const memoryUsageInfo: MemoryUsageInfo = this.props.memoryUsageInfo();
if (!memoryUsageInfo) {
return (
<Stack className="memoryTrackerContainer" horizontal>
<span>Memory</span>
<Spinner size={SpinnerSize.medium} />
</Stack>
);
}
const totalGB = memoryUsageInfo.totalKB / 1048576;
const usedGB = totalGB - memoryUsageInfo.freeKB / 1048576;
import { useNotebook } from "../../Notebook/useNotebook";
export const MemoryTracker: React.FC = (): JSX.Element => {
const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
if (!memoryUsageInfo) {
return (
<Stack className="memoryTrackerContainer" horizontal>
<span>Memory</span>
<ProgressIndicator
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
percentComplete={usedGB / totalGB}
/>
<Spinner size={SpinnerSize.medium} />
</Stack>
);
}
}
const totalGB = memoryUsageInfo.totalKB / 1048576;
const usedGB = totalGB - memoryUsageInfo.freeKB / 1048576;
return (
<Stack className="memoryTrackerContainer" horizontal>
<span>Memory</span>
<ProgressIndicator
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
percentComplete={usedGB / totalGB}
/>
</Stack>
);
};

View File

@ -8,25 +8,26 @@ import * as DataModels from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { useNotebook } from "./useNotebook";
export class NotebookContainerClient {
private clearReconnectionAttemptMessage? = () => {};
private isResettingWorkspace: boolean;
constructor(
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
private onConnectionLost: () => void,
private onMemoryUsageInfoUpdate: (update: DataModels.MemoryUsageInfo) => void
) {
if (notebookServerInfo() && notebookServerInfo().notebookServerEndpoint) {
constructor(private onConnectionLost: () => void) {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo?.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
} else {
const subscription = notebookServerInfo.subscribe((newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => {
if (newServerInfo && newServerInfo.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
}
subscription.dispose();
});
const unsub = useNotebook.subscribe(
(newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => {
if (newServerInfo?.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
}
unsub();
},
(state) => state.notebookServerInfo
);
}
}
@ -36,13 +37,14 @@ export class NotebookContainerClient {
private scheduleHeartbeat(delayMs: number): void {
setTimeout(() => {
this.getMemoryUsage()
.then((memoryUsageInfo) => this.onMemoryUsageInfoUpdate(memoryUsageInfo))
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo))
.finally(() => this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs));
}, delayMs);
}
private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
if (!this.notebookServerInfo() || !this.notebookServerInfo().notebookServerEndpoint) {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected";
Logger.logError(error, "NotebookContainerClient/getMemoryUsage");
return Promise.reject(error);
@ -98,7 +100,8 @@ export class NotebookContainerClient {
}
private async _resetWorkspace(): Promise<void> {
if (!this.notebookServerInfo() || !this.notebookServerInfo().notebookServerEndpoint) {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected";
Logger.logError(error, "NotebookContainerClient/resetWorkspace");
return Promise.reject(error);
@ -117,15 +120,11 @@ export class NotebookContainerClient {
}
private getNotebookServerConfig(): { notebookServerEndpoint: string; authToken: string } {
let authToken: string,
notebookServerEndpoint = this.notebookServerInfo().notebookServerEndpoint,
token = this.notebookServerInfo().authToken;
if (token) {
authToken = `Token ${token}`;
}
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
const authToken: string = notebookServerInfo.authToken ? `Token ${notebookServerInfo.authToken}` : undefined;
return {
notebookServerEndpoint,
notebookServerEndpoint: notebookServerInfo.notebookServerEndpoint,
authToken,
};
}

View File

@ -1,18 +1,14 @@
import { stringifyNotebook } from "@nteract/commutable";
import { FileType, IContent, IContentProvider, IEmptyContent, ServerConfig } from "@nteract/core";
import { AjaxResponse } from "rxjs/ajax";
import * as DataModels from "../../Contracts/DataModels";
import * as StringUtils from "../../Utils/StringUtils";
import * as FileSystemUtil from "./FileSystemUtil";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import { NotebookUtil } from "./NotebookUtil";
import { useNotebook } from "./useNotebook";
export class NotebookContentClient {
constructor(
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
public notebookBasePath: ko.Observable<string>,
private contentProvider: IContentProvider
) {}
constructor(private contentProvider: IContentProvider) {}
/**
* This updates the item and points all the children's parent to this item
@ -271,9 +267,10 @@ export class NotebookContentClient {
}
private getServerConfig(): ServerConfig {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
return {
endpoint: this.notebookServerInfo().notebookServerEndpoint,
token: this.notebookServerInfo().authToken,
endpoint: notebookServerInfo.notebookServerEndpoint,
token: notebookServerInfo.authToken,
crossDomain: true,
};
}

View File

@ -4,13 +4,11 @@
import { ImmutableNotebook } from "@nteract/commutable";
import type { IContentProvider } from "@nteract/core";
import ko from "knockout";
import React from "react";
import { contents } from "rx-jupyter";
import { Areas, HttpStatusCodes } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { MemoryUsageInfo } from "../../Contracts/DataModels";
import { GitHubClient } from "../../GitHub/GitHubClient";
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
@ -37,7 +35,6 @@ export type { NotebookPaneContent };
export interface NotebookManagerOptions {
container: Explorer;
notebookBasePath: ko.Observable<string>;
resourceTree: ResourceTreeAdapter;
refreshCommandBarButtons: () => void;
refreshNotebookList: () => void;
@ -81,17 +78,11 @@ export default class NotebookManager {
contents.JupyterContentProvider
);
this.notebookClient = new NotebookContainerClient(
this.params.container.notebookServerInfo,
() => this.params.container.initNotebooks(userContext?.databaseAccount),
(update: MemoryUsageInfo) => this.params.container.memoryUsageInfo(update)
this.notebookClient = new NotebookContainerClient(() =>
this.params.container.initNotebooks(userContext?.databaseAccount)
);
this.notebookContentClient = new NotebookContentClient(
this.params.container.notebookServerInfo,
this.params.notebookBasePath,
this.notebookContentProvider
);
this.notebookContentClient = new NotebookContentClient(this.notebookContentProvider);
this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
this.gitHubClient.setToken(token?.access_token);

View File

@ -0,0 +1,106 @@
import create, { UseStore } from "zustand";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
interface NotebookState {
isNotebookEnabled: boolean;
isNotebooksEnabledForAccount: boolean;
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo;
isSynapseLinkUpdating: boolean;
memoryUsageInfo: DataModels.MemoryUsageInfo;
isShellEnabled: boolean;
notebookBasePath: string;
isInitializingNotebooks: boolean;
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) => void;
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => void;
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void;
setIsShellEnabled: (isShellEnabled: boolean) => void;
setNotebookBasePath: (notebookBasePath: string) => void;
refreshNotebooksEnabledStateForAccount: () => Promise<void>;
}
export const useNotebook: UseStore<NotebookState> = create((set) => ({
isNotebookEnabled: false,
isNotebooksEnabledForAccount: false,
notebookServerInfo: {
notebookServerEndpoint: undefined,
authToken: undefined,
},
sparkClusterConnectionInfo: {
userName: undefined,
password: undefined,
endpoints: [],
},
isSynapseLinkUpdating: false,
memoryUsageInfo: undefined,
isShellEnabled: false,
notebookBasePath: Constants.Notebook.defaultBasePath,
isInitializingNotebooks: false,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
set({ notebookServerInfo }),
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) =>
set({ sparkClusterConnectionInfo }),
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }),
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
const { databaseAccount, authType } = userContext;
if (
authType === AuthType.EncryptedToken ||
authType === AuthType.ResourceToken ||
authType === AuthType.MasterKey
) {
set({ isNotebooksEnabledForAccount: false });
return;
}
const firstWriteLocation =
databaseAccount?.properties?.writeLocations &&
databaseAccount?.properties?.writeLocations[0]?.locationName.toLowerCase();
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
const authorizationHeader = getAuthorizationHeader();
try {
const response = await fetch(disallowedLocationsUri, {
method: "POST",
body: JSON.stringify({
resourceTypes: [Constants.ArmResourceTypes.notebookWorkspaces],
}),
headers: {
[authorizationHeader.header]: authorizationHeader.token,
[Constants.HttpHeaders.contentType]: "application/json",
},
});
if (!response.ok) {
throw new Error("Failed to fetch disallowed locations");
}
const disallowedLocations: string[] = await response.json();
if (!disallowedLocations) {
Logger.logInfo("No disallowed locations found", "Explorer/isNotebooksEnabledForAccount");
set({ isNotebooksEnabledForAccount: true });
return;
}
// firstWriteLocation should not be disallowed
const isAccountInAllowedLocation = firstWriteLocation && disallowedLocations.indexOf(firstWriteLocation) === -1;
set({ isNotebooksEnabledForAccount: isAccountInAllowedLocation });
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount");
set({ isNotebooksEnabledForAccount: false });
}
},
}));

View File

@ -9,6 +9,7 @@ import * as GitHubUtils from "../../../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
import { useNotebook } from "../../Notebook/useNotebook";
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
@ -101,7 +102,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
case "MyNotebooks":
parent = {
name: ResourceTreeAdapter.MyNotebooksTitle,
path: container.getNotebookBasePath(),
path: useNotebook.getState().notebookBasePath,
type: NotebookContentItemType.Directory,
};
break;

View File

@ -19,17 +19,9 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isSchemaEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function],
@ -46,7 +38,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"container": [Circular],
"parameters": [Function],
},
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],

View File

@ -63,7 +63,7 @@ export const SetupNoteBooksPanel: FunctionComponent<SetupNoteBooksPanelProps> =
userContext.databaseAccount.name,
"default"
);
explorer.isAccountReady.valueHasMutated(); // re-trigger init notebooks
explorer.refreshExplorer();
closeSidePanel();

View File

@ -9,17 +9,9 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isSchemaEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function],
@ -36,7 +28,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"container": [Circular],
"parameters": [Function],
},
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],

View File

@ -1,4 +1,3 @@
import * as ko from "knockout";
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
import Explorer from "../Explorer";
import { TabsManager } from "../Tabs/TabsManager";
@ -7,7 +6,6 @@ jest.mock("../Explorer");
const createExplorer = () => {
const mock = new Explorer();
mock.isNotebookEnabled = ko.observable(false);
mock.tabsManager = new TabsManager();
return mock as jest.Mocked<Explorer>;
};

View File

@ -22,6 +22,7 @@ import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLaunc
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
import Explorer from "../Explorer";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
import { useNotebook } from "../Notebook/useNotebook";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
@ -61,8 +62,13 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
public componentDidMount() {
this.subscriptions.push(
{ dispose: useSelectedNode.subscribe(() => this.setState({})) },
this.container.isNotebookEnabled.subscribe(() => this.setState({}))
{
dispose: useNotebook.subscribe(
() => this.setState({}),
(state) => state.isNotebookEnabled
),
},
{ dispose: useSelectedNode.subscribe(() => this.setState({})) }
);
}
@ -210,7 +216,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
});
}
if (this.container.isNotebookEnabled()) {
if (useNotebook.getState().isNotebookEnabled) {
heroes.push({
iconSrc: NewNotebookIcon,
title: "New Notebook",

View File

@ -6,6 +6,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import Explorer from "../Explorer";
import { NotebookClientV2 } from "../Notebook/NotebookClientV2";
import { useNotebook } from "../Notebook/useNotebook";
import TabsBase from "./TabsBase";
export interface NotebookTabBaseOptions extends ViewModels.TabOptions {
@ -28,7 +29,7 @@ export default class NotebookTabBase extends TabsBase {
if (!NotebookTabBase.clientManager) {
NotebookTabBase.clientManager = new NotebookClientV2({
connectionInfo: this.container.notebookServerInfo(),
connectionInfo: useNotebook.getState().notebookServerInfo,
databaseAccountName: userContext?.databaseAccount?.name,
defaultExperience: userContext.apiType,
contentProvider: this.container.notebookManager?.notebookContentProvider,

View File

@ -23,6 +23,7 @@ import * as CdbActions from "../Notebook/NotebookComponent/actions";
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types";
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
import { useNotebook } from "../Notebook/useNotebook";
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
export interface NotebookTabOptions extends NotebookTabBaseOptions {
@ -39,10 +40,13 @@ export default class NotebookTabV2 extends NotebookTabBase {
this.container = options.container;
this.notebookPath = ko.observable(options.notebookContentItem.path);
this.container.notebookServerInfo.subscribe(() => logConsoleInfo("New notebook server info received."));
useNotebook.subscribe(
() => logConsoleInfo("New notebook server info received."),
(state) => state.notebookServerInfo
);
this.notebookComponentAdapter = new NotebookComponentAdapter({
contentItem: options.notebookContentItem,
notebooksBasePath: this.container.getNotebookBasePath(),
notebooksBasePath: useNotebook.getState().notebookBasePath,
notebookClient: NotebookTabBase.clientManager,
onUpdateKernelInfo: this.onKernelUpdate,
});
@ -359,8 +363,8 @@ export default class NotebookTabV2 extends NotebookTabBase {
};
private async configureServiceEndpoints(kernelName: string) {
const notebookConnectionInfo = this.container && this.container.notebookServerInfo();
const sparkClusterConnectionInfo = this.container && this.container.sparkClusterConnectionInfo();
const notebookConnectionInfo = useNotebook.getState().notebookServerInfo;
const sparkClusterConnectionInfo = useNotebook.getState().sparkClusterConnectionInfo;
await NotebookConfigurationUtils.configureServiceEndpoints(
this.notebookPath(),
notebookConnectionInfo,

View File

@ -8,6 +8,7 @@ import { userContext } from "../../UserContext";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { NotebookTerminalComponent } from "../Controls/Notebook/NotebookTerminalComponent";
import Explorer from "../Explorer";
import { useNotebook } from "../Notebook/useNotebook";
import TabsBase from "./TabsBase";
export interface TerminalTabOptions extends ViewModels.TabOptions {
@ -54,8 +55,8 @@ export default class TerminalTab extends TabsBase {
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
if (
this.isTemplateReady() &&
this.container.isNotebookEnabled() &&
this.container.notebookServerInfo().notebookServerEndpoint
useNotebook.getState().isNotebookEnabled &&
useNotebook.getState().notebookServerInfo?.notebookServerEndpoint
) {
return true;
}
@ -95,7 +96,7 @@ export default class TerminalTab extends TabsBase {
throw new Error(`Terminal kind: ${options.kind} not supported`);
}
const info: DataModels.NotebookWorkspaceConnectionInfo = options.container.notebookServerInfo();
const info: DataModels.NotebookWorkspaceConnectionInfo = useNotebook.getState().notebookServerInfo;
return {
authToken: info.authToken,
notebookServerEndpoint: `${info.notebookServerEndpoint.replace(/\/+$/, "")}/${endpointSuffix}`,

View File

@ -31,6 +31,7 @@ import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { NotebookUtil } from "../Notebook/NotebookUtil";
import { useNotebook } from "../Notebook/useNotebook";
import TabsBase from "../Tabs/TabsBase";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
@ -57,7 +58,10 @@ export class ResourceTreeAdapter implements ReactAdapter {
useSelectedNode.subscribe(() => this.triggerRender());
this.container.tabsManager.activeTab.subscribe((newValue: TabsBase) => this.triggerRender());
this.container.isNotebookEnabled.subscribe((newValue) => this.triggerRender());
useNotebook.subscribe(
() => this.triggerRender(),
(state) => state.isNotebookEnabled
);
useDatabases.subscribe(() => this.triggerRender());
this.triggerRender();
@ -91,7 +95,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
const dataRootNode = this.buildDataTree();
const notebooksRootNode = this.buildNotebooksTrees();
if (this.container.isNotebookEnabled()) {
if (useNotebook.getState().isNotebookEnabled) {
return (
<>
<AccordionComponent>
@ -122,12 +126,12 @@ export class ResourceTreeAdapter implements ReactAdapter {
this.myNotebooksContentRoot = {
name: ResourceTreeAdapter.MyNotebooksTitle,
path: this.container.getNotebookBasePath(),
path: useNotebook.getState().notebookBasePath,
type: NotebookContentItemType.Directory,
};
// Only if notebook server is available we can refresh
if (this.container.notebookServerInfo().notebookServerEndpoint) {
if (useNotebook.getState().notebookServerInfo?.notebookServerEndpoint) {
refreshTasks.push(
this.container.refreshContentItem(this.myNotebooksContentRoot).then(() => {
this.triggerRender();
@ -268,7 +272,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection),
});
if (this.container.isNotebookEnabled() && userContext.apiType === "Mongo" && isPublicInternetAccessAllowed()) {
if (
useNotebook.getState().isNotebookEnabled &&
userContext.apiType === "Mongo" &&
isPublicInternetAccessAllowed()
) {
children.push({
label: "Schema (Preview)",
onClick: collection.onSchemaAnalyzerClick.bind(collection),

View File

@ -195,7 +195,6 @@ function configureEmulator(explorerParams: ExplorerParams): Explorer {
authType: AuthType.MasterKey,
});
const explorer = new Explorer(explorerParams);
explorer.isAccountReady(true);
return explorer;
}

View File

@ -54,7 +54,6 @@
"./src/Explorer/Notebook/NotebookComponent/loadTransform.ts",
"./src/Explorer/Notebook/NotebookComponent/reducers.ts",
"./src/Explorer/Notebook/NotebookComponent/types.ts",
"./src/Explorer/Notebook/NotebookContentClient.ts",
"./src/Explorer/Notebook/NotebookContentItem.ts",
"./src/Explorer/Notebook/NotebookRenderer/AzureTheme.tsx",
"./src/Explorer/Notebook/NotebookRenderer/Prompt.tsx",