mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-07-24 22:37:22 +01:00
Remove Phoenix & Notebooks - Phase 5: Remove Phoenix and the notebook container/allocation core
Deletes the Phoenix client, notebook container/allocation core, and the legacy ResourceTreeAdapter; rewires the surviving non-notebook Synapse Link state into a new minimal useSynapseLink store; removes dead Phoenix container command-bar UI; and simplifies database-shell gating to enableCloudShell. Database shells (Mongo/Cassandra/Postgres/VCoreMongo) continue to run on CloudShell. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -235,7 +235,39 @@ engine deleted there. No separate work remains for this phase.
|
|||||||
pinned-repo methods.
|
pinned-repo methods.
|
||||||
- Remove GitHub-related localization keys from **all** locale files (`en` + non-English).
|
- 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`,
|
- Delete `src/Phoenix/`, `src/Explorer/Notebook/NotebookContainerClient.ts`,
|
||||||
`src/Explorer/Notebook/NotebookManager.tsx`, `src/Explorer/Notebook/useNotebook.ts`,
|
`src/Explorer/Notebook/NotebookManager.tsx`, `src/Explorer/Notebook/useNotebook.ts`,
|
||||||
`src/Explorer/Notebook/NotebookContentItem.ts`, `src/Explorer/Notebook/NotebookUtil.ts`
|
`src/Explorer/Notebook/NotebookContentItem.ts`, `src/Explorer/Notebook/NotebookUtil.ts`
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import { userContext } from "../UserContext";
|
|||||||
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
||||||
import { useSidePanel } from "../hooks/useSidePanel";
|
import { useSidePanel } from "../hooks/useSidePanel";
|
||||||
import Explorer from "./Explorer";
|
import Explorer from "./Explorer";
|
||||||
import { useNotebook } from "./Notebook/useNotebook";
|
|
||||||
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
|
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
|
||||||
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
|
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
|
||||||
import StoredProcedure from "./Tree/StoredProcedure";
|
import StoredProcedure from "./Tree/StoredProcedure";
|
||||||
@@ -120,23 +119,17 @@ export const createCollectionContextMenuButton = (
|
|||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||||
if (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) {
|
if (userContext.features.enableCloudShell) {
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||||
} else {
|
} else {
|
||||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label:
|
label: userContext.features.enableCloudShell ? t(Keys.contextMenu.openMongoShell) : t(Keys.contextMenu.newShell),
|
||||||
useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell
|
|
||||||
? t(Keys.contextMenu.openMongoShell)
|
|
||||||
: t(Keys.contextMenu.newShell),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (userContext.features.enableCloudShell && userContext.apiType === "Cassandra") {
|
||||||
(useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) &&
|
|
||||||
userContext.apiType === "Cassandra"
|
|
||||||
) {
|
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
|
|||||||
@@ -109,29 +109,15 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"computedProperties": [Function],
|
"computedProperties": [Function],
|
||||||
"conflictResolutionPolicy": [Function],
|
"conflictResolutionPolicy": [Function],
|
||||||
"container": Explorer {
|
"container": Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"databasesRefreshed": Promise {},
|
"databasesRefreshed": Promise {},
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
},
|
},
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"dataMaskingPolicy": [Function],
|
"dataMaskingPolicy": [Function],
|
||||||
"databaseId": "test",
|
"databaseId": "test",
|
||||||
@@ -230,29 +216,15 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"computedProperties": [Function],
|
"computedProperties": [Function],
|
||||||
"conflictResolutionPolicy": [Function],
|
"conflictResolutionPolicy": [Function],
|
||||||
"container": Explorer {
|
"container": Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"databasesRefreshed": Promise {},
|
"databasesRefreshed": Promise {},
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
},
|
},
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"dataMaskingPolicy": [Function],
|
"dataMaskingPolicy": [Function],
|
||||||
"databaseId": "test",
|
"databaseId": "test",
|
||||||
@@ -456,29 +428,15 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"computedProperties": [Function],
|
"computedProperties": [Function],
|
||||||
"conflictResolutionPolicy": [Function],
|
"conflictResolutionPolicy": [Function],
|
||||||
"container": Explorer {
|
"container": Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"databasesRefreshed": Promise {},
|
"databasesRefreshed": Promise {},
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
},
|
},
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"dataMaskingPolicy": [Function],
|
"dataMaskingPolicy": [Function],
|
||||||
"databaseId": "test",
|
"databaseId": "test",
|
||||||
@@ -527,29 +485,15 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
}
|
}
|
||||||
explorer={
|
explorer={
|
||||||
Explorer {
|
Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"databasesRefreshed": Promise {},
|
"databasesRefreshed": Promise {},
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
},
|
},
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isReadOnly={false}
|
isReadOnly={false}
|
||||||
@@ -695,29 +639,15 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"computedProperties": [Function],
|
"computedProperties": [Function],
|
||||||
"conflictResolutionPolicy": [Function],
|
"conflictResolutionPolicy": [Function],
|
||||||
"container": Explorer {
|
"container": Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"databasesRefreshed": Promise {},
|
"databasesRefreshed": Promise {},
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
},
|
},
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"dataMaskingPolicy": [Function],
|
"dataMaskingPolicy": [Function],
|
||||||
"databaseId": "test",
|
"databaseId": "test",
|
||||||
@@ -766,29 +696,15 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
}
|
}
|
||||||
explorer={
|
explorer={
|
||||||
Explorer {
|
Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"databasesRefreshed": Promise {},
|
"databasesRefreshed": Promise {},
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
},
|
},
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ jest.mock("./Controls/Dialog", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Silence useNotebook subscription calls
|
// Silence useSynapseLink state calls
|
||||||
jest.mock("./Notebook/useNotebook", () => ({
|
jest.mock("./useSynapseLink", () => ({
|
||||||
useNotebook: {
|
useSynapseLink: {
|
||||||
subscribe: jest.fn(),
|
subscribe: jest.fn(),
|
||||||
getState: jest.fn().mockReturnValue(
|
getState: jest.fn().mockReturnValue(
|
||||||
new Proxy(
|
new Proxy(
|
||||||
|
|||||||
+6
-226
@@ -1,9 +1,6 @@
|
|||||||
import * as msal from "@azure/msal-browser";
|
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 { sendMessage } from "Common/MessageHandler";
|
||||||
import { stringifyError } from "Common/stringifyError";
|
import { stringifyError } from "Common/stringifyError";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||||
import {
|
import {
|
||||||
@@ -13,32 +10,27 @@ import {
|
|||||||
scheduleRefreshFabricToken,
|
scheduleRefreshFabricToken,
|
||||||
} from "Platform/Fabric/FabricUtil";
|
} from "Platform/Fabric/FabricUtil";
|
||||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
|
||||||
import { featureRegistered } from "Utils/FeatureRegistrationUtils";
|
import { featureRegistered } from "Utils/FeatureRegistrationUtils";
|
||||||
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
import shallow from "zustand/shallow";
|
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
|
|
||||||
import { getErrorMessage, getErrorStack } from "../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../Common/ErrorHandlingUtils";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { QueriesClient } from "../Common/QueriesClient";
|
import { QueriesClient } from "../Common/QueriesClient";
|
||||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||||
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import { ContainerConnectionInfo, IPhoenixServiceInfo, IProvisionData, IResponse } from "../Contracts/DataModels";
|
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { UploadDetailsRecord } from "../Contracts/ViewModels";
|
import { UploadDetailsRecord } from "../Contracts/ViewModels";
|
||||||
import MetricScenario from "../Metrics/MetricEvents";
|
import MetricScenario from "../Metrics/MetricEvents";
|
||||||
import { ApplicationMetricPhase } from "../Metrics/ScenarioConfig";
|
import { ApplicationMetricPhase } from "../Metrics/ScenarioConfig";
|
||||||
import { scenarioMonitor } from "../Metrics/ScenarioMonitor";
|
import { scenarioMonitor } from "../Metrics/ScenarioMonitor";
|
||||||
import { PhoenixClient } from "../Phoenix/PhoenixClient";
|
|
||||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
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 * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { updateUserContext, userContext } from "../UserContext";
|
import { updateUserContext, userContext } from "../UserContext";
|
||||||
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||||
@@ -49,8 +41,6 @@ import { ReactTabKind, useTabs } from "../hooks/useTabs";
|
|||||||
import "./ComponentRegisterer";
|
import "./ComponentRegisterer";
|
||||||
import { DialogProps, useDialog } from "./Controls/Dialog";
|
import { DialogProps, useDialog } from "./Controls/Dialog";
|
||||||
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import type NotebookManager from "./Notebook/NotebookManager";
|
|
||||||
import { useNotebook } from "./Notebook/useNotebook";
|
|
||||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel";
|
import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel";
|
||||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||||
@@ -60,10 +50,10 @@ import TabsBase from "./Tabs/TabsBase";
|
|||||||
import TerminalTab from "./Tabs/TerminalTab";
|
import TerminalTab from "./Tabs/TerminalTab";
|
||||||
import Database from "./Tree/Database";
|
import Database from "./Tree/Database";
|
||||||
import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
|
import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
|
||||||
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
|
|
||||||
import StoredProcedure from "./Tree/StoredProcedure";
|
import StoredProcedure from "./Tree/StoredProcedure";
|
||||||
import { useDatabases } from "./useDatabases";
|
import { useDatabases } from "./useDatabases";
|
||||||
import { useSelectedNode } from "./useSelectedNode";
|
import { useSelectedNode } from "./useSelectedNode";
|
||||||
|
import { useSynapseLink } from "./useSynapseLink";
|
||||||
|
|
||||||
BindingHandlersRegisterer.registerBindingHandlers();
|
BindingHandlersRegisterer.registerBindingHandlers();
|
||||||
|
|
||||||
@@ -72,19 +62,10 @@ export default class Explorer {
|
|||||||
public queriesClient: QueriesClient;
|
public queriesClient: QueriesClient;
|
||||||
public tableDataClient: TableDataClient;
|
public tableDataClient: TableDataClient;
|
||||||
|
|
||||||
// Resource Tree
|
|
||||||
private resourceTree: ResourceTreeAdapter;
|
|
||||||
|
|
||||||
// Tabs
|
// Tabs
|
||||||
public isTabsContentExpanded: ko.Observable<boolean>;
|
public isTabsContentExpanded: ko.Observable<boolean>;
|
||||||
|
|
||||||
// Notebooks
|
|
||||||
public notebookManager?: NotebookManager;
|
|
||||||
|
|
||||||
private _isInitializingNotebooks: boolean;
|
|
||||||
|
|
||||||
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
||||||
public phoenixClient: PhoenixClient;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves when the initial refreshAllDatabases (including collection loading) completes.
|
* Resolves when the initial refreshAllDatabases (including collection loading) completes.
|
||||||
@@ -95,13 +76,6 @@ export default class Explorer {
|
|||||||
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||||
});
|
});
|
||||||
this._isInitializingNotebooks = false;
|
|
||||||
|
|
||||||
this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
|
|
||||||
useNotebook.subscribe(
|
|
||||||
() => this.refreshCommandBarButtons(),
|
|
||||||
(state) => state.isNotebooksEnabledForAccount,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.queriesClient = new QueriesClient(this);
|
this.queriesClient = new QueriesClient(this);
|
||||||
|
|
||||||
@@ -156,31 +130,6 @@ export default class Explorer {
|
|||||||
startKey,
|
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()) {
|
if (isFabricMirrored()) {
|
||||||
useTabs.getState().closeReactTab(ReactTabKind.Home);
|
useTabs.getState().closeReactTab(ReactTabKind.Home);
|
||||||
}
|
}
|
||||||
@@ -188,23 +137,6 @@ export default class Explorer {
|
|||||||
this.refreshExplorer();
|
this.refreshExplorer();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async initiateAndRefreshNotebookList(): Promise<void> {
|
|
||||||
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 {
|
public openEnableSynapseLinkDialog(targetAccountOverride?: DataModels.AccountOverride): void {
|
||||||
const subscriptionId = targetAccountOverride?.subscriptionId ?? userContext.subscriptionId;
|
const subscriptionId = targetAccountOverride?.subscriptionId ?? userContext.subscriptionId;
|
||||||
const resourceGroup = targetAccountOverride?.resourceGroup ?? userContext.resourceGroup;
|
const resourceGroup = targetAccountOverride?.resourceGroup ?? userContext.resourceGroup;
|
||||||
@@ -227,7 +159,7 @@ export default class Explorer {
|
|||||||
const clearInProgressMessage = logConsoleProgress(
|
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.",
|
"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();
|
useDialog.getState().closeDialog();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -248,7 +180,7 @@ export default class Explorer {
|
|||||||
logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`);
|
logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`);
|
||||||
TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, {}, startTime);
|
TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, {}, startTime);
|
||||||
} finally {
|
} finally {
|
||||||
useNotebook.getState().setIsSynapseLinkUpdating(false);
|
useSynapseLink.getState().setIsSynapseLinkUpdating(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -431,7 +363,6 @@ export default class Explorer {
|
|||||||
await (userContext.authType === AuthType.ResourceToken
|
await (userContext.authType === AuthType.ResourceToken
|
||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
: this.refreshAllDatabases());
|
: this.refreshAllDatabases());
|
||||||
await this.refreshNotebookList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logConsoleInfo("Successfully refreshed databases");
|
logConsoleInfo("Successfully refreshed databases");
|
||||||
@@ -442,108 +373,6 @@ export default class Explorer {
|
|||||||
window.open(Constants.Urls.feedbackEmail, "_blank");
|
window.open(Constants.Urls.feedbackEmail, "_blank");
|
||||||
};
|
};
|
||||||
|
|
||||||
public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise<void> {
|
|
||||||
if (!databaseAccount) {
|
|
||||||
throw new Error("No database account specified");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._isInitializingNotebooks) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._isInitializingNotebooks = true;
|
|
||||||
this.refreshNotebookList();
|
|
||||||
this._isInitializingNotebooks = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async allocateContainer(): Promise<void> {
|
|
||||||
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<IPhoenixServiceInfo>,
|
|
||||||
connectionStatus: DataModels.ContainerConnectionInfo,
|
|
||||||
): Promise<void> {
|
|
||||||
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(
|
private getDeltaDatabases(
|
||||||
updatedDatabaseList: DataModels.Database[],
|
updatedDatabaseList: DataModels.Database[],
|
||||||
databases: ViewModels.Database[],
|
databases: ViewModels.Database[],
|
||||||
@@ -637,11 +466,6 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private refreshNotebookList = (): Promise<void> => {
|
|
||||||
// Notebook authoring and listing have been removed.
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
public openNotebookTerminal(kind: ViewModels.TerminalKind): void {
|
public openNotebookTerminal(kind: ViewModels.TerminalKind): void {
|
||||||
this.connectToNotebookTerminal(kind);
|
this.connectToNotebookTerminal(kind);
|
||||||
}
|
}
|
||||||
@@ -738,24 +562,6 @@ export default class Explorer {
|
|||||||
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
|
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDownloadModalContent(fileName: string): JSX.Element {
|
|
||||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p>{Notebook.galleryNotebookDownloadContent1}</p>
|
|
||||||
<br />
|
|
||||||
<p>
|
|
||||||
{Notebook.galleryNotebookDownloadContent2}
|
|
||||||
<Link href={Notebook.cosmosNotebookGitDocumentationUrl} target="_blank">
|
|
||||||
{Notebook.learnMore}
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <p> Download {fileName} from gallery as a copy to your notebooks to run and/or edit the notebook. </p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async refreshExplorer(): Promise<void> {
|
public async refreshExplorer(): Promise<void> {
|
||||||
// Start DatabaseLoad scenario before fetching databases
|
// Start DatabaseLoad scenario before fetching databases
|
||||||
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
||||||
@@ -764,8 +570,7 @@ export default class Explorer {
|
|||||||
|
|
||||||
// Run independent initialization tasks in parallel:
|
// Run independent initialization tasks in parallel:
|
||||||
// - Database loading (ARM/SDK calls for databases + collections)
|
// - 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)
|
||||||
// - Feature registration check (ARM call — no dependency on databases or notebooks)
|
|
||||||
const databasesTask =
|
const databasesTask =
|
||||||
userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo"
|
userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo"
|
||||||
? (async () => {
|
? (async () => {
|
||||||
@@ -782,37 +587,12 @@ export default class Explorer {
|
|||||||
})()
|
})()
|
||||||
: Promise.resolve();
|
: Promise.resolve();
|
||||||
|
|
||||||
const notebooksTask = !isFabricNative()
|
|
||||||
? useNotebook.getState().refreshNotebooksEnabledStateForAccount()
|
|
||||||
: Promise.resolve();
|
|
||||||
|
|
||||||
const featureRegistrationTask =
|
const featureRegistrationTask =
|
||||||
userContext.authType === AuthType.AAD && userContext.apiType === "SQL" && !isFabricNative()
|
userContext.authType === AuthType.AAD && userContext.apiType === "SQL" && !isFabricNative()
|
||||||
? featureRegistered(userContext.subscriptionId, "ThroughputBucketing")
|
? featureRegistered(userContext.subscriptionId, "ThroughputBucketing")
|
||||||
: Promise.resolve(false);
|
: Promise.resolve(false);
|
||||||
|
|
||||||
const [, , throughputBucketsEnabled] = await Promise.all([databasesTask, notebooksTask, featureRegistrationTask]);
|
const [, throughputBucketsEnabled] = await Promise.all([databasesTask, 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (throughputBucketsEnabled) {
|
if (throughputBucketsEnabled) {
|
||||||
updateUserContext({ throughputBucketsEnabled });
|
updateUserContext({ throughputBucketsEnabled });
|
||||||
|
|||||||
@@ -5,14 +5,12 @@
|
|||||||
*/
|
*/
|
||||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||||
import { makeStyles, useFluent } from "@fluentui/react-components";
|
import { makeStyles, useFluent } from "@fluentui/react-components";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
|
||||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import { ConnectionStatusType } from "../../../Common/Constants";
|
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
@@ -75,12 +73,6 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
||||||
const styles = useStyles();
|
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
|
// Subscribe to the store changes that affect button creation
|
||||||
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
||||||
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
||||||
@@ -134,11 +126,6 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, "var(--colorNeutralBackground1)");
|
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, "var(--colorNeutralBackground1)");
|
||||||
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
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 = {
|
const rootStyle = {
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
backgroundColor: "var(--colorNeutralBackground1)",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { DatabaseAccount } from "../../../Contracts/DataModels";
|
|||||||
import { CollectionBase } from "../../../Contracts/ViewModels";
|
import { CollectionBase } from "../../../Contracts/ViewModels";
|
||||||
import { updateUserContext } from "../../../UserContext";
|
import { updateUserContext } from "../../../UserContext";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
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", () => {
|
it("Cassandra Api not available - button should be hidden", () => {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
databaseAccount: {
|
databaseAccount: {
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
|||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
|
||||||
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||||
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
||||||
import { SettingsPane, useDataPlaneRbac } from "../../Panes/SettingsPane/SettingsPane";
|
import { SettingsPane, useDataPlaneRbac } from "../../Panes/SettingsPane/SettingsPane";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
import { SelectedNodeState } from "../../useSelectedNode";
|
import { SelectedNodeState } from "../../useSelectedNode";
|
||||||
|
import { useSynapseLink } from "../../useSynapseLink";
|
||||||
import { ThemeToggleButton } from "./ThemeToggleButton";
|
import { ThemeToggleButton } from "./ThemeToggleButton";
|
||||||
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
@@ -121,14 +121,13 @@ export function createContextCommandBarButtons(
|
|||||||
const buttons: CommandButtonComponentProps[] = [];
|
const buttons: CommandButtonComponentProps[] = [];
|
||||||
|
|
||||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
||||||
const label =
|
const label = userContext.features.enableCloudShell ? "Open Mongo Shell" : "New Shell";
|
||||||
useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell ? "Open Mongo Shell" : "New Shell";
|
|
||||||
const newMongoShellBtn: CommandButtonComponentProps = {
|
const newMongoShellBtn: CommandButtonComponentProps = {
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
if (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) {
|
if (userContext.features.enableCloudShell) {
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||||
} else {
|
} else {
|
||||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||||
@@ -142,7 +141,7 @@ export function createContextCommandBarButtons(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) &&
|
userContext.features.enableCloudShell &&
|
||||||
!selectedNodeState.isDatabaseNodeOrNoneSelected() &&
|
!selectedNodeState.isDatabaseNodeOrNoneSelected() &&
|
||||||
userContext.apiType === "Cassandra"
|
userContext.apiType === "Cassandra"
|
||||||
) {
|
) {
|
||||||
@@ -258,7 +257,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
|
|||||||
onCommandClick: () => container.openEnableSynapseLinkDialog(),
|
onCommandClick: () => container.openEnableSynapseLinkDialog(),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: useNotebook.getState().isSynapseLinkUpdating,
|
disabled: useSynapseLink.getState().isSynapseLinkUpdating,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -499,13 +498,12 @@ function createOpenTerminalButtonByKind(
|
|||||||
const label = `Open ${terminalFriendlyName()} shell`;
|
const label = `Open ${terminalFriendlyName()} shell`;
|
||||||
const tooltip =
|
const tooltip =
|
||||||
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||||
const disableButton =
|
const disableButton = !userContext.features.enableCloudShell;
|
||||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
|
||||||
return {
|
return {
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
if (useNotebook.getState().isNotebookEnabled || userContext.features.enableCloudShell) {
|
if (userContext.features.enableCloudShell) {
|
||||||
container.openNotebookTerminal(terminalKind);
|
container.openNotebookTerminal(terminalKind);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ import { configContext, Platform } from "../../../ConfigContext";
|
|||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
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
|
* 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: () => <MemoryTracker />,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createConnectionStatus = (container: Explorer, key: string): ICommandBarItemProps => {
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
onRender: () => <ConnectionStatus container={container} />,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap {
|
export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap {
|
||||||
const handlers: KeyboardHandlerMap = {};
|
const handlers: KeyboardHandlerMap = {};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<Props> = ({ 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<boolean>(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 (
|
|
||||||
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
|
|
||||||
<TooltipHost content={toolTipContent}>
|
|
||||||
<Stack className="connectionStatusContainer" horizontal>
|
|
||||||
<Icon iconName="ConnectVirtualMachine" className="connectIcon" />
|
|
||||||
<span>{connectionInfo.status}</span>
|
|
||||||
</Stack>
|
|
||||||
</TooltipHost>
|
|
||||||
</ActionButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<TooltipHost
|
|
||||||
content={
|
|
||||||
containerInfo?.status === ContainerStatusType.Active
|
|
||||||
? `Connected to temporary workspace. This temporary workspace will get disconnected in ${Math.round(
|
|
||||||
containerInfo.durationLeftInMinutes,
|
|
||||||
)} minutes.`
|
|
||||||
: toolTipContent
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ActionButton
|
|
||||||
id={buttonId}
|
|
||||||
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
|
|
||||||
onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
|
|
||||||
connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Stack className="connectionStatusContainer" horizontal>
|
|
||||||
<i className={statusColor}></i>
|
|
||||||
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
|
|
||||||
{connectionInfo.status}
|
|
||||||
</span>
|
|
||||||
{connectionInfo.status === ConnectionStatusType.Connecting && isActive && (
|
|
||||||
<ProgressIndicator description={minute + ":" + second} />
|
|
||||||
)}
|
|
||||||
{connectionInfo.status === ConnectionStatusType.Connected && !isActive && (
|
|
||||||
<ProgressIndicator
|
|
||||||
className={totalGB !== 0 && usedGB / totalGB > 0.8 ? "lowMemory" : ""}
|
|
||||||
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
|
|
||||||
percentComplete={totalGB !== 0 ? usedGB / totalGB : 0}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
{!isBarDismissed &&
|
|
||||||
containerInfo.status &&
|
|
||||||
containerInfo.status === ContainerStatusType.Active &&
|
|
||||||
Math.round(containerInfo.durationLeftInMinutes) <= Notebook.remainingTimeForAlert ? (
|
|
||||||
<FocusTrapCallout
|
|
||||||
role="alertdialog"
|
|
||||||
className={styles.callout}
|
|
||||||
gapSpace={0}
|
|
||||||
target={`#${buttonId}`}
|
|
||||||
onDismiss={() => setIsBarDismissed(true)}
|
|
||||||
setInitialFocus
|
|
||||||
>
|
|
||||||
<Text block variant="xLarge" className={styles.title}>
|
|
||||||
Remaining Time
|
|
||||||
</Text>
|
|
||||||
<Text block variant="small">
|
|
||||||
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.
|
|
||||||
</Text>
|
|
||||||
<FocusZone handleTabKey={FocusZoneTabbableElements.all} isCircularNavigation>
|
|
||||||
<Stack className={styles.buttons} gap={8} horizontal>
|
|
||||||
<DefaultButton onClick={() => setIsBarDismissed(true)}>Dimiss</DefaultButton>
|
|
||||||
</Stack>
|
|
||||||
</FocusZone>
|
|
||||||
</FocusTrapCallout>
|
|
||||||
) : undefined}
|
|
||||||
</ActionButton>
|
|
||||||
</TooltipHost>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<Stack className="memoryTrackerContainer" horizontal>
|
|
||||||
<span>Memory</span>
|
|
||||||
<Spinner size={SpinnerSize.medium} />
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalGB = memoryUsageInfo.totalKB / 1048576;
|
|
||||||
const usedGB = totalGB - memoryUsageInfo.freeKB / 1048576;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack className="memoryTrackerContainer" horizontal>
|
|
||||||
<span>Memory</span>
|
|
||||||
<ProgressIndicator
|
|
||||||
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
|
|
||||||
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
|
|
||||||
percentComplete={usedGB / totalGB}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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<DataModels.MemoryUsageInfo> {
|
|
||||||
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<DataModels.MemoryUsageInfo> {
|
|
||||||
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<IResponse<IPhoenixServiceInfo>> {
|
|
||||||
this.isResettingWorkspace = true;
|
|
||||||
let response: IResponse<IPhoenixServiceInfo>;
|
|
||||||
try {
|
|
||||||
response = await this._resetWorkspace();
|
|
||||||
} catch (error) {
|
|
||||||
Promise.reject(error);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
this.isResettingWorkspace = false;
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _resetWorkspace(): Promise<IResponse<IPhoenixServiceInfo>> {
|
|
||||||
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",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<void>;
|
|
||||||
findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem;
|
|
||||||
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem) => void;
|
|
||||||
updateNotebookItem: (item: NotebookContentItem) => void;
|
|
||||||
deleteNotebookItem: (item: NotebookContentItem) => void;
|
|
||||||
initializeNotebooksTree: () => Promise<void>;
|
|
||||||
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => void;
|
|
||||||
setIsAllocating: (isAllocating: boolean) => void;
|
|
||||||
resetContainerConnection: (connectionStatus: ContainerConnectionInfo) => void;
|
|
||||||
setIsRefreshed: (isAllocating: boolean) => void;
|
|
||||||
setContainerStatus: (containerStatus: ContainerInfo) => void;
|
|
||||||
getPhoenixStatus: () => Promise<void>;
|
|
||||||
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => void;
|
|
||||||
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useNotebook: UseStore<NotebookState> = 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<void> => {
|
|
||||||
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<void> => {
|
|
||||||
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 }),
|
|
||||||
}));
|
|
||||||
-14
@@ -145,29 +145,15 @@ exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
|
|||||||
<AnalyticalStoreComponent
|
<AnalyticalStoreComponent
|
||||||
explorer={
|
explorer={
|
||||||
Explorer {
|
Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"databasesRefreshed": Promise {},
|
"databasesRefreshed": Promise {},
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
},
|
},
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setEnableAnalyticalStore={[Function]}
|
setEnableAnalyticalStore={[Function]}
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import { userContext } from "../../UserContext";
|
|||||||
import { getCollectionName } from "../../Utils/APITypeUtils";
|
import { getCollectionName } from "../../Utils/APITypeUtils";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
||||||
import { useNotebook } from "../Notebook/useNotebook";
|
|
||||||
import { useDatabases } from "../useDatabases";
|
import { useDatabases } from "../useDatabases";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
|
|
||||||
@@ -182,12 +181,6 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
{
|
|
||||||
dispose: useNotebook.subscribe(
|
|
||||||
() => setState({}),
|
|
||||||
(state) => state.isNotebookEnabled,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{ dispose: useSelectedNode.subscribe(() => setState({})) },
|
{ dispose: useSelectedNode.subscribe(() => setState({})) },
|
||||||
{
|
{
|
||||||
dispose: useCarousel.subscribe(
|
dispose: useCarousel.subscribe(
|
||||||
@@ -609,7 +602,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getRecentItems = (): JSX.Element => {
|
const getRecentItems = (): JSX.Element => {
|
||||||
const recentItems = createRecentItems()?.filter((item) => item.description !== "Notebook");
|
const recentItems = createRecentItems();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
TriggerDefinition,
|
TriggerDefinition,
|
||||||
UserDefinedFunctionDefinition,
|
UserDefinedFunctionDefinition,
|
||||||
} from "@azure/cosmos";
|
} from "@azure/cosmos";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
|
||||||
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
||||||
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
|
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
|
||||||
|
|||||||
@@ -18,12 +18,8 @@ import { userContext } from "UserContext";
|
|||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import shallow from "zustand/shallow";
|
|
||||||
import { useDatabaseLoadScenario } from "../../Metrics/useMetricPhases";
|
import { useDatabaseLoadScenario } from "../../Metrics/useMetricPhases";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { useNotebook } from "../Notebook/useNotebook";
|
|
||||||
|
|
||||||
export const MyNotebooksTitle = "My Notebooks";
|
|
||||||
|
|
||||||
interface ResourceTreeProps {
|
interface ResourceTreeProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
@@ -40,13 +36,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
|
|||||||
const [openItems, setOpenItems] = React.useState<TreeItemValue[]>([]);
|
const [openItems, setOpenItems] = React.useState<TreeItemValue[]>([]);
|
||||||
const treeStyles = useTreeStyles();
|
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.
|
// We intentionally avoid using a state selector here because we want to re-render the tree if the active tab changes.
|
||||||
const { refreshActiveTab } = useTabs();
|
const { refreshActiveTab } = useTabs();
|
||||||
|
|
||||||
@@ -65,24 +54,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
|
|||||||
const databaseTreeNodes = useMemo(() => {
|
const databaseTreeNodes = useMemo(() => {
|
||||||
return userContext.authType === AuthType.ResourceToken
|
return userContext.authType === AuthType.ResourceToken
|
||||||
? createResourceTokenTreeNodes(resourceTokenCollection)
|
? createResourceTokenTreeNodes(resourceTokenCollection)
|
||||||
: createDatabaseTreeNodes(
|
: createDatabaseTreeNodes(explorer, databases, refreshActiveTab, searchText, sortOrder, pinnedDatabaseIds);
|
||||||
explorer,
|
}, [resourceTokenCollection, databases, refreshActiveTab, searchText, sortOrder, pinnedDatabaseIds]);
|
||||||
isNotebookEnabled,
|
|
||||||
databases,
|
|
||||||
refreshActiveTab,
|
|
||||||
searchText,
|
|
||||||
sortOrder,
|
|
||||||
pinnedDatabaseIds,
|
|
||||||
);
|
|
||||||
}, [
|
|
||||||
resourceTokenCollection,
|
|
||||||
databases,
|
|
||||||
isNotebookEnabled,
|
|
||||||
refreshActiveTab,
|
|
||||||
searchText,
|
|
||||||
sortOrder,
|
|
||||||
pinnedDatabaseIds,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const headerNodes: TreeNode[] = isFabricMirrored()
|
const headerNodes: TreeNode[] = isFabricMirrored()
|
||||||
? []
|
? []
|
||||||
|
|||||||
@@ -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(<LegacyTreeComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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<number>;
|
|
||||||
|
|
||||||
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 <LegacyTreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async initialize(): Promise<void[]> {
|
|
||||||
const refreshTasks: Promise<void>[] = [];
|
|
||||||
|
|
||||||
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()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Resource tree for schema should render 1`] = `
|
|
||||||
<div
|
|
||||||
className="treeComponent dataResourceTree"
|
|
||||||
role="tree"
|
|
||||||
>
|
|
||||||
<LegacyTreeNodeComponent
|
|
||||||
generation={0}
|
|
||||||
node={
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "String",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "_rid",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "Int64",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "_ts",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "String",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "id",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "String",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "pk",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "String",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "other",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "String",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "Int64",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "someNumber",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "Double",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "anotherNumber",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "nested",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "String",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "Int64",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "someNumber",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "Double",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "anotherNumber",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "items",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "list",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "items",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "String",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "_etag",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "Schema",
|
|
||||||
"onClick": [Function],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
paddingLeft={0}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -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": [
|
"children": [
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
|||||||
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
|
||||||
import { DeleteDatabaseConfirmationPanel } from "Explorer/Panes/DeleteDatabaseConfirmationPanel";
|
import { DeleteDatabaseConfirmationPanel } from "Explorer/Panes/DeleteDatabaseConfirmationPanel";
|
||||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||||
import StoredProcedure from "Explorer/Tree/StoredProcedure";
|
import StoredProcedure from "Explorer/Tree/StoredProcedure";
|
||||||
@@ -363,7 +362,7 @@ describe("createDatabaseTreeNodes", () => {
|
|||||||
},
|
},
|
||||||
} as never,
|
} as never,
|
||||||
});
|
});
|
||||||
nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab, "");
|
nodes = createDatabaseTreeNodes(explorer, useDatabases.getState().databases, refreshActiveTab, "");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates expected tree", () => {
|
it("creates expected tree", () => {
|
||||||
@@ -371,11 +370,10 @@ describe("createDatabaseTreeNodes", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each<[string, Platform, boolean, Partial<DataModels.DatabaseAccountExtendedProperties>, Partial<UserContext>]>([
|
it.each<[string, Platform, Partial<DataModels.DatabaseAccountExtendedProperties>, Partial<UserContext>]>([
|
||||||
[
|
[
|
||||||
"the SQL API, on Fabric read-only (mirrored)",
|
"the SQL API, on Fabric read-only (mirrored)",
|
||||||
Platform.Fabric,
|
Platform.Fabric,
|
||||||
false,
|
|
||||||
{ capabilities: [], enableMultipleWriteLocations: true },
|
{ capabilities: [], enableMultipleWriteLocations: true },
|
||||||
{
|
{
|
||||||
fabricContext: {
|
fabricContext: {
|
||||||
@@ -387,7 +385,6 @@ describe("createDatabaseTreeNodes", () => {
|
|||||||
[
|
[
|
||||||
"the SQL API, on Fabric non read-only (native)",
|
"the SQL API, on Fabric non read-only (native)",
|
||||||
Platform.Fabric,
|
Platform.Fabric,
|
||||||
false,
|
|
||||||
{ capabilities: [], enableMultipleWriteLocations: true },
|
{ capabilities: [], enableMultipleWriteLocations: true },
|
||||||
{
|
{
|
||||||
fabricContext: {
|
fabricContext: {
|
||||||
@@ -399,7 +396,6 @@ describe("createDatabaseTreeNodes", () => {
|
|||||||
[
|
[
|
||||||
"the SQL API, on Portal",
|
"the SQL API, on Portal",
|
||||||
Platform.Portal,
|
Platform.Portal,
|
||||||
false,
|
|
||||||
{ capabilities: [], enableMultipleWriteLocations: true },
|
{ capabilities: [], enableMultipleWriteLocations: true },
|
||||||
{
|
{
|
||||||
fabricContext: undefined,
|
fabricContext: undefined,
|
||||||
@@ -408,7 +404,6 @@ describe("createDatabaseTreeNodes", () => {
|
|||||||
[
|
[
|
||||||
"the Cassandra API, serverless, on Hosted",
|
"the Cassandra API, serverless, on Hosted",
|
||||||
Platform.Hosted,
|
Platform.Hosted,
|
||||||
false,
|
|
||||||
{
|
{
|
||||||
capabilities: [
|
capabilities: [
|
||||||
{ name: CapabilityNames.EnableCassandra, description: "" },
|
{ name: CapabilityNames.EnableCassandra, description: "" },
|
||||||
@@ -418,45 +413,34 @@ describe("createDatabaseTreeNodes", () => {
|
|||||||
{ fabricContext: undefined },
|
{ fabricContext: undefined },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"the Mongo API, with Notebooks and Phoenix features, on Emulator",
|
"the Mongo API, on Emulator",
|
||||||
Platform.Emulator,
|
Platform.Emulator,
|
||||||
true,
|
|
||||||
{
|
{
|
||||||
capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }],
|
capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }],
|
||||||
},
|
},
|
||||||
{ fabricContext: undefined },
|
{ fabricContext: undefined },
|
||||||
],
|
],
|
||||||
])(
|
])("generates the correct tree structure for %s", (_, platform, dbAccountProperties, userContext) => {
|
||||||
"generates the correct tree structure for %s",
|
updateConfigContext({ platform });
|
||||||
(_, platform, isNotebookEnabled, dbAccountProperties, userContext) => {
|
updateUserContext({
|
||||||
useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled });
|
...userContext,
|
||||||
updateConfigContext({ platform });
|
databaseAccount: {
|
||||||
updateUserContext({
|
properties: {
|
||||||
...userContext,
|
enableMultipleWriteLocations: true,
|
||||||
databaseAccount: {
|
...dbAccountProperties,
|
||||||
properties: {
|
},
|
||||||
enableMultipleWriteLocations: true,
|
} as unknown as DataModels.DatabaseAccount,
|
||||||
...dbAccountProperties,
|
});
|
||||||
},
|
const nodes = createDatabaseTreeNodes(explorer, useDatabases.getState().databases, refreshActiveTab, "");
|
||||||
} as unknown as DataModels.DatabaseAccount,
|
expect(nodes).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
const nodes = createDatabaseTreeNodes(
|
|
||||||
explorer,
|
|
||||||
isNotebookEnabled,
|
|
||||||
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.
|
// 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.
|
// 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.
|
// 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", () => {
|
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());
|
const giganticDbNode = nodes.find((node) => node.label === giganticDb.id());
|
||||||
expect(giganticDbNode).toBeDefined();
|
expect(giganticDbNode).toBeDefined();
|
||||||
expect(giganticDbNode.children.map((node) => node.label)).toStrictEqual(["schemaCollection", "load more"]);
|
expect(giganticDbNode.children.map((node) => node.label)).toStrictEqual(["schemaCollection", "load more"]);
|
||||||
@@ -488,7 +472,7 @@ describe("createDatabaseTreeNodes", () => {
|
|||||||
},
|
},
|
||||||
} as unknown as DataModels.DatabaseAccount,
|
} 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());
|
standardDbNode = nodes.find((node) => node.label === standardDb.id());
|
||||||
sharedDbNode = nodes.find((node) => node.label === sharedDb.id());
|
sharedDbNode = nodes.find((node) => node.label === sharedDb.id());
|
||||||
giganticDbNode = nodes.find((node) => node.label === giganticDb.id());
|
giganticDbNode = nodes.find((node) => node.label === giganticDb.id());
|
||||||
@@ -643,7 +627,7 @@ describe("createDatabaseTreeNodes", () => {
|
|||||||
setup();
|
setup();
|
||||||
|
|
||||||
// Rebuild the nodes after changing the user/config context.
|
// 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());
|
standardDbNode = nodes.find((node) => node.label === standardDb.id());
|
||||||
standardCollectionNode = standardDbNode.children.find((node) => node.label === standardCollection.id());
|
standardCollectionNode = standardDbNode.children.find((node) => node.label === standardCollection.id());
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,6 @@ export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBa
|
|||||||
|
|
||||||
export const createDatabaseTreeNodes = (
|
export const createDatabaseTreeNodes = (
|
||||||
container: Explorer,
|
container: Explorer,
|
||||||
isNotebookEnabled: boolean,
|
|
||||||
databases: ViewModels.Database[],
|
databases: ViewModels.Database[],
|
||||||
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void,
|
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void,
|
||||||
searchText = "",
|
searchText = "",
|
||||||
@@ -184,9 +183,7 @@ export const createDatabaseTreeNodes = (
|
|||||||
database
|
database
|
||||||
.collections()
|
.collections()
|
||||||
.forEach((collection: ViewModels.Collection) =>
|
.forEach((collection: ViewModels.Collection) =>
|
||||||
databaseNode.children.push(
|
databaseNode.children.push(buildCollectionNode(database, collection, container, refreshActiveTab)),
|
||||||
buildCollectionNode(database, collection, isNotebookEnabled, container, refreshActiveTab),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (database.collectionsContinuationToken) {
|
if (database.collectionsContinuationToken) {
|
||||||
@@ -254,14 +251,13 @@ export const createDatabaseTreeNodes = (
|
|||||||
export const buildCollectionNode = (
|
export const buildCollectionNode = (
|
||||||
database: ViewModels.Database,
|
database: ViewModels.Database,
|
||||||
collection: ViewModels.Collection,
|
collection: ViewModels.Collection,
|
||||||
isNotebookEnabled: boolean,
|
|
||||||
container: Explorer,
|
container: Explorer,
|
||||||
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void,
|
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void,
|
||||||
): TreeNode => {
|
): TreeNode => {
|
||||||
let children: TreeNode[];
|
let children: TreeNode[];
|
||||||
// Flat Tree for Fabric
|
// Flat Tree for Fabric
|
||||||
if (!isFabricMirrored()) {
|
if (!isFabricMirrored()) {
|
||||||
children = buildCollectionNodeChildren(database, collection, isNotebookEnabled, container, refreshActiveTab);
|
children = buildCollectionNodeChildren(database, collection, container, refreshActiveTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
const collectionNode: TreeNode = {
|
const collectionNode: TreeNode = {
|
||||||
@@ -310,7 +306,6 @@ export const buildCollectionNode = (
|
|||||||
const buildCollectionNodeChildren = (
|
const buildCollectionNodeChildren = (
|
||||||
database: ViewModels.Database,
|
database: ViewModels.Database,
|
||||||
collection: ViewModels.Collection,
|
collection: ViewModels.Collection,
|
||||||
isNotebookEnabled: boolean,
|
|
||||||
container: Explorer,
|
container: Explorer,
|
||||||
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void,
|
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void,
|
||||||
): TreeNode[] => {
|
): TreeNode[] => {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { ConnectionStatusType } from "Common/Constants";
|
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { useTabs } from "../hooks/useTabs";
|
import { useTabs } from "../hooks/useTabs";
|
||||||
@@ -13,7 +11,6 @@ export interface SelectedNodeState {
|
|||||||
collectionId?: string,
|
collectionId?: string,
|
||||||
subnodeKinds?: ViewModels.CollectionTabKind[],
|
subnodeKinds?: ViewModels.CollectionTabKind[],
|
||||||
) => boolean;
|
) => boolean;
|
||||||
isConnectedToContainer: () => boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) => ({
|
export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) => ({
|
||||||
@@ -61,7 +58,4 @@ export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) =>
|
|||||||
subnodeKinds.includes(selectedSubnodeKind)
|
subnodeKinds.includes(selectedSubnodeKind)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
isConnectedToContainer: (): boolean => {
|
|
||||||
return useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected;
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import create, { UseStore } from "zustand";
|
||||||
|
|
||||||
|
interface SynapseLinkState {
|
||||||
|
isSynapseLinkUpdating: boolean;
|
||||||
|
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSynapseLink: UseStore<SynapseLinkState> = create((set) => ({
|
||||||
|
isSynapseLinkUpdating: false,
|
||||||
|
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }),
|
||||||
|
}));
|
||||||
@@ -52,8 +52,6 @@ import { ErrorBoundary } from "./Explorer/ErrorBoundary";
|
|||||||
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
||||||
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
|
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
|
||||||
import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
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 "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
|
||||||
import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import "./Explorer/Panes/PanelComponent.less";
|
import "./Explorer/Panes/PanelComponent.less";
|
||||||
|
|||||||
@@ -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<IResponse<IPhoenixServiceInfo>> {
|
|
||||||
this.initializeCancelEventListener();
|
|
||||||
|
|
||||||
return promiseRetry(() => this.executeContainerAssignmentOperation(provisionData, "allocate"), {
|
|
||||||
retries: 4,
|
|
||||||
maxTimeout: 20000,
|
|
||||||
minTimeout: 20000,
|
|
||||||
signal: this.abortSignal,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async resetContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
|
|
||||||
return this.executeContainerAssignmentOperation(provisionData, "reset");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async executeContainerAssignmentOperation(
|
|
||||||
provisionData: IProvisionData,
|
|
||||||
operation: string,
|
|
||||||
): Promise<IResponse<IPhoenixServiceInfo>> {
|
|
||||||
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<ContainerInfo> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<void> => {
|
|
||||||
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<void> => {
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
@@ -83,12 +83,6 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const effect = async () => {
|
const effect = async () => {
|
||||||
if (platform) {
|
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;
|
let explorer: Explorer;
|
||||||
try {
|
try {
|
||||||
if (platform === Platform.Hosted) {
|
if (platform === Platform.Hosted) {
|
||||||
@@ -999,15 +993,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
|||||||
if (inputs.flights.indexOf(Flights.PKPartitionKeyTest) !== -1) {
|
if (inputs.flights.indexOf(Flights.PKPartitionKeyTest) !== -1) {
|
||||||
userContext.features.partitionKeyDefault2 = true;
|
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
|
// Handle initial theme from portal
|
||||||
|
|||||||
@@ -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<NotebookSnapshotHooks> = create((set) => ({
|
|
||||||
snapshot: undefined,
|
|
||||||
error: undefined,
|
|
||||||
setSnapshot: (imageSrc: string) => set((state) => ({ ...state, snapshot: imageSrc })),
|
|
||||||
setError: (error: string) => set((state) => ({ ...state, error })),
|
|
||||||
}));
|
|
||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
} from "Shared/AppStatePersistenceUtility";
|
} from "Shared/AppStatePersistenceUtility";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { CollectionTabKind } from "../Contracts/ViewModels";
|
|
||||||
import TabsBase from "../Explorer/Tabs/TabsBase";
|
import TabsBase from "../Explorer/Tabs/TabsBase";
|
||||||
|
|
||||||
export interface TabsState {
|
export interface TabsState {
|
||||||
@@ -27,7 +26,6 @@ export interface TabsState {
|
|||||||
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void;
|
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void;
|
||||||
closeTabsByComparator: (comparator: (tab: TabsBase) => boolean) => void;
|
closeTabsByComparator: (comparator: (tab: TabsBase) => boolean) => void;
|
||||||
closeTab: (tab: TabsBase) => void;
|
closeTab: (tab: TabsBase) => void;
|
||||||
closeAllNotebookTabs: (hardClose: boolean) => void;
|
|
||||||
openAndActivateReactTab: (tabKind: ReactTabKind) => void;
|
openAndActivateReactTab: (tabKind: ReactTabKind) => void;
|
||||||
closeReactTab: (tabKind: ReactTabKind) => void;
|
closeReactTab: (tabKind: ReactTabKind) => void;
|
||||||
setIsTabExecuting: (state: boolean) => void;
|
setIsTabExecuting: (state: boolean) => void;
|
||||||
@@ -132,33 +130,6 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
|||||||
|
|
||||||
get().persistTabsState();
|
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) => {
|
openAndActivateReactTab: (tabKind: ReactTabKind) => {
|
||||||
if (get().openedReactTabs.indexOf(tabKind) === -1) {
|
if (get().openedReactTabs.indexOf(tabKind) === -1) {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
|
|||||||
@@ -43,7 +43,6 @@
|
|||||||
"./src/Explorer/Graph/GraphExplorerComponent/GraphData.ts",
|
"./src/Explorer/Graph/GraphExplorerComponent/GraphData.ts",
|
||||||
"./src/Explorer/LazyMonaco.ts",
|
"./src/Explorer/LazyMonaco.ts",
|
||||||
"./src/Explorer/Menus/NotificationConsole/ConsoleData.tsx",
|
"./src/Explorer/Menus/NotificationConsole/ConsoleData.tsx",
|
||||||
"./src/Explorer/Notebook/NotebookContentItem.ts",
|
|
||||||
"./src/Explorer/OpenFullScreen.test.tsx",
|
"./src/Explorer/OpenFullScreen.test.tsx",
|
||||||
"./src/Explorer/OpenFullScreen.tsx",
|
"./src/Explorer/OpenFullScreen.tsx",
|
||||||
"./src/Explorer/Panes/PanelContainerComponent.test.tsx",
|
"./src/Explorer/Panes/PanelContainerComponent.test.tsx",
|
||||||
@@ -62,6 +61,7 @@
|
|||||||
"./src/Explorer/Tables/QueryBuilder/DateTimeUtilities.test.ts",
|
"./src/Explorer/Tables/QueryBuilder/DateTimeUtilities.test.ts",
|
||||||
"./src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts",
|
"./src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts",
|
||||||
"./src/Explorer/Tree/AccessibleVerticalList.ts",
|
"./src/Explorer/Tree/AccessibleVerticalList.ts",
|
||||||
|
"./src/Explorer/useSynapseLink.ts",
|
||||||
"./src/HostedExplorerChildFrame.ts",
|
"./src/HostedExplorerChildFrame.ts",
|
||||||
"./src/Platform/Hosted/Components/MeControl.test.tsx",
|
"./src/Platform/Hosted/Components/MeControl.test.tsx",
|
||||||
"./src/Platform/Hosted/Components/MeControl.tsx",
|
"./src/Platform/Hosted/Components/MeControl.tsx",
|
||||||
@@ -106,7 +106,6 @@
|
|||||||
"./src/hooks/useConfig.ts",
|
"./src/hooks/useConfig.ts",
|
||||||
"./src/hooks/useDirectories.tsx",
|
"./src/hooks/useDirectories.tsx",
|
||||||
"./src/hooks/useGraphPhoto.tsx",
|
"./src/hooks/useGraphPhoto.tsx",
|
||||||
"./src/hooks/useNotebookSnapshotStore.ts",
|
|
||||||
"./src/hooks/usePortalAccessToken.tsx",
|
"./src/hooks/usePortalAccessToken.tsx",
|
||||||
"./src/hooks/useNotificationConsole.ts",
|
"./src/hooks/useNotificationConsole.ts",
|
||||||
"./src/hooks/useObservable.ts",
|
"./src/hooks/useObservable.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user