diff --git a/docs/remove-notebooks-plan.md b/docs/remove-notebooks-plan.md index 6070eaf3e..0efae7411 100644 --- a/docs/remove-notebooks-plan.md +++ b/docs/remove-notebooks-plan.md @@ -235,7 +235,39 @@ engine deleted there. No separate work remains for this phase. pinned-repo methods. - Remove GitHub-related localization keys from **all** locale files (`en` + non-English). -### Phase 5 — Remove Phoenix and the notebook container/allocation core +### Phase 5 — Remove Phoenix and the notebook container/allocation core ✅ COMPLETED +> **Status:** Implemented on branch `users/jawelton/removenotebooks-phase5-062926`. Full +> verification sweep is green: `compile`, `compile:strict`, `lint` (0 errors), `format:check`, +> `test` (1894 passing), and `build:ci` (webpack) all pass. +> +> **Implementation notes / deviations:** +> - **`useNotebook.ts` was slim-renamed, not just deleted.** Its only surviving non-notebook +> state — `isSynapseLinkUpdating` (used by the Synapse Link enable flow + command bar) — was +> extracted into a new minimal store `src/Explorer/useSynapseLink.ts`; everything else +> (Phoenix/container/notebook state, `sparkClusterConnectionInfo`) was dropped. +> - **`ResourceTreeAdapter.tsx` (+ test + snapshot) was fully deleted**, along with its +> `Explorer.tsx` field/instantiation — after the "My Notebooks" tree removal it was entirely +> notebook-only/dead (its last consumers were the deleted `NotebookManager` and `Explorer`). +> - **Dead Phoenix-container command-bar UI removed:** `ConnectionStatusComponent` / +> `MemoryTrackerComponent` (+`.less`, + `Main.tsx` imports), `CommandBarUtil.createConnectionStatus` +> / `createMemoryTracker`, and the `CommandBarComponentAdapter` connection-status branch +> (they only rendered when `isPhoenix*` was true, now always false). +> - **`isShellEnabled` simplified to `enableCloudShell`** in `CommandBarComponentButtonFactory`, +> `ContextMenuButtonFactory`, and `SplashScreen` (it derived from `isPhoenixFeatures`, always +> false). `createOpenTerminalButtonByKind`'s disable logic was rebased onto `enableCloudShell`. +> - **Other dead survivors removed:** `useSelectedNode.isConnectedToContainer` (no consumers), +> `useTabs.closeAllNotebookTabs` (only caller was the deleted `useNotebook`), the dead +> `isNotebookEnabled` pass-through param in `treeNodeUtil`, and the stale `useNotebook` import +> in `Collection.ts`. +> - **`useKnockoutExplorer.ts`:** removed the Phoenix MPAC flag block and the +> PhoenixNotebooks/PhoenixFeatures/NotebooksDownBanner flight assignments (the +> `userContext.features.*` field definitions in `extractFeatures.ts` stay for Phase 6). +> - **Strict config:** removed `NotebookContentItem.ts` and `useNotebookSnapshotStore.ts`; added +> the new `useSynapseLink.ts`. +> - **Phase 6 boundary held:** `Juno`, the `cosmosNotebooks` ARM client, `Constants.Notebook`, +> notebook `DataModels`/`ViewModels`/`ActionContracts`, telemetry actions, and locale strings +> were left untouched. + - Delete `src/Phoenix/`, `src/Explorer/Notebook/NotebookContainerClient.ts`, `src/Explorer/Notebook/NotebookManager.tsx`, `src/Explorer/Notebook/useNotebook.ts`, `src/Explorer/Notebook/NotebookContentItem.ts`, `src/Explorer/Notebook/NotebookUtil.ts` diff --git a/src/Explorer/ContextMenuButtonFactory.tsx b/src/Explorer/ContextMenuButtonFactory.tsx index 59c49e5ac..f1e85d6cd 100644 --- a/src/Explorer/ContextMenuButtonFactory.tsx +++ b/src/Explorer/ContextMenuButtonFactory.tsx @@ -28,7 +28,6 @@ import { userContext } from "../UserContext"; import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils"; import { useSidePanel } from "../hooks/useSidePanel"; 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"; @@ -120,23 +119,17 @@ export const createCollectionContextMenuButton = ( iconSrc: HostedTerminalIcon, onClick: () => { const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); - if (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) { + if (userContext.features.enableCloudShell) { container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); } else { selectedCollection && selectedCollection.onNewMongoShellClick(); } }, - label: - useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell - ? t(Keys.contextMenu.openMongoShell) - : t(Keys.contextMenu.newShell), + label: userContext.features.enableCloudShell ? t(Keys.contextMenu.openMongoShell) : t(Keys.contextMenu.newShell), }); } - if ( - (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) && - userContext.apiType === "Cassandra" - ) { + if (userContext.features.enableCloudShell && userContext.apiType === "Cassandra") { items.push({ iconSrc: HostedTerminalIcon, onClick: () => { diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index dd1d8153b..2384c25be 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -109,29 +109,15 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { - "_isInitializingNotebooks": false, "databasesRefreshed": Promise {}, "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], - "phoenixClient": PhoenixClient { - "armResourceId": undefined, - "retryOptions": { - "maxTimeout": 5000, - "minTimeout": 5000, - "retries": 3, - }, - }, "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], - "resourceTree": ResourceTreeAdapter { - "container": [Circular], - "parameters": [Function], - }, }, "dataMaskingPolicy": [Function], "databaseId": "test", @@ -230,29 +216,15 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { - "_isInitializingNotebooks": false, "databasesRefreshed": Promise {}, "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], - "phoenixClient": PhoenixClient { - "armResourceId": undefined, - "retryOptions": { - "maxTimeout": 5000, - "minTimeout": 5000, - "retries": 3, - }, - }, "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], - "resourceTree": ResourceTreeAdapter { - "container": [Circular], - "parameters": [Function], - }, }, "dataMaskingPolicy": [Function], "databaseId": "test", @@ -456,29 +428,15 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { - "_isInitializingNotebooks": false, "databasesRefreshed": Promise {}, "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], - "phoenixClient": PhoenixClient { - "armResourceId": undefined, - "retryOptions": { - "maxTimeout": 5000, - "minTimeout": 5000, - "retries": 3, - }, - }, "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], - "resourceTree": ResourceTreeAdapter { - "container": [Circular], - "parameters": [Function], - }, }, "dataMaskingPolicy": [Function], "databaseId": "test", @@ -527,29 +485,15 @@ exports[`SettingsComponent renders 1`] = ` } explorer={ Explorer { - "_isInitializingNotebooks": false, "databasesRefreshed": Promise {}, "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], - "phoenixClient": PhoenixClient { - "armResourceId": undefined, - "retryOptions": { - "maxTimeout": 5000, - "minTimeout": 5000, - "retries": 3, - }, - }, "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], - "resourceTree": ResourceTreeAdapter { - "container": [Circular], - "parameters": [Function], - }, } } isReadOnly={false} @@ -695,29 +639,15 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { - "_isInitializingNotebooks": false, "databasesRefreshed": Promise {}, "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], - "phoenixClient": PhoenixClient { - "armResourceId": undefined, - "retryOptions": { - "maxTimeout": 5000, - "minTimeout": 5000, - "retries": 3, - }, - }, "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], - "resourceTree": ResourceTreeAdapter { - "container": [Circular], - "parameters": [Function], - }, }, "dataMaskingPolicy": [Function], "databaseId": "test", @@ -766,29 +696,15 @@ exports[`SettingsComponent renders 1`] = ` } explorer={ Explorer { - "_isInitializingNotebooks": false, "databasesRefreshed": Promise {}, "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], - "phoenixClient": PhoenixClient { - "armResourceId": undefined, - "retryOptions": { - "maxTimeout": 5000, - "minTimeout": 5000, - "retries": 3, - }, - }, "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], - "resourceTree": ResourceTreeAdapter { - "container": [Circular], - "parameters": [Function], - }, } } /> diff --git a/src/Explorer/Explorer.test.tsx b/src/Explorer/Explorer.test.tsx index 5a4e89b94..26bafc4b1 100644 --- a/src/Explorer/Explorer.test.tsx +++ b/src/Explorer/Explorer.test.tsx @@ -26,9 +26,9 @@ jest.mock("./Controls/Dialog", () => ({ }, })); -// Silence useNotebook subscription calls -jest.mock("./Notebook/useNotebook", () => ({ - useNotebook: { +// Silence useSynapseLink state calls +jest.mock("./useSynapseLink", () => ({ + useSynapseLink: { subscribe: jest.fn(), getState: jest.fn().mockReturnValue( new Proxy( diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index bcacb8d65..49b6f78b0 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -1,9 +1,6 @@ import * as msal from "@azure/msal-browser"; -import { Link } from "@fluentui/react/lib/Link"; -import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { sendMessage } from "Common/MessageHandler"; import { stringifyError } from "Common/stringifyError"; -import { Platform, configContext } from "ConfigContext"; import { MessageTypes } from "Contracts/ExplorerContracts"; import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; import { @@ -13,32 +10,27 @@ import { scheduleRefreshFabricToken, } from "Platform/Fabric/FabricUtil"; import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils"; -import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils"; import { featureRegistered } from "Utils/FeatureRegistrationUtils"; import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts"; import * as ko from "knockout"; import React from "react"; import _ from "underscore"; -import shallow from "zustand/shallow"; import { AuthType } from "../AuthType"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import * as Constants from "../Common/Constants"; -import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants"; import { getErrorMessage, getErrorStack } from "../Common/ErrorHandlingUtils"; import * as Logger from "../Common/Logger"; import { QueriesClient } from "../Common/QueriesClient"; import { readCollection } from "../Common/dataAccess/readCollection"; import { readDatabases } from "../Common/dataAccess/readDatabases"; import * as DataModels from "../Contracts/DataModels"; -import { ContainerConnectionInfo, IPhoenixServiceInfo, IProvisionData, IResponse } from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; import { UploadDetailsRecord } from "../Contracts/ViewModels"; import MetricScenario from "../Metrics/MetricEvents"; import { ApplicationMetricPhase } from "../Metrics/ScenarioConfig"; import { scenarioMonitor } from "../Metrics/ScenarioMonitor"; -import { PhoenixClient } from "../Phoenix/PhoenixClient"; import * as ExplorerSettings from "../Shared/ExplorerSettings"; -import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; +import { Action } from "../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import { updateUserContext, userContext } from "../UserContext"; import { getCollectionName, getUploadName } from "../Utils/APITypeUtils"; @@ -49,8 +41,6 @@ import { ReactTabKind, useTabs } from "../hooks/useTabs"; import "./ComponentRegisterer"; import { DialogProps, useDialog } from "./Controls/Dialog"; import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter"; -import type NotebookManager from "./Notebook/NotebookManager"; -import { useNotebook } from "./Notebook/useNotebook"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; @@ -60,10 +50,10 @@ import TabsBase from "./Tabs/TabsBase"; import TerminalTab from "./Tabs/TerminalTab"; import Database from "./Tree/Database"; import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; -import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; import StoredProcedure from "./Tree/StoredProcedure"; import { useDatabases } from "./useDatabases"; import { useSelectedNode } from "./useSelectedNode"; +import { useSynapseLink } from "./useSynapseLink"; BindingHandlersRegisterer.registerBindingHandlers(); @@ -72,19 +62,10 @@ export default class Explorer { public queriesClient: QueriesClient; public tableDataClient: TableDataClient; - // Resource Tree - private resourceTree: ResourceTreeAdapter; - // Tabs public isTabsContentExpanded: ko.Observable; - // Notebooks - public notebookManager?: NotebookManager; - - private _isInitializingNotebooks: boolean; - private static readonly MaxNbDatabasesToAutoExpand = 5; - public phoenixClient: PhoenixClient; /** * Resolves when the initial refreshAllDatabases (including collection loading) completes. @@ -95,13 +76,6 @@ export default class Explorer { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { dataExplorerArea: Constants.Areas.ResourceTree, }); - this._isInitializingNotebooks = false; - - this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id); - useNotebook.subscribe( - () => this.refreshCommandBarButtons(), - (state) => state.isNotebooksEnabledForAccount, - ); this.queriesClient = new QueriesClient(this); @@ -156,31 +130,6 @@ export default class Explorer { startKey, ); - useNotebook.subscribe( - async () => this.initiateAndRefreshNotebookList(), - (state) => [state.isNotebookEnabled, state.isRefreshed], - shallow, - ); - - this.resourceTree = new ResourceTreeAdapter(this); - - // Override notebook server parameters from URL parameters - if ( - userContext.features.notebookServerUrl && - validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) && - userContext.features.notebookServerToken - ) { - useNotebook.getState().setNotebookServerInfo({ - notebookServerEndpoint: userContext.features.notebookServerUrl, - authToken: userContext.features.notebookServerToken, - forwardingId: undefined, - }); - } - - if (userContext.features.notebookBasePath) { - useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath); - } - if (isFabricMirrored()) { useTabs.getState().closeReactTab(ReactTabKind.Home); } @@ -188,23 +137,6 @@ export default class Explorer { this.refreshExplorer(); } - public async initiateAndRefreshNotebookList(): Promise { - if (!this.notebookManager) { - const NotebookManager = (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(); - } - public openEnableSynapseLinkDialog(targetAccountOverride?: DataModels.AccountOverride): void { const subscriptionId = targetAccountOverride?.subscriptionId ?? userContext.subscriptionId; const resourceGroup = targetAccountOverride?.resourceGroup ?? userContext.resourceGroup; @@ -227,7 +159,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.", ); - useNotebook.getState().setIsSynapseLinkUpdating(true); + useSynapseLink.getState().setIsSynapseLinkUpdating(true); useDialog.getState().closeDialog(); try { @@ -248,7 +180,7 @@ export default class Explorer { logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`); TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, {}, startTime); } finally { - useNotebook.getState().setIsSynapseLinkUpdating(false); + useSynapseLink.getState().setIsSynapseLinkUpdating(false); } }, @@ -431,7 +363,6 @@ export default class Explorer { await (userContext.authType === AuthType.ResourceToken ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases()); - await this.refreshNotebookList(); } logConsoleInfo("Successfully refreshed databases"); @@ -442,108 +373,6 @@ export default class Explorer { window.open(Constants.Urls.feedbackEmail, "_blank"); }; - public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise { - if (!databaseAccount) { - throw new Error("No database account specified"); - } - - if (this._isInitializingNotebooks) { - return; - } - this._isInitializingNotebooks = true; - this.refreshNotebookList(); - this._isInitializingNotebooks = false; - } - - public async allocateContainer(): Promise { - const notebookServerInfo = useNotebook.getState().notebookServerInfo; - const isAllocating = useNotebook.getState().isAllocating; - if ( - isAllocating === false && - (notebookServerInfo === undefined || - (notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined)) - ) { - const connectionStatus: ContainerConnectionInfo = { - status: ConnectionStatusType.Connecting, - }; - - useNotebook.getState().setConnectionInfo(connectionStatus); - - let connectionInfo; - try { - TelemetryProcessor.traceStart(Action.PhoenixConnection, { - dataExplorerArea: Areas.Notebook, - }); - useNotebook.getState().setIsAllocating(true); - const provisionData: IProvisionData = { - cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint, - poolId: undefined, - }; - connectionInfo = await this.phoenixClient.allocateContainer(provisionData); - if (!connectionInfo?.data?.phoenixServiceUrl) { - throw new Error(`PhoenixServiceUrl is invalid!`); - } - await this.setNotebookInfo(connectionInfo, connectionStatus); - TelemetryProcessor.traceSuccess(Action.PhoenixConnection, { - dataExplorerArea: Areas.Notebook, - }); - } catch (error) { - TelemetryProcessor.traceFailure(Action.PhoenixConnection, { - dataExplorerArea: Areas.Notebook, - status: error.status, - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }); - connectionStatus.status = ConnectionStatusType.Failed; - useNotebook.getState().resetContainerConnection(connectionStatus); - if (error?.status === HttpStatusCodes.Forbidden && error.message) { - useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`); - } else { - useDialog - .getState() - .showOkModalDialog( - "Connection Failed", - "We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket.", - ); - } - throw error; - } finally { - useNotebook.getState().setIsAllocating(false); - this.refreshCommandBarButtons(); - this.refreshNotebookList(); - this._isInitializingNotebooks = false; - } - } - } - - public async setNotebookInfo( - connectionInfo: IResponse, - connectionStatus: DataModels.ContainerConnectionInfo, - ): Promise { - const containerData = { - forwardingId: connectionInfo.data.forwardingId, - dbAccountName: userContext.databaseAccount.name, - }; - await this.phoenixClient.initiateContainerHeartBeat(true, containerData); - - connectionStatus.status = ConnectionStatusType.Connected; - useNotebook.getState().setConnectionInfo(connectionStatus); - - const noteBookServerInfo = { - notebookServerEndpoint: - (validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) && - userContext.features.notebookServerUrl) || - connectionInfo.data.phoenixServiceUrl, - authToken: userContext.features.notebookServerToken || connectionInfo.data.authToken, - forwardingId: connectionInfo.data.forwardingId, - }; - useNotebook.getState().setNotebookServerInfo(noteBookServerInfo); - - this.notebookManager?.notebookClient - .getMemoryUsage() - .then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo)); - } - private getDeltaDatabases( updatedDatabaseList: DataModels.Database[], databases: ViewModels.Database[], @@ -637,11 +466,6 @@ export default class Explorer { } } - private refreshNotebookList = (): Promise => { - // Notebook authoring and listing have been removed. - return Promise.resolve(); - }; - public openNotebookTerminal(kind: ViewModels.TerminalKind): void { this.connectToNotebookTerminal(kind); } @@ -738,24 +562,6 @@ export default class Explorer { .openSidePanel("Input parameters", ); } - public getDownloadModalContent(fileName: string): JSX.Element { - if (useNotebook.getState().isPhoenixNotebooks) { - return ( - <> -

{Notebook.galleryNotebookDownloadContent1}

-
-

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

- - ); - } - return

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

; - } - public async refreshExplorer(): Promise { // Start DatabaseLoad scenario before fetching databases if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") { @@ -764,8 +570,7 @@ export default class Explorer { // Run independent initialization tasks in parallel: // - Database loading (ARM/SDK calls for databases + collections) - // - Notebook enabled check (Phoenix + Portal backend — no dependency on databases) - // - Feature registration check (ARM call — no dependency on databases or notebooks) + // - Feature registration check (ARM call — no dependency on databases) const databasesTask = userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" ? (async () => { @@ -782,37 +587,12 @@ export default class Explorer { })() : Promise.resolve(); - const notebooksTask = !isFabricNative() - ? useNotebook.getState().refreshNotebooksEnabledStateForAccount() - : Promise.resolve(); - const featureRegistrationTask = userContext.authType === AuthType.AAD && userContext.apiType === "SQL" && !isFabricNative() ? featureRegistered(userContext.subscriptionId, "ThroughputBucketing") : Promise.resolve(false); - const [, , throughputBucketsEnabled] = await Promise.all([databasesTask, notebooksTask, featureRegistrationTask]); - - // Notebook initialization depends on refreshNotebooksEnabledStateForAccount completing above - // TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount - const isNotebookEnabled = - configContext.platform !== Platform.Fabric && - (userContext.features.notebooksDownBanner || - useNotebook.getState().isPhoenixNotebooks || - useNotebook.getState().isPhoenixFeatures); - useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled); - useNotebook - .getState() - .setIsShellEnabled(useNotebook.getState().isPhoenixFeatures && isPublicInternetAccessAllowed()); - - TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, { - isNotebookEnabled, - dataExplorerArea: Constants.Areas.Notebook, - }); - - if (useNotebook.getState().isPhoenixNotebooks) { - await this.initNotebooks(userContext.databaseAccount); - } + const [, throughputBucketsEnabled] = await Promise.all([databasesTask, featureRegistrationTask]); if (throughputBucketsEnabled) { updateUserContext({ throughputBucketsEnabled }); diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index 4d2326f3f..022174a40 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -5,14 +5,12 @@ */ import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; import { makeStyles, useFluent } from "@fluentui/react-components"; -import { useNotebook } from "Explorer/Notebook/useNotebook"; import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; import { isFabric } from "Platform/Fabric/FabricUtil"; import { userContext } from "UserContext"; import * as React from "react"; import create, { UseStore } from "zustand"; -import { ConnectionStatusType } from "../../../Common/Constants"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import Explorer from "../../Explorer"; import { useSelectedNode } from "../../useSelectedNode"; @@ -75,12 +73,6 @@ export const CommandBar: React.FC = ({ container }: Props) => { const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR); const styles = useStyles(); - const { connectionInfo, isPhoenixNotebooks, isPhoenixFeatures } = useNotebook((state) => ({ - connectionInfo: state.connectionInfo, - isPhoenixNotebooks: state.isPhoenixNotebooks, - isPhoenixFeatures: state.isPhoenixFeatures, - })); - // Subscribe to the store changes that affect button creation const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled); const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated); @@ -134,11 +126,6 @@ export const CommandBar: React.FC = ({ container }: Props) => { const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, "var(--colorNeutralBackground1)"); uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); - // Add connection status if needed (using the hook values we got at the top level) - if ((isPhoenixNotebooks || isPhoenixFeatures) && connectionInfo?.status !== ConnectionStatusType.Connect) { - uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus")); - } - const rootStyle = { root: { backgroundColor: "var(--colorNeutralBackground1)", diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts index 12831e3d7..7e0807d0a 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts @@ -4,7 +4,6 @@ import { DatabaseAccount } from "../../../Contracts/DataModels"; import { CollectionBase } from "../../../Contracts/ViewModels"; import { updateUserContext } from "../../../UserContext"; import Explorer from "../../Explorer"; -import { useNotebook } from "../../Notebook/useNotebook"; import { useDatabases } from "../../useDatabases"; import { useSelectedNode } from "../../useSelectedNode"; import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory"; @@ -99,11 +98,6 @@ describe("CommandBarComponentButtonFactory tests", () => { }); }); - afterEach(() => { - useNotebook.getState().setIsNotebookEnabled(false); - useNotebook.getState().setIsNotebooksEnabledForAccount(false); - }); - it("Cassandra Api not available - button should be hidden", () => { updateUserContext({ databaseAccount: { diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 8ab643e3a..33e068c7c 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -24,12 +24,12 @@ import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils"; import { useSidePanel } from "../../../hooks/useSidePanel"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import Explorer from "../../Explorer"; -import { useNotebook } from "../../Notebook/useNotebook"; import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane"; import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane"; import { SettingsPane, useDataPlaneRbac } from "../../Panes/SettingsPane/SettingsPane"; import { useDatabases } from "../../useDatabases"; import { SelectedNodeState } from "../../useSelectedNode"; +import { useSynapseLink } from "../../useSynapseLink"; import { ThemeToggleButton } from "./ThemeToggleButton"; let counter = 0; @@ -121,14 +121,13 @@ export function createContextCommandBarButtons( const buttons: CommandButtonComponentProps[] = []; if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") { - const label = - useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell ? "Open Mongo Shell" : "New Shell"; + const label = userContext.features.enableCloudShell ? "Open Mongo Shell" : "New Shell"; const newMongoShellBtn: CommandButtonComponentProps = { iconSrc: HostedTerminalIcon, iconAlt: label, onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); - if (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) { + if (userContext.features.enableCloudShell) { container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); } else { selectedCollection && selectedCollection.onNewMongoShellClick(); @@ -142,7 +141,7 @@ export function createContextCommandBarButtons( } if ( - (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) && + userContext.features.enableCloudShell && !selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Cassandra" ) { @@ -258,7 +257,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo onCommandClick: () => container.openEnableSynapseLinkDialog(), commandButtonLabel: label, hasPopup: false, - disabled: useNotebook.getState().isSynapseLinkUpdating, + disabled: useSynapseLink.getState().isSynapseLinkUpdating, ariaLabel: label, }; } @@ -499,13 +498,12 @@ function createOpenTerminalButtonByKind( const label = `Open ${terminalFriendlyName()} shell`; const tooltip = "This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."; - const disableButton = - !useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled; + const disableButton = !userContext.features.enableCloudShell; return { iconSrc: HostedTerminalIcon, iconAlt: label, onCommandClick: () => { - if (useNotebook.getState().isNotebookEnabled || userContext.features.enableCloudShell) { + if (userContext.features.enableCloudShell) { container.openNotebookTerminal(terminalKind); } }, diff --git a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx index 40628db28..b298be795 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx @@ -17,9 +17,6 @@ import { configContext, Platform } from "../../../ConfigContext"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; -import Explorer from "../../Explorer"; -import { ConnectionStatus } from "./ConnectionStatusComponent"; -import { MemoryTracker } from "./MemoryTrackerComponent"; /** * Convert our NavbarButtonConfig to UI Fabric buttons @@ -258,20 +255,6 @@ export const createDivider = (key: string): ICommandBarItemProps => { }; }; -export const createMemoryTracker = (key: string): ICommandBarItemProps => { - return { - key, - onRender: () => , - }; -}; - -export const createConnectionStatus = (container: Explorer, key: string): ICommandBarItemProps => { - return { - key, - onRender: () => , - }; -}; - export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap { const handlers: KeyboardHandlerMap = {}; diff --git a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less deleted file mode 100644 index 0688e0baa..000000000 --- a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less +++ /dev/null @@ -1,184 +0,0 @@ -@import "../../../../less/Common/Constants"; - -.connectionStatusContainer { - cursor: default; - align-items: center; - border: 1px; - min-height: 44px; - - > span { - padding-right: 12px; - font-size: 12px; - font-family: @DataExplorerFont; - color: @DefaultFontColor; - } - &:focus{ - outline: 0px; - } -} -.commandReactBtn { - &:hover { - background-color: rgb(238, 247, 255); - color: rgb(32, 31, 30); - cursor: pointer; - } - &:focus{ - outline: 1px dashed #605e5c; - } -} -.connectedReactBtn { - &:hover { - background-color: rgb(238, 247, 255); - color: rgb(32, 31, 30); - cursor: pointer; - } - &:focus{ - outline: 0px; - } -} -.connectIcon{ - margin: 0px 4px; - height: 18px; - width: 18px; - color: rgb(0, 120, 212); -} - .status { - position: relative; - display: block; - margin-right: 8px; - width: 1em; - height: 1em; - font-size: 9px!important; - padding: 0px!important; - border-radius: 0.5em; - } - - .status::before, - .status::after { - position: absolute; - content: ""; - } - - .status::before { - top: 0; - left: 0; - width: 1em; - height: 1em; - background-color: rgba(#fff, 0.1); - border-radius: 100%; - opacity: 1; - transform: translate3d(0, 0, 0) scale(0); - } - - .connected{ - background-color: green; - box-shadow: - 0 0 0 0em rgba(green, 0), - 0em 0.05em 0.1em rgba(#000000, 0.2); - transform: translate3d(0, 0, 0) scale(1); - } - .connecting{ - background-color:#ffbf00; - box-shadow: - 0 0 0 0em rgba(#ffbf00, 0), - 0em 0.05em 0.1em rgba(#000000, 0.2); - transform: translate3d(0, 0, 0) scale(1); - } - .failed{ - background-color:#bd1919; - box-shadow: - 0 0 0 0em rgba(#bd1919, 0), - 0em 0.05em 0.1em rgba(#000000, 0.2); - transform: translate3d(0, 0, 0) scale(1); - } - - .status.connecting.is-animating { - animation: status-outer-connecting 3000ms infinite; - } - .status.failed.is-animating { - animation: status-outer-failed 3000ms infinite; - } - .status.connected.is-animating { - animation: status-outer-connected 3000ms infinite; - } - @keyframes status-outer-connected { - - 0% { - transform: translate3d(0, 0, 0) scale(1); - box-shadow: 0 0 0 0em #008000, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2); - } - 20% { - transform: translate3d(0, 0, 0) scale(1); - box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.6), 0em 0.05em 0.1em rgba(0, 0, 0, 0.5); - } - 40% { - transform: translate3d(0, 0, 0) scale(1); - box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.5), 0em 0.05em 0.1em rgba(0, 0, 0, 0.4); - } - 60% { - transform: translate3d(0, 0, 0) scale(1); - box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3); - } - 80% { - transform: translate3d(0, 0, 0) scale(1); - box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1); - } - 85% { - transform: translate3d(0, 0, 0) scale(1); - box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0); - } - } - @keyframes status-outer-failed { - - 0% { - transform: translate3d(0, 0, 0) scale(1); - box-shadow: 0 0 0 0em #bd1919, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2); - } - 20% { - transform: translate3d(0, 0, 0) scale(1); - box-shadow: 0 0 0 0em #c52d2d, 0em 0.05em 0.1em rgba(0, 0, 0, 0.5); - } - 40% { - transform: translate3d(0, 0, 0) scale(1); - box-shadow: 0 0 0 0em #b47b7b, 0em 0.05em 0.1em rgba(0, 0, 0, 0.4); - } - 60% { - transform: translate3d(0, 0, 0) scale(1); - box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3); - } - 80% { - transform: translate3d(0, 0, 0) scale(1); - box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1); - } - 85% { - transform: translate3d(0, 0, 0) scale(1); - box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0); - } - } - @keyframes status-outer-connecting { - - 0% { - transform: translate3d(0, 0, 0) scale(1); - box-shadow: 0 0 0 0em #ffbf00, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2); - } - 20% { - transform: translate3d(0, 0, 0) scale(1); - box-shadow: 0 0 0 0em #f0dfad, 0em 0.05em 0.1em rgba(0, 0, 0, 0.5); - } - 40% { - transform: translate3d(0, 0, 0) scale(1); - box-shadow: 0 0 0 0em rgba(198, 243, 198, 0.5), 0em 0.05em 0.1em rgba(0, 0, 0, 0.4); - } - 60% { - transform: translate3d(0, 0, 0) scale(1); - box-shadow: 0 0 0 0em rgba(213, 241, 213, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3); - } - 80% { - transform: translate3d(0, 0, 0) scale(1); - box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1); - } - 85% { - transform: translate3d(0, 0, 0) scale(1); - box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0); - } - } \ No newline at end of file diff --git a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx deleted file mode 100644 index 60499661f..000000000 --- a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import { - FocusTrapCallout, - FocusZone, - FocusZoneTabbableElements, - FontWeights, - Icon, - mergeStyleSets, - ProgressIndicator, - Stack, - Text, - TooltipHost, -} from "@fluentui/react"; -import { useId } from "@fluentui/react-hooks"; -import { ActionButton, DefaultButton } from "@fluentui/react/lib/Button"; -import * as React from "react"; -import "../../../../less/hostedexplorer.less"; -import { ConnectionStatusType, ContainerStatusType, Notebook } from "../../../Common/Constants"; -import Explorer from "../../Explorer"; -import { useNotebook } from "../../Notebook/useNotebook"; -import "../CommandBar/ConnectionStatusComponent.less"; -interface Props { - container: Explorer; -} -export const ConnectionStatus: React.FC = ({ container }: Props): JSX.Element => { - const connectionInfo = useNotebook((state) => state.connectionInfo); - const [second, setSecond] = React.useState("00"); - const [minute, setMinute] = React.useState("00"); - const [isActive, setIsActive] = React.useState(false); - const [counter, setCounter] = React.useState(0); - const [statusColor, setStatusColor] = React.useState(""); - const [toolTipContent, setToolTipContent] = React.useState("Connect to temporary workspace."); - const [isBarDismissed, setIsBarDismissed] = React.useState(false); - const buttonId = useId("callout-button"); - const containerInfo = useNotebook((state) => state.containerStatus); - - const styles = mergeStyleSets({ - callout: { - width: 320, - padding: "20px 24px", - }, - title: { - marginBottom: 12, - fontWeight: FontWeights.semilight, - }, - buttons: { - display: "flex", - justifyContent: "flex-end", - marginTop: 20, - }, - }); - - React.useEffect(() => { - let intervalId: NodeJS.Timeout; - - if (isActive) { - intervalId = setInterval(() => { - const secondCounter = counter % 60; - const minuteCounter = Math.floor(counter / 60); - const computedSecond: string = String(secondCounter).length === 1 ? `0${secondCounter}` : `${secondCounter}`; - const computedMinute: string = String(minuteCounter).length === 1 ? `0${minuteCounter}` : `${minuteCounter}`; - - setSecond(computedSecond); - setMinute(computedMinute); - - setCounter((counter) => counter + 1); - }, 1000); - } - return () => clearInterval(intervalId); - }, [isActive, counter]); - - React.useEffect(() => { - if (connectionInfo?.status === ConnectionStatusType.Reconnect) { - setToolTipContent("Click here to Reconnect to temporary workspace."); - } else if (connectionInfo?.status === ConnectionStatusType.Failed) { - setStatusColor("status failed is-animating"); - setToolTipContent("Click here to Reconnect to temporary workspace."); - } - }, [connectionInfo.status]); - - const stopTimer = () => { - setIsActive(false); - setCounter(0); - setSecond("00"); - setMinute("00"); - }; - - const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo); - const totalGB = memoryUsageInfo ? memoryUsageInfo.totalKB / Notebook.memoryGuageToGB : 0; - const usedGB = totalGB > 0 ? totalGB - memoryUsageInfo.freeKB / Notebook.memoryGuageToGB : 0; - - if ( - connectionInfo && - (connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.Reconnect) - ) { - return ( - container.allocateContainer()}> - - - - {connectionInfo.status} - - - - ); - } - - if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) { - stopTimer(); - setIsActive(true); - setStatusColor("status connecting is-animating"); - setToolTipContent("Connecting to temporary workspace."); - } else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) { - stopTimer(); - setStatusColor("status connected is-animating"); - setToolTipContent("Connected to temporary workspace."); - } else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) { - stopTimer(); - setStatusColor("status failed is-animating"); - setToolTipContent("Click here to Reconnect to temporary workspace."); - } - return ( - <> - - ) => - connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault() - } - > - - - - {connectionInfo.status} - - {connectionInfo.status === ConnectionStatusType.Connecting && isActive && ( - - )} - {connectionInfo.status === ConnectionStatusType.Connected && !isActive && ( - 0.8 ? "lowMemory" : ""} - description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"} - percentComplete={totalGB !== 0 ? usedGB / totalGB : 0} - /> - )} - - {!isBarDismissed && - containerInfo.status && - containerInfo.status === ContainerStatusType.Active && - Math.round(containerInfo.durationLeftInMinutes) <= Notebook.remainingTimeForAlert ? ( - setIsBarDismissed(true)} - setInitialFocus - > - - Remaining Time - - - This temporary workspace will get disconnected in {Math.round(containerInfo.durationLeftInMinutes)}{" "} - minutes. To save your work permanently, save your notebooks to a GitHub repository or download the - notebooks to your local machine before the session ends. - - - - setIsBarDismissed(true)}>Dimiss - - - - ) : undefined} - - - - ); -}; diff --git a/src/Explorer/Menus/CommandBar/MemoryTrackerComponent.less b/src/Explorer/Menus/CommandBar/MemoryTrackerComponent.less deleted file mode 100644 index 404025a91..000000000 --- a/src/Explorer/Menus/CommandBar/MemoryTrackerComponent.less +++ /dev/null @@ -1,24 +0,0 @@ -@import "../../../../less/Common/Constants"; - -.memoryTrackerContainer { - cursor: default; - align-items: center; - margin: 0 9px; - border: 1px; - - > span { - padding-right: 12px; - font-size: 13px; - font-family: @DataExplorerFont; - color: @DefaultFontColor; - } - - > .lowMemory { - .ms-ProgressIndicator-progressBar { - background-color: @SelectionHigh; - } - .ms-ProgressIndicator-itemDescription { - color: @SelectionHigh; - } - } -} \ No newline at end of file diff --git a/src/Explorer/Menus/CommandBar/MemoryTrackerComponent.tsx b/src/Explorer/Menus/CommandBar/MemoryTrackerComponent.tsx deleted file mode 100644 index ac0621897..000000000 --- a/src/Explorer/Menus/CommandBar/MemoryTrackerComponent.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { ProgressIndicator, Spinner, SpinnerSize, Stack } from "@fluentui/react"; -import * as React from "react"; -import { useNotebook } from "../../Notebook/useNotebook"; - -export const MemoryTracker: React.FC = (): JSX.Element => { - const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo); - if (!memoryUsageInfo) { - return ( - - Memory - - - ); - } - - const totalGB = memoryUsageInfo.totalKB / 1048576; - const usedGB = totalGB - memoryUsageInfo.freeKB / 1048576; - - return ( - - Memory - 0.8 ? "lowMemory" : ""} - description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"} - percentComplete={usedGB / totalGB} - /> - - ); -}; diff --git a/src/Explorer/Notebook/NotebookContainerClient.ts b/src/Explorer/Notebook/NotebookContainerClient.ts deleted file mode 100644 index 76d4ba2b8..000000000 --- a/src/Explorer/Notebook/NotebookContainerClient.ts +++ /dev/null @@ -1,195 +0,0 @@ -/** - * Notebook container related stuff - */ -import { useDialog } from "Explorer/Controls/Dialog"; -import promiseRetry, { AbortError, Options } from "p-retry"; -import { PhoenixClient } from "Phoenix/PhoenixClient"; -import * as Constants from "../../Common/Constants"; -import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants"; -import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; -import * as Logger from "../../Common/Logger"; -import * as DataModels from "../../Contracts/DataModels"; -import { IPhoenixServiceInfo, IProvisionData, IResponse } from "../../Contracts/DataModels"; -import { userContext } from "../../UserContext"; -import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; -import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; -import { useNotebook } from "./useNotebook"; - -export class NotebookContainerClient { - private clearReconnectionAttemptMessage? = () => {}; - private isResettingWorkspace: boolean; - private phoenixClient: PhoenixClient; - private retryOptions: Options; - private scheduleTimerId: NodeJS.Timeout; - - constructor(private onConnectionLost: () => void) { - this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id); - this.retryOptions = { - retries: Notebook.retryAttempts, - maxTimeout: Notebook.retryAttemptDelayMs, - minTimeout: Notebook.retryAttemptDelayMs, - }; - - this.initHeartbeat(Constants.Notebook.heartbeatDelayMs); - } - - private initHeartbeat(delayMs: number): void { - this.scheduleHeartbeat(delayMs); - - useNotebook.subscribe( - () => this.scheduleHeartbeat(delayMs), - (state) => state.notebookServerInfo, - ); - } - - private scheduleHeartbeat(delayMs: number) { - if (this.scheduleTimerId) { - clearInterval(this.scheduleTimerId); - } - - const notebookServerInfo = useNotebook.getState().notebookServerInfo; - if (notebookServerInfo?.notebookServerEndpoint) { - this.scheduleTimerId = setInterval(async () => { - const notebookServerInfo = useNotebook.getState().notebookServerInfo; - if (notebookServerInfo?.notebookServerEndpoint) { - const memoryUsageInfo = await this.getMemoryUsage(); - useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo); - } - }, delayMs); - } - } - - public async getMemoryUsage(): Promise { - const notebookServerInfo = useNotebook.getState().notebookServerInfo; - if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) { - const error = "No server endpoint detected"; - Logger.logError(error, "NotebookContainerClient/getMemoryUsage"); - return Promise.reject(error); - } - - if (this.isResettingWorkspace) { - return undefined; - } - - const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig(); - try { - const runMemoryAsync = async () => { - return await this._getMemoryAsync(notebookServerEndpoint, authToken); - }; - return await promiseRetry(runMemoryAsync, this.retryOptions); - } catch (error) { - Logger.logError(getErrorMessage(error), "NotebookContainerClient/getMemoryUsage"); - if (!this.clearReconnectionAttemptMessage) { - this.clearReconnectionAttemptMessage = logConsoleProgress( - "Connection lost with Notebook server. Attempting to reconnect...", - ); - } - this.onConnectionLost(); - return undefined; - } - } - - private async _getMemoryAsync( - notebookServerEndpoint: string, - authToken: string, - ): Promise { - if (this.shouldExecuteMemoryCall()) { - const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, { - method: "GET", - headers: { - Authorization: authToken, - "content-type": "application/json", - }, - }); - if (response.ok) { - if (this.clearReconnectionAttemptMessage) { - this.clearReconnectionAttemptMessage(); - this.clearReconnectionAttemptMessage = undefined; - } - const memoryUsageInfo = await response.json(); - if (memoryUsageInfo) { - return { - totalKB: memoryUsageInfo.total, - freeKB: memoryUsageInfo.free, - }; - } - } else if (response.status === HttpStatusCodes.NotFound) { - throw new AbortError(response.statusText); - } - throw new Error(response.statusText); - } else { - return undefined; - } - } - - private shouldExecuteMemoryCall(): boolean { - return ( - useNotebook.getState().containerStatus?.status === Constants.ContainerStatusType.Active && - useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected - ); - } - - public async resetWorkspace(): Promise> { - this.isResettingWorkspace = true; - let response: IResponse; - try { - response = await this._resetWorkspace(); - } catch (error) { - Promise.reject(error); - return response; - } - this.isResettingWorkspace = false; - return response; - } - - private async _resetWorkspace(): Promise> { - const notebookServerInfo = useNotebook.getState().notebookServerInfo; - if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) { - const error = "No server endpoint detected"; - Logger.logError(error, "NotebookContainerClient/resetWorkspace"); - return Promise.reject(error); - } - - try { - if (useNotebook.getState().isPhoenixNotebooks) { - const provisionData: IProvisionData = { - cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint, - poolId: PoolIdType.DefaultPoolId, - }; - return await this.phoenixClient.resetContainer(provisionData); - } - return null; - } catch (error) { - Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace"); - if (error?.status === HttpStatusCodes.Forbidden && error.message) { - useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`); - } else { - useDialog - .getState() - .showOkModalDialog( - "Connection Failed", - "We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket.", - ); - } - throw error; - } - } - - private getNotebookServerConfig(): { notebookServerEndpoint: string; authToken: string } { - const notebookServerInfo = useNotebook.getState().notebookServerInfo; - const authToken: string = notebookServerInfo.authToken ? `Token ${notebookServerInfo.authToken}` : undefined; - - return { - notebookServerEndpoint: notebookServerInfo.notebookServerEndpoint, - authToken, - }; - } - - private getHeaders(): HeadersInit { - const authorizationHeader = getAuthorizationHeader(); - return { - [authorizationHeader.header]: authorizationHeader.token, - [HttpHeaders.contentType]: "application/json", - }; - } -} diff --git a/src/Explorer/Notebook/NotebookContentItem.ts b/src/Explorer/Notebook/NotebookContentItem.ts deleted file mode 100644 index bb79485b8..000000000 --- a/src/Explorer/Notebook/NotebookContentItem.ts +++ /dev/null @@ -1,14 +0,0 @@ -export interface NotebookContentItem { - name: string; - path: string; - type: NotebookContentItemType; - children?: NotebookContentItem[]; - parent?: NotebookContentItem; - timestamp?: number; -} - -export enum NotebookContentItemType { - Notebook, - File, - Directory, -} diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx deleted file mode 100644 index 175c76061..000000000 --- a/src/Explorer/Notebook/NotebookManager.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Contains all notebook related stuff meant to be dynamically loaded by explorer - */ - -import { JunoClient } from "../../Juno/JunoClient"; -import { userContext } from "../../UserContext"; -import Explorer from "../Explorer"; -import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; -import { NotebookContainerClient } from "./NotebookContainerClient"; - -export interface NotebookManagerOptions { - container: Explorer; - resourceTree: ResourceTreeAdapter; - refreshCommandBarButtons: () => void; - refreshNotebookList: () => void; -} - -export default class NotebookManager { - private params: NotebookManagerOptions; - public junoClient: JunoClient; - - public notebookClient: NotebookContainerClient; - - public initialize(params: NotebookManagerOptions): void { - this.params = params; - this.junoClient = new JunoClient(); - - this.notebookClient = new NotebookContainerClient(() => - this.params.container.initNotebooks(userContext?.databaseAccount), - ); - } -} diff --git a/src/Explorer/Notebook/NotebookUtil.test.ts b/src/Explorer/Notebook/NotebookUtil.test.ts deleted file mode 100644 index 97ff4514a..000000000 --- a/src/Explorer/Notebook/NotebookUtil.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { NotebookUtil } from "./NotebookUtil"; - -const fileName = "file"; -const notebookName = "file.ipynb"; -const folderPath = "folder"; -const filePath = `${folderPath}/${fileName}`; -const notebookPath = `${folderPath}/${notebookName}`; - -describe("NotebookUtil", () => { - describe("isNotebookFile", () => { - it("works for jupyter file paths", () => { - expect(NotebookUtil.isNotebookFile(filePath)).toBeFalsy(); - expect(NotebookUtil.isNotebookFile(notebookPath)).toBeTruthy(); - }); - }); - - describe("getFilePath", () => { - it("works for jupyter file paths", () => { - expect(NotebookUtil.getFilePath(folderPath, fileName)).toEqual(filePath); - }); - }); - - describe("getParentPath", () => { - it("works for jupyter file paths", () => { - expect(NotebookUtil.getParentPath(filePath)).toEqual(folderPath); - }); - }); - - describe("getName", () => { - it("works for jupyter file paths", () => { - expect(NotebookUtil.getName(filePath)).toEqual(fileName); - expect(NotebookUtil.getName(notebookPath)).toEqual(notebookName); - }); - }); - - describe("replaceName", () => { - it("works for jupyter file paths", () => { - expect(NotebookUtil.replaceName(filePath, "newName")).toEqual(filePath.replace(fileName, "newName")); - expect(NotebookUtil.replaceName(notebookPath, "newName")).toEqual(notebookPath.replace(notebookName, "newName")); - }); - }); -}); diff --git a/src/Explorer/Notebook/NotebookUtil.ts b/src/Explorer/Notebook/NotebookUtil.ts deleted file mode 100644 index d8e752f3d..000000000 --- a/src/Explorer/Notebook/NotebookUtil.ts +++ /dev/null @@ -1,81 +0,0 @@ -import * as StringUtils from "../../Utils/StringUtils"; -import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem"; - -// Must match rx-jupyter' FileType -export type FileType = "directory" | "file" | "notebook"; - -// Utilities for notebooks -export class NotebookUtil { - /** - * It's a notebook file if the filename ends with .ipynb. - */ - public static isNotebookFile(notebookPath: string): boolean { - const fileName = NotebookUtil.getName(notebookPath); - return !!fileName && StringUtils.endsWith(fileName, ".ipynb"); - } - - /** - * Note: this does not connect the item to a parent in a tree. - * @param name - * @param path - */ - public static createNotebookContentItem(name: string, path: string, type: FileType): NotebookContentItem { - return { - name, - path, - type: NotebookUtil.getType(type), - timestamp: NotebookUtil.getCurrentTimestamp(), - }; - } - - /** - * Convert rx-jupyter type to our type - * @param type - */ - public static getType(type: FileType): NotebookContentItemType { - switch (type) { - case "directory": - return NotebookContentItemType.Directory; - case "notebook": - return NotebookContentItemType.Notebook; - case "file": - return NotebookContentItemType.File; - default: - throw new Error(`Unknown file type: ${type}`); - } - } - - public static getCurrentTimestamp(): number { - return new Date().getTime(); - } - - public static getFilePath(path: string, fileName: string): string { - return `${path}/${fileName}`; - } - - public static getParentPath(filepath: string): undefined | string { - const basename = NotebookUtil.getName(filepath); - if (basename) { - const parentPath = filepath.split(basename).shift(); - if (parentPath) { - return parentPath.replace(/\/$/, ""); // no trailling slash - } - } - - return undefined; - } - - public static getName(path: string): undefined | string { - return path.split("/").pop(); - } - - public static replaceName(path: string, newName: string): string { - const contentName = path.split("/").pop(); - if (!contentName) { - throw new Error(`Failed to extract name from path ${path}`); - } - - const basePath = path.split(contentName).shift(); - return `${basePath}${newName}`; - } -} diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts deleted file mode 100644 index 608358cf7..000000000 --- a/src/Explorer/Notebook/useNotebook.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { cloneDeep } from "lodash"; -import create, { UseStore } from "zustand"; -import { AuthType } from "../../AuthType"; -import * as Constants from "../../Common/Constants"; -import { ConnectionStatusType } 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 { ContainerConnectionInfo, ContainerInfo } from "../../Contracts/DataModels"; -import { Action } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import { userContext } from "../../UserContext"; -import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; -import { useTabs } from "../../hooks/useTabs"; -import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem"; - -interface NotebookState { - isNotebookEnabled: boolean; - isNotebooksEnabledForAccount: boolean; - notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo; - sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo; - isSynapseLinkUpdating: boolean; - memoryUsageInfo: DataModels.MemoryUsageInfo; - isShellEnabled: boolean; - notebookBasePath: string; - isInitializingNotebooks: boolean; - myNotebooksContentRoot: NotebookContentItem; - galleryContentRoot: NotebookContentItem; - connectionInfo: ContainerConnectionInfo; - notebookFolderName: string; - isAllocating: boolean; - isRefreshed: boolean; - containerStatus: ContainerInfo; - isPhoenixNotebooks: boolean; - isPhoenixFeatures: 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; - setNotebookFolderName: (notebookFolderName: string) => void; - refreshNotebooksEnabledStateForAccount: () => Promise; - findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem; - insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem) => void; - updateNotebookItem: (item: NotebookContentItem) => void; - deleteNotebookItem: (item: NotebookContentItem) => void; - initializeNotebooksTree: () => Promise; - setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => void; - setIsAllocating: (isAllocating: boolean) => void; - resetContainerConnection: (connectionStatus: ContainerConnectionInfo) => void; - setIsRefreshed: (isAllocating: boolean) => void; - setContainerStatus: (containerStatus: ContainerInfo) => void; - getPhoenixStatus: () => Promise; - setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => void; - setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void; -} - -export const useNotebook: UseStore = create((set, get) => ({ - isNotebookEnabled: false, - isNotebooksEnabledForAccount: false, - notebookServerInfo: { - notebookServerEndpoint: undefined, - authToken: undefined, - forwardingId: undefined, - }, - sparkClusterConnectionInfo: { - userName: undefined, - password: undefined, - endpoints: [], - }, - isSynapseLinkUpdating: false, - memoryUsageInfo: undefined, - isShellEnabled: false, - notebookBasePath: Constants.Notebook.defaultBasePath, - isInitializingNotebooks: false, - myNotebooksContentRoot: undefined, - galleryContentRoot: undefined, - connectionInfo: { - status: ConnectionStatusType.Connect, - }, - notebookFolderName: undefined, - isAllocating: false, - isRefreshed: false, - containerStatus: { - status: undefined, - durationLeftInMinutes: undefined, - phoenixServerInfo: undefined, - }, - isPhoenixNotebooks: undefined, - isPhoenixFeatures: undefined, - 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 }), - setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }), - refreshNotebooksEnabledStateForAccount: async (): Promise => { - await get().getPhoenixStatus(); - const { databaseAccount, authType } = userContext; - if ( - authType === AuthType.EncryptedToken || - authType === AuthType.ResourceToken || - authType === AuthType.MasterKey - ) { - set({ isNotebooksEnabledForAccount: false }); - return; - } - - const firstWriteLocation = - userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" - ? databaseAccount?.location - : databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase(); - const disallowedLocationsUri: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`; - const authorizationHeader = getAuthorizationHeader(); - const startKey = TelemetryProcessor.traceStart(Action.RefreshNotebooksEnabled, { - dataExplorerArea: "Notebook", - }); - 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 }); - TelemetryProcessor.traceSuccess(Action.RefreshNotebooksEnabled, { isAccountInAllowedLocation }, startKey); - } catch (error) { - Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount"); - set({ isNotebooksEnabledForAccount: false }); - TelemetryProcessor.traceFailure(Action.RefreshNotebooksEnabled, { error: getErrorMessage(error) }, startKey); - } - }, - findItem: (root: NotebookContentItem, item: NotebookContentItem): NotebookContentItem => { - const currentItem = root || get().myNotebooksContentRoot; - - if (currentItem) { - if (currentItem.path === item.path && currentItem.name === item.name) { - return currentItem; - } - - if (currentItem.children) { - for (const childItem of currentItem.children) { - const result = get().findItem(childItem, item); - if (result) { - return result; - } - } - } - } - - return undefined; - }, - insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem): void => { - const root = cloneDeep(get().myNotebooksContentRoot); - const parentItem = get().findItem(root, parent); - item.parent = parentItem; - if (parentItem.children) { - parentItem.children.push(item); - } else { - parentItem.children = [item]; - } - set({ myNotebooksContentRoot: root }); - }, - updateNotebookItem: (item: NotebookContentItem): void => { - const root = cloneDeep(get().myNotebooksContentRoot); - const parentItem = get().findItem(root, item.parent); - parentItem.children = parentItem.children.filter((child) => child.path !== item.path); - parentItem.children.push(item); - item.parent = parentItem; - set({ myNotebooksContentRoot: root }); - }, - deleteNotebookItem: (item: NotebookContentItem): void => { - const root = cloneDeep(get().myNotebooksContentRoot); - const parentItem = get().findItem(root, item.parent); - parentItem.children = parentItem.children.filter((child) => child.path !== item.path); - set({ myNotebooksContentRoot: root }); - }, - initializeNotebooksTree: async (): Promise => { - const notebookFolderName = get().isPhoenixNotebooks ? "Temporary Notebooks" : "My Notebooks"; - set({ notebookFolderName }); - const myNotebooksContentRoot = { - name: get().notebookFolderName, - path: get().notebookBasePath, - type: NotebookContentItemType.Directory, - }; - const galleryContentRoot = { - name: "Gallery", - path: "Gallery", - type: NotebookContentItemType.File, - }; - - set({ - myNotebooksContentRoot, - galleryContentRoot, - }); - }, - setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }), - setIsAllocating: (isAllocating: boolean) => set({ isAllocating }), - resetContainerConnection: (connectionStatus: ContainerConnectionInfo): void => { - useTabs.getState().closeAllNotebookTabs(true); - useNotebook.getState().setConnectionInfo(connectionStatus); - useNotebook.getState().setNotebookServerInfo(undefined); - useNotebook.getState().setIsAllocating(false); - useNotebook.getState().setContainerStatus({ - status: undefined, - durationLeftInMinutes: undefined, - phoenixServerInfo: undefined, - }); - }, - setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }), - setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }), - getPhoenixStatus: async () => { - if (get().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) { - // getDbAccountAllowedStatus has been deprecated; Phoenix features are no longer available. - set({ isPhoenixNotebooks: false }); - set({ isPhoenixFeatures: false }); - } - }, - setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }), - setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }), -})); diff --git a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/__snapshots__/AddGlobalSecondaryIndexPanel.test.tsx.snap b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/__snapshots__/AddGlobalSecondaryIndexPanel.test.tsx.snap index 2495d4165..ee0df3b77 100644 --- a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/__snapshots__/AddGlobalSecondaryIndexPanel.test.tsx.snap +++ b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/__snapshots__/AddGlobalSecondaryIndexPanel.test.tsx.snap @@ -145,29 +145,15 @@ exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = ` = ({ explorer }) => { React.useEffect(() => { subscriptions.push( - { - dispose: useNotebook.subscribe( - () => setState({}), - (state) => state.isNotebookEnabled, - ), - }, { dispose: useSelectedNode.subscribe(() => setState({})) }, { dispose: useCarousel.subscribe( @@ -609,7 +602,7 @@ export const SplashScreen: React.FC = ({ explorer }) => { }; const getRecentItems = (): JSX.Element => { - const recentItems = createRecentItems()?.filter((item) => item.description !== "Notebook"); + const recentItems = createRecentItems(); return ( diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index f78a95eac..78faf849c 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -5,7 +5,6 @@ import { TriggerDefinition, UserDefinedFunctionDefinition, } from "@azure/cosmos"; -import { useNotebook } from "Explorer/Notebook/useNotebook"; import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; import { isFabricMirrored } from "Platform/Fabric/FabricUtil"; import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils"; diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index dfcf53422..8ce6e2cb0 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -18,12 +18,8 @@ import { userContext } from "UserContext"; import { ReactTabKind, useTabs } from "hooks/useTabs"; import * as React from "react"; import { useEffect, useMemo } from "react"; -import shallow from "zustand/shallow"; import { useDatabaseLoadScenario } from "../../Metrics/useMetricPhases"; import Explorer from "../Explorer"; -import { useNotebook } from "../Notebook/useNotebook"; - -export const MyNotebooksTitle = "My Notebooks"; interface ResourceTreeProps { explorer: Explorer; @@ -40,13 +36,6 @@ export const ResourceTree: React.FC = ({ explorer }: Resource const [openItems, setOpenItems] = React.useState([]); const treeStyles = useTreeStyles(); - const { isNotebookEnabled } = useNotebook( - (state) => ({ - isNotebookEnabled: state.isNotebookEnabled, - }), - shallow, - ); - // We intentionally avoid using a state selector here because we want to re-render the tree if the active tab changes. const { refreshActiveTab } = useTabs(); @@ -65,24 +54,8 @@ export const ResourceTree: React.FC = ({ explorer }: Resource const databaseTreeNodes = useMemo(() => { return userContext.authType === AuthType.ResourceToken ? createResourceTokenTreeNodes(resourceTokenCollection) - : createDatabaseTreeNodes( - explorer, - isNotebookEnabled, - databases, - refreshActiveTab, - searchText, - sortOrder, - pinnedDatabaseIds, - ); - }, [ - resourceTokenCollection, - databases, - isNotebookEnabled, - refreshActiveTab, - searchText, - sortOrder, - pinnedDatabaseIds, - ]); + : createDatabaseTreeNodes(explorer, databases, refreshActiveTab, searchText, sortOrder, pinnedDatabaseIds); + }, [resourceTokenCollection, databases, refreshActiveTab, searchText, sortOrder, pinnedDatabaseIds]); const headerNodes: TreeNode[] = isFabricMirrored() ? [] diff --git a/src/Explorer/Tree/ResourceTreeAdapter.test.tsx b/src/Explorer/Tree/ResourceTreeAdapter.test.tsx deleted file mode 100644 index 13e1137be..000000000 --- a/src/Explorer/Tree/ResourceTreeAdapter.test.tsx +++ /dev/null @@ -1,240 +0,0 @@ -import { shallow } from "enzyme"; -import React from "react"; -import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { - LegacyTreeComponent, - LegacyTreeComponentProps, - LegacyTreeNode, -} from "../Controls/TreeComponent/LegacyTreeComponent"; -import Explorer from "../Explorer"; -import Collection from "./Collection"; -import { ResourceTreeAdapter } from "./ResourceTreeAdapter"; - -const schema: DataModels.ISchema = { - id: "fakeSchemaId", - accountName: "fakeAccountName", - resource: "dbs/FakeDbName/colls/FakeCollectionName", - fields: [ - { - dataType: { - code: 15, - name: "String", - }, - hasNulls: true, - isArray: false, - schemaType: { - code: 0, - name: "Data", - }, - name: "_rid", - path: "_rid", - maxRepetitionLevel: 0, - maxDefinitionLevel: 1, - }, - { - dataType: { - code: 11, - name: "Int64", - }, - hasNulls: true, - isArray: false, - schemaType: { - code: 0, - name: "Data", - }, - name: "_ts", - path: "_ts", - maxRepetitionLevel: 0, - maxDefinitionLevel: 1, - }, - { - dataType: { - code: 15, - name: "String", - }, - hasNulls: true, - isArray: false, - schemaType: { - code: 0, - name: "Data", - }, - name: "id", - path: "id", - maxRepetitionLevel: 0, - maxDefinitionLevel: 1, - }, - { - dataType: { - code: 15, - name: "String", - }, - hasNulls: true, - isArray: false, - schemaType: { - code: 0, - name: "Data", - }, - name: "pk", - path: "pk", - maxRepetitionLevel: 0, - maxDefinitionLevel: 1, - }, - { - dataType: { - code: 15, - name: "String", - }, - hasNulls: true, - isArray: false, - schemaType: { - code: 0, - name: "Data", - }, - name: "other", - path: "other", - maxRepetitionLevel: 0, - maxDefinitionLevel: 1, - }, - { - dataType: { - code: 15, - name: "String", - }, - hasNulls: true, - isArray: false, - schemaType: { - code: 0, - name: "Data", - }, - name: "name", - path: "nested.name", - maxRepetitionLevel: 0, - maxDefinitionLevel: 1, - }, - { - dataType: { - code: 11, - name: "Int64", - }, - hasNulls: true, - isArray: false, - schemaType: { - code: 0, - name: "Data", - }, - name: "someNumber", - path: "nested.someNumber", - maxRepetitionLevel: 0, - maxDefinitionLevel: 1, - }, - { - dataType: { - code: 17, - name: "Double", - }, - hasNulls: true, - isArray: false, - schemaType: { - code: 0, - name: "Data", - }, - name: "anotherNumber", - path: "nested.anotherNumber", - maxRepetitionLevel: 0, - maxDefinitionLevel: 1, - }, - { - dataType: { - code: 15, - name: "String", - }, - hasNulls: true, - isArray: false, - schemaType: { - code: 0, - name: "Data", - }, - name: "name", - path: "items.list.items.name", - maxRepetitionLevel: 1, - maxDefinitionLevel: 3, - }, - { - dataType: { - code: 11, - name: "Int64", - }, - hasNulls: true, - isArray: false, - schemaType: { - code: 0, - name: "Data", - }, - name: "someNumber", - path: "items.list.items.someNumber", - maxRepetitionLevel: 1, - maxDefinitionLevel: 3, - }, - { - dataType: { - code: 17, - name: "Double", - }, - hasNulls: true, - isArray: false, - schemaType: { - code: 0, - name: "Data", - }, - name: "anotherNumber", - path: "items.list.items.anotherNumber", - maxRepetitionLevel: 1, - maxDefinitionLevel: 3, - }, - { - dataType: { - code: 15, - name: "String", - }, - hasNulls: true, - isArray: false, - schemaType: { - code: 0, - name: "Data", - }, - name: "_etag", - path: "_etag", - maxRepetitionLevel: 0, - maxDefinitionLevel: 1, - }, - ], -}; - -const createMockCollection = (): ViewModels.Collection => { - const mockCollection = {} as DataModels.Collection; - mockCollection._rid = "fakeRid"; - mockCollection._self = "fakeSelf"; - mockCollection.id = "fakeId"; - mockCollection.analyticalStorageTtl = 0; - mockCollection.schema = schema; - - const mockCollectionVM: ViewModels.Collection = new Collection(new Explorer(), "fakeDatabaseId", mockCollection); - - return mockCollectionVM; -}; - -describe("Resource tree for schema", () => { - const mockContainer = new Explorer(); - const resourceTree = new ResourceTreeAdapter(mockContainer); - - it("should render", () => { - const rootNode: LegacyTreeNode = resourceTree.buildSchemaNode(createMockCollection()); - const props: LegacyTreeComponentProps = { - rootNode, - className: "dataResourceTree", - }; - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx deleted file mode 100644 index e25b76a45..000000000 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ /dev/null @@ -1,496 +0,0 @@ -import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; -import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity"; -import { shouldShowScriptNodes } from "Explorer/Tree/treeNodeUtil"; -import { getItemName } from "Utils/APITypeUtils"; -import * as ko from "knockout"; -import * as React from "react"; -import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; -import DeleteIcon from "../../../images/delete.svg"; -import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; -import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; -import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; -import FileIcon from "../../../images/notebook/file-cosmos.svg"; -import RefreshIcon from "../../../images/refresh-cosmos.svg"; -import CollectionIcon from "../../../images/tree-collection.svg"; -import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; -import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; -import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import { userContext } from "../../UserContext"; -import { isServerlessAccount } from "../../Utils/CapabilityUtils"; -import { useTabs } from "../../hooks/useTabs"; -import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; -import { useDialog } from "../Controls/Dialog"; -import { LegacyTreeComponent, LegacyTreeNode } from "../Controls/TreeComponent/LegacyTreeComponent"; -import Explorer from "../Explorer"; -import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; -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"; -import StoredProcedure from "./StoredProcedure"; -import Trigger from "./Trigger"; -import UserDefinedFunction from "./UserDefinedFunction"; - -export class ResourceTreeAdapter implements ReactAdapter { - public static readonly MyNotebooksTitle = "My Notebooks"; - - private static readonly DataTitle = "DATA"; - private static readonly NotebooksTitle = "NOTEBOOKS"; - private static readonly PseudoDirPath = "PsuedoDir"; - - public parameters: ko.Observable; - - public myNotebooksContentRoot: NotebookContentItem; - - public constructor(private container: Explorer) { - this.parameters = ko.observable(Date.now()); - - useSelectedNode.subscribe(() => this.triggerRender()); - useTabs.subscribe( - () => this.triggerRender(), - (state) => state.activeTab, - ); - useNotebook.subscribe( - () => this.triggerRender(), - (state) => state.isNotebookEnabled, - ); - - useDatabases.subscribe(() => this.triggerRender()); - this.triggerRender(); - } - - private traceMyNotebookTreeInfo() { - const myNotebooksTree = this.myNotebooksContentRoot; - if (myNotebooksTree.children) { - // Count 1st generation children (tree is lazy-loaded) - const nodeCounts = { files: 0, notebooks: 0, directories: 0 }; - myNotebooksTree.children.forEach((treeNode) => { - switch ((treeNode as NotebookContentItem).type) { - case NotebookContentItemType.File: - nodeCounts.files++; - break; - case NotebookContentItemType.Directory: - nodeCounts.directories++; - break; - case NotebookContentItemType.Notebook: - nodeCounts.notebooks++; - break; - default: - break; - } - }); - TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts }); - } - } - - public renderComponent(): JSX.Element { - const dataRootNode = this.buildDataTree(); - return ; - } - - public async initialize(): Promise { - const refreshTasks: Promise[] = []; - - this.myNotebooksContentRoot = { - name: useNotebook.getState().notebookFolderName, - path: useNotebook.getState().notebookBasePath, - type: NotebookContentItemType.Directory, - }; - - return Promise.all(refreshTasks); - } - - private buildDataTree(): LegacyTreeNode { - const databaseTreeNodes: LegacyTreeNode[] = useDatabases - .getState() - .databases.map((database: ViewModels.Database) => { - const databaseNode: LegacyTreeNode = { - label: database.id(), - iconSrc: CosmosDBIcon, - isExpanded: false, - className: "databaseHeader", - children: [], - isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()), - contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database.id()), - onClick: async (isExpanded) => { - // Rewritten version of expandCollapseDatabase(): - if (isExpanded) { - database.collapseDatabase(); - } else { - if (databaseNode.children?.length === 0) { - databaseNode.isLoading = true; - } - await database.expandDatabase(); - } - databaseNode.isLoading = false; - useSelectedNode.getState().setSelectedNode(database); - useCommandBar.getState().setContextButtons([]); - useTabs.getState().refreshActiveTab((tab: TabsBase) => tab.collection?.databaseId === database.id()); - }, - onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(database), - }; - - if (database.isDatabaseShared()) { - databaseNode.children.push({ - label: "Scale", - isSelected: () => - useSelectedNode - .getState() - .isDataNodeSelected(database.id(), undefined, [ViewModels.CollectionTabKind.DatabaseSettings]), - onClick: database.onSettingsClick.bind(database), - }); - } - - // Find collections - database - .collections() - .forEach((collection: ViewModels.Collection) => - databaseNode.children.push(this.buildCollectionNode(database, collection)), - ); - - database.collections.subscribe((collections: ViewModels.Collection[]) => { - collections.forEach((collection: ViewModels.Collection) => - databaseNode.children.push(this.buildCollectionNode(database, collection)), - ); - }); - - return databaseNode; - }); - - return { - label: undefined, - isExpanded: true, - children: databaseTreeNodes, - }; - } - - private buildCollectionNode(database: ViewModels.Database, collection: ViewModels.Collection): LegacyTreeNode { - const children: LegacyTreeNode[] = []; - children.push({ - label: getItemName(), - onClick: () => { - collection.openTab(); - // push to most recent - collectionWasOpened(userContext.databaseAccount?.name, collection); - }, - isSelected: () => - useSelectedNode - .getState() - .isDataNodeSelected(collection.databaseId, collection.id(), [ - ViewModels.CollectionTabKind.Documents, - ViewModels.CollectionTabKind.Graph, - ]), - contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection), - }); - - if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) { - children.push({ - label: database.isDatabaseShared() || isServerlessAccount() ? "Settings" : "Scale & Settings", - onClick: collection.onSettingsClick.bind(collection), - isSelected: () => - useSelectedNode - .getState() - .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Settings]), - }); - } - - const schemaNode: LegacyTreeNode = this.buildSchemaNode(collection); - if (schemaNode) { - children.push(schemaNode); - } - - if (shouldShowScriptNodes()) { - children.push(this.buildStoredProcedureNode(collection)); - children.push(this.buildUserDefinedFunctionsNode(collection)); - children.push(this.buildTriggerNode(collection)); - } - - // This is a rewrite of showConflicts - const showConflicts = - userContext?.databaseAccount?.properties.enableMultipleWriteLocations && - collection.rawDataModel && - !!collection.rawDataModel.conflictResolutionPolicy; - - if (showConflicts) { - children.push({ - label: "Conflicts", - onClick: collection.onConflictsClick.bind(collection), - isSelected: () => - useSelectedNode - .getState() - .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Conflicts]), - }); - } - - return { - label: collection.id(), - iconSrc: CollectionIcon, - isExpanded: false, - children: children, - className: "collectionHeader", - contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection), - onClick: () => { - // Rewritten version of expandCollapseCollection - useSelectedNode.getState().setSelectedNode(collection); - useCommandBar.getState().setContextButtons([]); - useTabs - .getState() - .refreshActiveTab( - (tab: TabsBase) => - tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId, - ); - }, - onExpanded: () => { - if (shouldShowScriptNodes()) { - collection.loadStoredProcedures(); - collection.loadUserDefinedFunctions(); - collection.loadTriggers(); - } - }, - isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()), - onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection), - }; - } - - private buildStoredProcedureNode(collection: ViewModels.Collection): LegacyTreeNode { - return { - label: "Stored Procedures", - children: collection.storedProcedures().map((sp: StoredProcedure) => ({ - label: sp.id(), - onClick: sp.open.bind(sp), - isSelected: () => - useSelectedNode - .getState() - .isDataNodeSelected(collection.databaseId, collection.id(), [ - ViewModels.CollectionTabKind.StoredProcedures, - ]), - contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(this.container, sp), - })), - onClick: () => { - collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures); - useTabs - .getState() - .refreshActiveTab( - (tab: TabsBase) => - tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId, - ); - }, - }; - } - - private buildUserDefinedFunctionsNode(collection: ViewModels.Collection): LegacyTreeNode { - return { - label: "User Defined Functions", - children: collection.userDefinedFunctions().map((udf: UserDefinedFunction) => ({ - label: udf.id(), - onClick: udf.open.bind(udf), - isSelected: () => - useSelectedNode - .getState() - .isDataNodeSelected(collection.databaseId, collection.id(), [ - ViewModels.CollectionTabKind.UserDefinedFunctions, - ]), - contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems( - this.container, - udf, - ), - })), - onClick: () => { - collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions); - useTabs - .getState() - .refreshActiveTab( - (tab: TabsBase) => - tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId, - ); - }, - }; - } - - private buildTriggerNode(collection: ViewModels.Collection): LegacyTreeNode { - return { - label: "Triggers", - children: collection.triggers().map((trigger: Trigger) => ({ - label: trigger.id(), - onClick: trigger.open.bind(trigger), - isSelected: () => - useSelectedNode - .getState() - .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Triggers]), - contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(this.container, trigger), - })), - onClick: () => { - collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers); - useTabs - .getState() - .refreshActiveTab( - (tab: TabsBase) => - tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId, - ); - }, - }; - } - - public buildSchemaNode(collection: ViewModels.Collection): LegacyTreeNode { - if (collection.analyticalStorageTtl() == undefined) { - return undefined; - } - - if (!collection.schema || !collection.schema.fields) { - return undefined; - } - - return { - label: "Schema", - children: this.getSchemaNodes(collection.schema.fields), - onClick: () => { - collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Schema); - useTabs.getState().refreshActiveTab((tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid); - }, - }; - } - - private getSchemaNodes(fields: DataModels.IDataField[]): LegacyTreeNode[] { - const schema: any = {}; - - //unflatten - fields.forEach((field: DataModels.IDataField, fieldIndex: number) => { - const path: string[] = field.path.split("."); - const fieldProperties = [field.dataType.name, `HasNulls: ${field.hasNulls}`]; - let current: any = {}; - path.forEach((name: string, pathIndex: number) => { - if (pathIndex === 0) { - if (schema[name] === undefined) { - if (pathIndex === path.length - 1) { - schema[name] = fieldProperties; - } else { - schema[name] = {}; - } - } - current = schema[name]; - } else { - if (current[name] === undefined) { - if (pathIndex === path.length - 1) { - current[name] = fieldProperties; - } else { - current[name] = {}; - } - } - current = current[name]; - } - }); - }); - - const traverse = (obj: any): LegacyTreeNode[] => { - const children: LegacyTreeNode[] = []; - - if (obj !== null && !Array.isArray(obj) && typeof obj === "object") { - Object.entries(obj).forEach(([key, value]) => { - children.push({ label: key, children: traverse(value) }); - }); - } else if (Array.isArray(obj)) { - return [{ label: obj[0] }, { label: obj[1] }]; - } - - return children; - }; - - return traverse(schema); - } - - private buildChildNodes( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - createDirectoryContextMenu: boolean, - createFileContextMenu: boolean, - ): LegacyTreeNode[] { - if (!item || !item.children) { - return []; - } else { - return item.children.map((item) => { - const result = - item.type === NotebookContentItemType.Directory - ? this.buildNotebookDirectoryNode(item, onFileClick, createDirectoryContextMenu, createFileContextMenu) - : this.buildNotebookFileNode(item, onFileClick, createFileContextMenu); - result.timestamp = item.timestamp; - return result; - }); - } - } - - private buildNotebookFileNode( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - createFileContextMenu: boolean, - ): LegacyTreeNode { - return { - label: item.name, - iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon, - className: "notebookHeader", - onClick: () => onFileClick(item), - isSelected: () => { - const activeTab = useTabs.getState().activeTab; - return ( - activeTab && - activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && - /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. - NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. - */ - (activeTab as any).notebookPath() === item.path - ); - }, - contextMenu: createFileContextMenu && this.createFileContextMenu(), - data: item, - }; - } - - private createFileContextMenu(): TreeNodeMenuItem[] { - return []; - } - - private createDirectoryContextMenu(): TreeNodeMenuItem[] { - return []; - } - - private buildNotebookDirectoryNode( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - createDirectoryContextMenu: boolean, - createFileContextMenu: boolean, - ): LegacyTreeNode { - return { - label: item.name, - iconSrc: undefined, - className: "notebookHeader", - isAlphaSorted: true, - isLeavesParentsSeparate: true, - onClick: undefined, - isSelected: () => { - const activeTab = useTabs.getState().activeTab; - return ( - activeTab && - activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && - /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. - NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. - */ - (activeTab as any).notebookPath() === item.path - ); - }, - contextMenu: - createDirectoryContextMenu && item.path !== ResourceTreeAdapter.PseudoDirPath - ? this.createDirectoryContextMenu() - : undefined, - data: item, - children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu), - }; - } - - public triggerRender() { - window.requestAnimationFrame(() => this.parameters(Date.now())); - } -} diff --git a/src/Explorer/Tree/__snapshots__/ResourceTreeAdapter.test.tsx.snap b/src/Explorer/Tree/__snapshots__/ResourceTreeAdapter.test.tsx.snap deleted file mode 100644 index f184ea29f..000000000 --- a/src/Explorer/Tree/__snapshots__/ResourceTreeAdapter.test.tsx.snap +++ /dev/null @@ -1,173 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Resource tree for schema should render 1`] = ` -
- -
-`; diff --git a/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap b/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap index ead9bdb15..53c49c24f 100644 --- a/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap +++ b/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap @@ -363,7 +363,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca ] `; -exports[`createDatabaseTreeNodes generates the correct tree structure for the Mongo API, with Notebooks and Phoenix features, on Emulator 1`] = ` +exports[`createDatabaseTreeNodes generates the correct tree structure for the Mongo API, on Emulator 1`] = ` [ { "children": [ diff --git a/src/Explorer/Tree/treeNodeUtil.test.ts b/src/Explorer/Tree/treeNodeUtil.test.ts index c64f9276e..f30c3e166 100644 --- a/src/Explorer/Tree/treeNodeUtil.test.ts +++ b/src/Explorer/Tree/treeNodeUtil.test.ts @@ -4,7 +4,6 @@ import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract"; import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import Explorer from "Explorer/Explorer"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; -import { useNotebook } from "Explorer/Notebook/useNotebook"; import { DeleteDatabaseConfirmationPanel } from "Explorer/Panes/DeleteDatabaseConfirmationPanel"; import TabsBase from "Explorer/Tabs/TabsBase"; import StoredProcedure from "Explorer/Tree/StoredProcedure"; @@ -363,7 +362,7 @@ describe("createDatabaseTreeNodes", () => { }, } as never, }); - nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab, ""); + nodes = createDatabaseTreeNodes(explorer, useDatabases.getState().databases, refreshActiveTab, ""); }); it("creates expected tree", () => { @@ -371,11 +370,10 @@ describe("createDatabaseTreeNodes", () => { }); }); - it.each<[string, Platform, boolean, Partial, Partial]>([ + it.each<[string, Platform, Partial, Partial]>([ [ "the SQL API, on Fabric read-only (mirrored)", Platform.Fabric, - false, { capabilities: [], enableMultipleWriteLocations: true }, { fabricContext: { @@ -387,7 +385,6 @@ describe("createDatabaseTreeNodes", () => { [ "the SQL API, on Fabric non read-only (native)", Platform.Fabric, - false, { capabilities: [], enableMultipleWriteLocations: true }, { fabricContext: { @@ -399,7 +396,6 @@ describe("createDatabaseTreeNodes", () => { [ "the SQL API, on Portal", Platform.Portal, - false, { capabilities: [], enableMultipleWriteLocations: true }, { fabricContext: undefined, @@ -408,7 +404,6 @@ describe("createDatabaseTreeNodes", () => { [ "the Cassandra API, serverless, on Hosted", Platform.Hosted, - false, { capabilities: [ { name: CapabilityNames.EnableCassandra, description: "" }, @@ -418,45 +413,34 @@ describe("createDatabaseTreeNodes", () => { { fabricContext: undefined }, ], [ - "the Mongo API, with Notebooks and Phoenix features, on Emulator", + "the Mongo API, on Emulator", Platform.Emulator, - true, { capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }], }, { fabricContext: undefined }, ], - ])( - "generates the correct tree structure for %s", - (_, platform, isNotebookEnabled, dbAccountProperties, userContext) => { - useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled }); - updateConfigContext({ platform }); - updateUserContext({ - ...userContext, - databaseAccount: { - properties: { - enableMultipleWriteLocations: true, - ...dbAccountProperties, - }, - } as unknown as DataModels.DatabaseAccount, - }); - const nodes = createDatabaseTreeNodes( - explorer, - isNotebookEnabled, - useDatabases.getState().databases, - refreshActiveTab, - "", - ); - expect(nodes).toMatchSnapshot(); - }, - ); + ])("generates the correct tree structure for %s", (_, platform, dbAccountProperties, userContext) => { + updateConfigContext({ platform }); + updateUserContext({ + ...userContext, + databaseAccount: { + properties: { + enableMultipleWriteLocations: true, + ...dbAccountProperties, + }, + } as unknown as DataModels.DatabaseAccount, + }); + const nodes = createDatabaseTreeNodes(explorer, useDatabases.getState().databases, refreshActiveTab, ""); + expect(nodes).toMatchSnapshot(); + }); // The above tests focused on the tree structure. The below tests focus on some core behaviors of the nodes. // They are not exhaustive, because exhaustive tests here require a lot of mocking and can become very brittle. // The goal is to cover some key behaviors like loading child nodes, opening tabs/side panels, etc. it("adds new collections to database as they appear", () => { - const nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab, ""); + const nodes = createDatabaseTreeNodes(explorer, useDatabases.getState().databases, refreshActiveTab, ""); const giganticDbNode = nodes.find((node) => node.label === giganticDb.id()); expect(giganticDbNode).toBeDefined(); expect(giganticDbNode.children.map((node) => node.label)).toStrictEqual(["schemaCollection", "load more"]); @@ -488,7 +472,7 @@ describe("createDatabaseTreeNodes", () => { }, } as unknown as DataModels.DatabaseAccount, }); - nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab, ""); + nodes = createDatabaseTreeNodes(explorer, useDatabases.getState().databases, refreshActiveTab, ""); standardDbNode = nodes.find((node) => node.label === standardDb.id()); sharedDbNode = nodes.find((node) => node.label === sharedDb.id()); giganticDbNode = nodes.find((node) => node.label === giganticDb.id()); @@ -643,7 +627,7 @@ describe("createDatabaseTreeNodes", () => { setup(); // Rebuild the nodes after changing the user/config context. - nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab, ""); + nodes = createDatabaseTreeNodes(explorer, useDatabases.getState().databases, refreshActiveTab, ""); standardDbNode = nodes.find((node) => node.label === standardDb.id()); standardCollectionNode = standardDbNode.children.find((node) => node.label === standardCollection.id()); diff --git a/src/Explorer/Tree/treeNodeUtil.tsx b/src/Explorer/Tree/treeNodeUtil.tsx index 224542755..d784080cb 100644 --- a/src/Explorer/Tree/treeNodeUtil.tsx +++ b/src/Explorer/Tree/treeNodeUtil.tsx @@ -137,7 +137,6 @@ export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBa export const createDatabaseTreeNodes = ( container: Explorer, - isNotebookEnabled: boolean, databases: ViewModels.Database[], refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void, searchText = "", @@ -184,9 +183,7 @@ export const createDatabaseTreeNodes = ( database .collections() .forEach((collection: ViewModels.Collection) => - databaseNode.children.push( - buildCollectionNode(database, collection, isNotebookEnabled, container, refreshActiveTab), - ), + databaseNode.children.push(buildCollectionNode(database, collection, container, refreshActiveTab)), ); if (database.collectionsContinuationToken) { @@ -254,14 +251,13 @@ export const createDatabaseTreeNodes = ( export const buildCollectionNode = ( database: ViewModels.Database, collection: ViewModels.Collection, - isNotebookEnabled: boolean, container: Explorer, refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void, ): TreeNode => { let children: TreeNode[]; // Flat Tree for Fabric if (!isFabricMirrored()) { - children = buildCollectionNodeChildren(database, collection, isNotebookEnabled, container, refreshActiveTab); + children = buildCollectionNodeChildren(database, collection, container, refreshActiveTab); } const collectionNode: TreeNode = { @@ -310,7 +306,6 @@ export const buildCollectionNode = ( const buildCollectionNodeChildren = ( database: ViewModels.Database, collection: ViewModels.Collection, - isNotebookEnabled: boolean, container: Explorer, refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void, ): TreeNode[] => { diff --git a/src/Explorer/useSelectedNode.ts b/src/Explorer/useSelectedNode.ts index 4dd34a91e..cbade8ed9 100644 --- a/src/Explorer/useSelectedNode.ts +++ b/src/Explorer/useSelectedNode.ts @@ -1,5 +1,3 @@ -import { ConnectionStatusType } from "Common/Constants"; -import { useNotebook } from "Explorer/Notebook/useNotebook"; import create, { UseStore } from "zustand"; import * as ViewModels from "../Contracts/ViewModels"; import { useTabs } from "../hooks/useTabs"; @@ -13,7 +11,6 @@ export interface SelectedNodeState { collectionId?: string, subnodeKinds?: ViewModels.CollectionTabKind[], ) => boolean; - isConnectedToContainer: () => boolean; } export const useSelectedNode: UseStore = create((set, get) => ({ @@ -61,7 +58,4 @@ export const useSelectedNode: UseStore = create((set, get) => subnodeKinds.includes(selectedSubnodeKind) ); }, - isConnectedToContainer: (): boolean => { - return useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected; - }, })); diff --git a/src/Explorer/useSynapseLink.ts b/src/Explorer/useSynapseLink.ts new file mode 100644 index 000000000..f02673282 --- /dev/null +++ b/src/Explorer/useSynapseLink.ts @@ -0,0 +1,11 @@ +import create, { UseStore } from "zustand"; + +interface SynapseLinkState { + isSynapseLinkUpdating: boolean; + setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => void; +} + +export const useSynapseLink: UseStore = create((set) => ({ + isSynapseLinkUpdating: false, + setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }), +})); diff --git a/src/Main.tsx b/src/Main.tsx index 6179711d6..e60a5e863 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -52,8 +52,6 @@ import { ErrorBoundary } from "./Explorer/ErrorBoundary"; import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less"; import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter"; -import "./Explorer/Menus/CommandBar/ConnectionStatusComponent.less"; -import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less"; import "./Explorer/Menus/NotificationConsole/NotificationConsole.less"; import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import "./Explorer/Panes/PanelComponent.less"; diff --git a/src/Phoenix/PhoenixClient.ts b/src/Phoenix/PhoenixClient.ts deleted file mode 100644 index f1d722e3b..000000000 --- a/src/Phoenix/PhoenixClient.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { configContext } from "ConfigContext"; -import { useDialog } from "Explorer/Controls/Dialog"; -import { Action } from "Shared/Telemetry/TelemetryConstants"; -import { userContext } from "UserContext"; -import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils"; -import promiseRetry, { AbortError, Options } from "p-retry"; -import { - Areas, - ConnectionStatusType, - ContainerStatusType, - HttpHeaders, - HttpStatusCodes, - Notebook, -} from "../Common/Constants"; -import { getErrorMessage } from "../Common/ErrorHandlingUtils"; -import * as Logger from "../Common/Logger"; -import { - ContainerConnectionInfo, - ContainerInfo, - IContainerData, - IMaxAllocationTimeExceeded, - IPhoenixConnectionInfoResult, - IPhoenixError, - IPhoenixServiceInfo, - IProvisionData, - IResponse, - PhoenixErrorType, -} from "../Contracts/DataModels"; -import { useNotebook } from "../Explorer/Notebook/useNotebook"; -import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; -import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; - -export class PhoenixClient { - private armResourceId: string; - private containerHealthHandler: NodeJS.Timeout; - private retryOptions: Options = { - retries: Notebook.retryAttempts, - maxTimeout: Notebook.retryAttemptDelayMs, - minTimeout: Notebook.retryAttemptDelayMs, - }; - private abortController: AbortController; - private abortSignal: AbortSignal; - - constructor(armResourceId: string) { - this.armResourceId = armResourceId; - } - - public async allocateContainer(provisionData: IProvisionData): Promise> { - this.initializeCancelEventListener(); - - return promiseRetry(() => this.executeContainerAssignmentOperation(provisionData, "allocate"), { - retries: 4, - maxTimeout: 20000, - minTimeout: 20000, - signal: this.abortSignal, - }); - } - - public async resetContainer(provisionData: IProvisionData): Promise> { - return this.executeContainerAssignmentOperation(provisionData, "reset"); - } - - private async executeContainerAssignmentOperation( - provisionData: IProvisionData, - operation: string, - ): Promise> { - let response; - try { - response = await fetch(`${this.getPhoenixControlPlanePathPrefix()}/containerconnections/multicontainer`, { - method: operation === "allocate" ? "POST" : "PATCH", - headers: PhoenixClient.getHeaders(), - body: JSON.stringify(provisionData), - }); - const responseJson = await response?.json(); - if (response.ok) { - const phoenixConnectionInfoResult = responseJson as IPhoenixConnectionInfoResult[]; - if ( - !phoenixConnectionInfoResult || - phoenixConnectionInfoResult.length === 0 || - !phoenixConnectionInfoResult[0] - ) { - throw new Error("Received invalid phoenix connection response."); - } - return { - status: response.status, - data: phoenixConnectionInfoResult[0].phoenixServiceInfo, - }; - } - const phoenixError = responseJson as IPhoenixError; - if (response.status === HttpStatusCodes.Forbidden) { - if (phoenixError.message === "Sequence contains no elements") { - throw Error("Phoenix container allocation failed, please try again later."); - } - throw new AbortError(this.ConvertToForbiddenErrorString(phoenixError)); - } - throw new AbortError(phoenixError.message); - } catch (error) { - error.status = response?.status; - throw error; - } - } - - public async initiateContainerHeartBeat(shouldUseNotebookStates: boolean, containerData: IContainerData) { - if (this.containerHealthHandler) { - clearTimeout(this.containerHealthHandler); - } - await this.getContainerHealth(shouldUseNotebookStates, Notebook.containerStatusHeartbeatDelayMs, containerData); - } - - private scheduleContainerHeartbeat( - shouldUseNotebookStates: boolean, - delayMs: number, - containerData: IContainerData, - ): void { - this.containerHealthHandler = setTimeout(async () => { - await this.getContainerHealth(shouldUseNotebookStates, delayMs, containerData); - }, delayMs); - } - - private async getContainerStatusAsync( - shouldUseNotebookStates: boolean, - containerData: IContainerData, - ): Promise { - try { - const runContainerStatusAsync = async () => { - const response = await window.fetch( - `${this.getPhoenixControlPlanePathPrefix()}/${containerData.forwardingId}`, - { - method: "GET", - headers: PhoenixClient.getHeaders(), - }, - ); - if (response.status === HttpStatusCodes.OK) { - const containerStatus = await response.json(); - return { - durationLeftInMinutes: containerStatus?.durationLeftInMinutes, - phoenixServerInfo: containerStatus?.phoenixServerInfo, - status: ContainerStatusType.Active, - }; - } else if (response.status === HttpStatusCodes.NotFound) { - const error = "Disconnected from compute workspace"; - Logger.logError(error, ""); - const connectionStatus: ContainerConnectionInfo = { - status: ConnectionStatusType.Reconnect, - }; - TelemetryProcessor.traceMark(Action.PhoenixHeartBeat, { - dataExplorerArea: Areas.Notebook, - message: getErrorMessage(error), - }); - shouldUseNotebookStates ? useNotebook.getState().resetContainerConnection(connectionStatus) : undefined; - shouldUseNotebookStates && useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - shouldUseNotebookStates && - useDialog - .getState() - .showOkModalDialog( - "Disconnected", - "Disconnected from temporary workspace. Please click on connect button to connect to temporary workspace.", - ); - throw new AbortError(response.statusText); - } else if (response?.status === HttpStatusCodes.Forbidden) { - const validationMessage = this.ConvertToForbiddenErrorString(await response.json()); - if (validationMessage) { - useDialog.getState().showOkModalDialog("Connection Failed", `${validationMessage}`); - } - throw new AbortError(response.statusText); - } - throw new Error(response.statusText); - }; - return await promiseRetry(runContainerStatusAsync, this.retryOptions); - } catch (error) { - TelemetryProcessor.traceFailure(Action.PhoenixHeartBeat, { - dataExplorerArea: Areas.Notebook, - }); - Logger.logError(getErrorMessage(error), ""); - const connectionStatus: ContainerConnectionInfo = { - status: ConnectionStatusType.Failed, - }; - shouldUseNotebookStates ? useNotebook.getState().resetContainerConnection(connectionStatus) : undefined; - shouldUseNotebookStates && useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - return { - durationLeftInMinutes: undefined, - phoenixServerInfo: undefined, - status: ContainerStatusType.Disconnected, - }; - } - } - - private async getContainerHealth(shouldUseNotebookStates: boolean, delayMs: number, containerData: IContainerData) { - const containerInfo = await this.getContainerStatusAsync(shouldUseNotebookStates, containerData); - shouldUseNotebookStates ? useNotebook.getState().setContainerStatus(containerInfo) : undefined; - - const containerStatus = shouldUseNotebookStates ? useNotebook.getState().containerStatus?.status : undefined; - if (containerStatus === ContainerStatusType.Active) { - this.scheduleContainerHeartbeat(shouldUseNotebookStates, delayMs, containerData); - } - } - - private getPhoenixControlPlanePathPrefix(): string { - if (!this.armResourceId) { - throw new Error("The Phoenix client was not initialized properly: missing ARM resource id"); - } - - const toolsEndpoint = - userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT; - - if (!validateEndpoint(toolsEndpoint, allowedJunoOrigins)) { - const error = `${toolsEndpoint} not allowed as tools endpoint`; - console.error(error); - throw new Error(error); - } - - return `${toolsEndpoint}/api/controlplane/toolscontainer/cosmosaccounts${this.armResourceId}`; - } - - private static getHeaders(): HeadersInit { - const authorizationHeader = getAuthorizationHeader(); - return { - [authorizationHeader.header]: authorizationHeader.token, - [HttpHeaders.contentType]: "application/json", - }; - } - - private initializeCancelEventListener(): void { - this.abortController = new AbortController(); - this.abortSignal = this.abortController.signal; - - document.addEventListener("keydown", (event: KeyboardEvent) => { - if (event.ctrlKey && (event.key === "c" || event.key === "z")) { - this.abortController.abort(new AbortError("Request canceled")); - } - }); - } - - public ConvertToForbiddenErrorString(jsonData: IPhoenixError): string { - const errInfo = jsonData; - switch (errInfo?.type) { - case PhoenixErrorType.MaxAllocationTimeExceeded: { - const maxAllocationTimeExceeded = errInfo as IMaxAllocationTimeExceeded; - const allocateAfterTimestamp = new Date(maxAllocationTimeExceeded?.earliestAllocationTimestamp); - allocateAfterTimestamp.setDate(allocateAfterTimestamp.getDate() + 1); - return `${errInfo.message}` + ". Please try again after " + `${allocateAfterTimestamp.toLocaleString()}`; - } - case PhoenixErrorType.MaxDbAccountsPerUserExceeded: - case PhoenixErrorType.MaxUsersPerDbAccountExceeded: - case PhoenixErrorType.AllocationValidationResult: - case PhoenixErrorType.RegionNotServicable: - case PhoenixErrorType.UserMissingPermissionsError: - case PhoenixErrorType.SubscriptionNotAllowed: { - return `${errInfo.message}`; - } - default: { - return undefined; - } - } - } -} diff --git a/src/Utils/NotebookConfigurationUtils.ts b/src/Utils/NotebookConfigurationUtils.ts deleted file mode 100644 index 31bfccfdd..000000000 --- a/src/Utils/NotebookConfigurationUtils.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { getErrorMessage } from "../Common/ErrorHandlingUtils"; -import * as Logger from "../Common/Logger"; -import * as DataModels from "../Contracts/DataModels"; - -interface KernelConnectionMetadata { - name: string; - configurationEndpoints: DataModels.NotebookConfigurationEndpoints; - notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo; -} - -export const _configureServiceEndpoints = async (kernelMetadata: KernelConnectionMetadata): Promise => { - if (!kernelMetadata) { - // should never get into this state - Logger.logWarning("kernel metadata is null or undefined", "NotebookConfigurationUtils/configureServiceEndpoints"); - return; - } - - const notebookConnectionInfo = kernelMetadata.notebookConnectionInfo; - const configurationEndpoints = kernelMetadata.configurationEndpoints; - if (notebookConnectionInfo && configurationEndpoints) { - try { - const headers: HeadersInit = { "Content-Type": "application/json" }; - if (notebookConnectionInfo.authToken) { - headers["Authorization"] = `token ${notebookConnectionInfo.authToken}`; - } - const response = await fetch(`${notebookConnectionInfo.notebookServerEndpoint}/api/configureEndpoints`, { - method: "POST", - headers, - body: JSON.stringify(configurationEndpoints), - }); - if (!response.ok) { - const responseMessage = await response.json(); - Logger.logError( - getErrorMessage(responseMessage), - "NotebookConfigurationUtils/configureServiceEndpoints", - response.status, - ); - } - } catch (error) { - Logger.logError(getErrorMessage(error), "NotebookConfigurationUtils/configureServiceEndpoints"); - } - } -}; - -export const configureServiceEndpoints = async ( - notebookPath: string, - notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo, - kernelName: string, - clusterConnectionInfo: DataModels.SparkClusterConnectionInfo, -): Promise => { - if (!notebookPath || !notebookConnectionInfo || !kernelName) { - Logger.logError( - "Invalid or missing notebook connection info/path", - "NotebookConfigurationUtils/configureServiceEndpoints", - ); - return Promise.reject("Invalid or missing notebook connection info"); - } - - if (!clusterConnectionInfo || !clusterConnectionInfo.endpoints || clusterConnectionInfo.endpoints.length === 0) { - Logger.logError( - "Invalid or missing cluster connection info/endpoints", - "NotebookConfigurationUtils/configureServiceEndpoints", - ); - return Promise.reject("Invalid or missing cluster connection info"); - } - - const notebookEndpointInfo: DataModels.NotebookConfigurationEndpointInfo[] = clusterConnectionInfo.endpoints.map( - (clusterEndpoint) => ({ - type: clusterEndpoint.kind.toLowerCase(), - endpoint: clusterEndpoint && clusterEndpoint.endpoint, - username: clusterConnectionInfo.userName, - password: clusterConnectionInfo.password, - token: "", // TODO. This was arcadiaToken() when our synapse/spark integration comes back - }), - ); - const configurationEndpoints: DataModels.NotebookConfigurationEndpoints = { - path: notebookPath, - endpoints: notebookEndpointInfo, - }; - const kernelMetadata: KernelConnectionMetadata = { - configurationEndpoints, - notebookConnectionInfo, - name: kernelName, - }; - - return await _configureServiceEndpoints(kernelMetadata); -}; diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index d4a4f9555..20586c9c1 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -83,12 +83,6 @@ export function useKnockoutExplorer(platform: Platform): Explorer { useEffect(() => { const effect = async () => { if (platform) { - //Updating phoenix feature flags for MPAC based of config context - if (configContext.isPhoenixEnabled === true) { - userContext.features.phoenixNotebooks = true; - userContext.features.phoenixFeatures = true; - } - let explorer: Explorer; try { if (platform === Platform.Hosted) { @@ -999,15 +993,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { if (inputs.flights.indexOf(Flights.PKPartitionKeyTest) !== -1) { userContext.features.partitionKeyDefault2 = true; } - if (inputs.flights.indexOf(Flights.PhoenixNotebooks) !== -1) { - userContext.features.phoenixNotebooks = true; - } - if (inputs.flights.indexOf(Flights.PhoenixFeatures) !== -1) { - userContext.features.phoenixFeatures = true; - } - if (inputs.flights.indexOf(Flights.NotebooksDownBanner) !== -1) { - userContext.features.notebooksDownBanner = true; - } } // Handle initial theme from portal diff --git a/src/hooks/useNotebookSnapshotStore.ts b/src/hooks/useNotebookSnapshotStore.ts deleted file mode 100644 index 841f6b5d1..000000000 --- a/src/hooks/useNotebookSnapshotStore.ts +++ /dev/null @@ -1,15 +0,0 @@ -import create, { UseStore } from "zustand"; - -export interface NotebookSnapshotHooks { - snapshot?: string; - error?: string; - setSnapshot: (imageSrc: string) => void; - setError: (error: string) => void; -} - -export const useNotebookSnapshotStore: UseStore = create((set) => ({ - snapshot: undefined, - error: undefined, - setSnapshot: (imageSrc: string) => set((state) => ({ ...state, snapshot: imageSrc })), - setError: (error: string) => set((state) => ({ ...state, error })), -})); diff --git a/src/hooks/useTabs.ts b/src/hooks/useTabs.ts index f276e1498..4444f5c8c 100644 --- a/src/hooks/useTabs.ts +++ b/src/hooks/useTabs.ts @@ -9,7 +9,6 @@ import { } from "Shared/AppStatePersistenceUtility"; import create, { UseStore } from "zustand"; import * as ViewModels from "../Contracts/ViewModels"; -import { CollectionTabKind } from "../Contracts/ViewModels"; import TabsBase from "../Explorer/Tabs/TabsBase"; export interface TabsState { @@ -27,7 +26,6 @@ export interface TabsState { refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void; closeTabsByComparator: (comparator: (tab: TabsBase) => boolean) => void; closeTab: (tab: TabsBase) => void; - closeAllNotebookTabs: (hardClose: boolean) => void; openAndActivateReactTab: (tabKind: ReactTabKind) => void; closeReactTab: (tabKind: ReactTabKind) => void; setIsTabExecuting: (state: boolean) => void; @@ -132,33 +130,6 @@ export const useTabs: UseStore = create((set, get) => ({ get().persistTabsState(); }, - closeAllNotebookTabs: (): void => { - const isNotebook = (tabKind: CollectionTabKind): boolean => { - if ( - tabKind === CollectionTabKind.Notebook || - tabKind === CollectionTabKind.NotebookV2 || - tabKind === CollectionTabKind.SchemaAnalyzer || - tabKind === CollectionTabKind.Terminal - ) { - return true; - } - return false; - }; - - const tabList = get().openedTabs; - if (tabList && tabList.length > 0) { - tabList.forEach((tab: TabsBase) => { - const tabKind: CollectionTabKind = tab.tabKind; - if (tabKind && isNotebook(tabKind)) { - tab.onCloseTabButtonClick(); - } - }); - - if (get().openedTabs.length === 0 && !isFabricMirrored()) { - set({ activeTab: undefined, activeReactTab: undefined }); - } - } - }, openAndActivateReactTab: (tabKind: ReactTabKind) => { if (get().openedReactTabs.indexOf(tabKind) === -1) { set((state) => ({ diff --git a/tsconfig.strict.json b/tsconfig.strict.json index e1021f162..b3db86d8f 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -43,7 +43,6 @@ "./src/Explorer/Graph/GraphExplorerComponent/GraphData.ts", "./src/Explorer/LazyMonaco.ts", "./src/Explorer/Menus/NotificationConsole/ConsoleData.tsx", - "./src/Explorer/Notebook/NotebookContentItem.ts", "./src/Explorer/OpenFullScreen.test.tsx", "./src/Explorer/OpenFullScreen.tsx", "./src/Explorer/Panes/PanelContainerComponent.test.tsx", @@ -62,6 +61,7 @@ "./src/Explorer/Tables/QueryBuilder/DateTimeUtilities.test.ts", "./src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts", "./src/Explorer/Tree/AccessibleVerticalList.ts", + "./src/Explorer/useSynapseLink.ts", "./src/HostedExplorerChildFrame.ts", "./src/Platform/Hosted/Components/MeControl.test.tsx", "./src/Platform/Hosted/Components/MeControl.tsx", @@ -106,7 +106,6 @@ "./src/hooks/useConfig.ts", "./src/hooks/useDirectories.tsx", "./src/hooks/useGraphPhoto.tsx", - "./src/hooks/useNotebookSnapshotStore.ts", "./src/hooks/usePortalAccessToken.tsx", "./src/hooks/useNotificationConsole.ts", "./src/hooks/useObservable.ts",