From a3d88af1758228523eb383a21b8b949a2917557f Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Fri, 5 Nov 2021 10:23:21 -0700 Subject: [PATCH 01/33] Fix throughputcap check (#1156) --- src/Contracts/DataModels.ts | 1 + .../Controls/Settings/SettingsComponent.tsx | 47 ++++++++++++------- .../ThroughputInput/ThroughputInput.tsx | 6 ++- src/Explorer/Explorer.tsx | 4 +- .../CommandBarComponentButtonFactory.tsx | 8 +++- src/Explorer/SplashScreen/SplashScreen.tsx | 8 +++- src/Explorer/Tree/Collection.ts | 4 +- src/Explorer/Tree/Database.tsx | 6 ++- src/Explorer/useDatabases.ts | 14 ++++++ 9 files changed, 72 insertions(+), 26 deletions(-) diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 99db16f11..0f5db93ff 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -27,6 +27,7 @@ export interface DatabaseAccountExtendedProperties { ipRules?: IpRule[]; privateEndpointConnections?: unknown[]; capacity?: { totalThroughputLimit: number }; + locations?: DatabaseAccountResponseLocation[]; } export interface DatabaseAccountResponseLocation { diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 880a50b99..132622e9f 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -213,20 +213,9 @@ export class SettingsComponent extends React.Component { - if (database.offer()) { - const dbThroughput = database.offer().autoscaleMaxThroughput || database.offer().manualThroughput; - this.totalThroughputUsed += dbThroughput; - } - - (database.collections() || []).forEach((collection) => { - if (collection.offer()) { - const colThroughput = collection.offer().autoscaleMaxThroughput || collection.offer().manualThroughput; - this.totalThroughputUsed += colThroughput; - } - }); - }); + if (userContext.databaseAccount?.properties.capacity?.totalThroughputLimit) { + this.calculateTotalThroughputUsed(); + } } componentDidMount(): void { @@ -504,6 +493,26 @@ export class SettingsComponent extends React.Component this.setState({ isMongoIndexingPolicyDiscardable }); + private calculateTotalThroughputUsed = (): void => { + this.totalThroughputUsed = 0; + (useDatabases.getState().databases || []).forEach(async (database) => { + if (database.offer()) { + const dbThroughput = database.offer().autoscaleMaxThroughput || database.offer().manualThroughput; + this.totalThroughputUsed += dbThroughput; + } + + (database.collections() || []).forEach(async (collection) => { + if (collection.offer()) { + const colThroughput = collection.offer().autoscaleMaxThroughput || collection.offer().manualThroughput; + this.totalThroughputUsed += colThroughput; + } + }); + }); + + const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1; + this.totalThroughputUsed *= numberOfRegions; + }; + public getAnalyticalStorageTtl = (): number => { if (this.isAnalyticalStorageEnabled) { if (this.state.analyticalStorageTtlSelection === TtlType.On) { @@ -669,9 +678,11 @@ export class SettingsComponent extends React.Component { let throughputError = ""; const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; - if (throughputCap && throughputCap - this.totalThroughputUsed < newThroughput - this.offer.autoscaleMaxThroughput) { + const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1; + const throughputDelta = (newThroughput - this.offer.autoscaleMaxThroughput) * numberOfRegions; + if (throughputCap && throughputCap - this.totalThroughputUsed < throughputDelta) { throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${ - this.totalThroughputUsed + newThroughput + this.totalThroughputUsed + throughputDelta } RU/s. Change total throughput limit in cost management.`; } this.setState({ autoPilotThroughput: newThroughput, throughputError }); @@ -680,9 +691,11 @@ export class SettingsComponent extends React.Component { let throughputError = ""; const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; + const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1; + const throughputDelta = (newThroughput - this.offer.manualThroughput) * numberOfRegions; if (throughputCap && throughputCap - this.totalThroughputUsed < newThroughput - this.offer.manualThroughput) { throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${ - this.totalThroughputUsed + newThroughput + this.totalThroughputUsed + throughputDelta } RU/s. Change total throughput limit in cost management.`; } this.setState({ throughput: newThroughput, throughputError }); diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx index 1b5972d36..2a6c637ac 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx @@ -40,6 +40,7 @@ export const ThroughputInput: FunctionComponent = ({ setThroughputValue(throughput); const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; + const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1; useEffect(() => { // throughput cap check for the initial state @@ -57,12 +58,13 @@ export const ThroughputInput: FunctionComponent = ({ } }); }); + totalThroughput *= numberOfRegions; setTotalThroughputUsed(totalThroughput); if (throughputCap && throughputCap - totalThroughput < throughput) { setThroughputError( `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${ - totalThroughputUsed + throughput + totalThroughput + throughput * numberOfRegions } RU/s. Change total throughput limit in cost management.` ); @@ -74,7 +76,7 @@ export const ThroughputInput: FunctionComponent = ({ if (throughputCap && throughputCap - totalThroughputUsed < newThroughput) { setThroughputError( `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${ - totalThroughputUsed + newThroughput + totalThroughputUsed + newThroughput * numberOfRegions } RU/s. Change total throughput limit in cost management.` ); setIsThroughputCapExceeded(true); diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 6a3a49bfb..58ef9ce66 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -1176,7 +1176,9 @@ export default class Explorer { ); } else { - await useDatabases.getState().loadDatabaseOffers(); + userContext.databaseAccount?.properties.capacity?.totalThroughputLimit + ? await useDatabases.getState().loadAllOffers() + : await useDatabases.getState().loadDatabaseOffers(); useSidePanel .getState() .openSidePanel("New " + getCollectionName(), ); diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index c659d7712..97bf851c1 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -308,8 +308,12 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps { return { iconSrc: AddDatabaseIcon, iconAlt: label, - onCommandClick: () => - useSidePanel.getState().openSidePanel("New " + getDatabaseName(), ), + onCommandClick: async () => { + if (userContext.databaseAccount?.properties.capacity?.totalThroughputLimit) { + await useDatabases.getState().loadAllOffers(); + } + useSidePanel.getState().openSidePanel("New " + getDatabaseName(), ); + }, commandButtonLabel: label, ariaLabel: label, hasPopup: true, diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index b52593707..10866cf6f 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -307,10 +307,14 @@ export class SplashScreen extends React.Component { iconSrc: AddDatabaseIcon, title: "New " + getDatabaseName(), description: undefined, - onClick: () => + onClick: async () => { + if (userContext.databaseAccount?.properties.capacity?.totalThroughputLimit) { + await useDatabases.getState().loadAllOffers(); + } useSidePanel .getState() - .openSidePanel("New " + getDatabaseName(), ), + .openSidePanel("New " + getDatabaseName(), ); + }, }); } diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index fd444c0ed..5f836f7fb 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -576,7 +576,9 @@ export default class Collection implements ViewModels.Collection { public onSettingsClick = async (): Promise => { useSelectedNode.getState().setSelectedNode(this); - await this.loadOffer(); + userContext.databaseAccount?.properties.capacity?.totalThroughputLimit + ? await useDatabases.getState().loadAllOffers() + : await this.loadOffer(); this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings); TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { description: "Settings node", diff --git a/src/Explorer/Tree/Database.tsx b/src/Explorer/Tree/Database.tsx index 5dc01343f..0ea1130fb 100644 --- a/src/Explorer/Tree/Database.tsx +++ b/src/Explorer/Tree/Database.tsx @@ -57,7 +57,7 @@ export default class Database implements ViewModels.Database { this.isOfferRead = false; } - public onSettingsClick = (): void => { + public onSettingsClick = async (): Promise => { useSelectedNode.getState().setSelectedNode(this); this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings); TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { @@ -66,6 +66,10 @@ export default class Database implements ViewModels.Database { dataExplorerArea: Constants.Areas.ResourceTree, }); + if (userContext.databaseAccount?.properties.capacity?.totalThroughputLimit) { + await useDatabases.getState().loadAllOffers(); + } + const pendingNotificationsPromise: Promise = this.getPendingThroughputSplitNotification(); const tabKind = ViewModels.CollectionTabKind.DatabaseSettingsV2; const matchingTabs = useTabs.getState().getTabs(tabKind, (tab) => tab.node?.id() === this.id()); diff --git a/src/Explorer/useDatabases.ts b/src/Explorer/useDatabases.ts index 1f9886a7d..7d1760af3 100644 --- a/src/Explorer/useDatabases.ts +++ b/src/Explorer/useDatabases.ts @@ -18,6 +18,7 @@ interface DatabasesState { findCollection: (databaseId: string, collectionId: string) => ViewModels.Collection; isLastCollection: () => boolean; loadDatabaseOffers: () => Promise; + loadAllOffers: () => Promise; isFirstResourceCreated: () => boolean; findSelectedDatabase: () => ViewModels.Database; validateDatabaseId: (id: string) => boolean; @@ -97,6 +98,19 @@ export const useDatabases: UseStore = create((set, get) => ({ }) ); }, + loadAllOffers: async () => { + await Promise.all( + get().databases?.map(async (database: ViewModels.Database) => { + await database.loadOffer(); + await database.loadCollections(); + await Promise.all( + (database.collections() || []).map(async (collection: ViewModels.Collection) => { + await collection.loadOffer(); + }) + ); + }) + ); + }, isFirstResourceCreated: () => { const databases = get().databases; From 2dfabf3c6974efd458cc2af9f0e9819229bb36c9 Mon Sep 17 00:00:00 2001 From: siddjoshi-ms <86025894+siddjoshi-ms@users.noreply.github.com> Date: Mon, 8 Nov 2021 10:34:22 -0800 Subject: [PATCH 02/33] Sqlx currency code fix (#1149) * using currency code from fetch prices api * formatting & linting fixes * Update SqlX.rp.ts --- src/SelfServe/SqlX/SqlX.rp.ts | 23 ++++++++++++++--------- src/SelfServe/SqlX/SqlX.tsx | 9 ++++++--- src/SelfServe/SqlX/SqlxTypes.ts | 6 ++++++ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/SelfServe/SqlX/SqlX.rp.ts b/src/SelfServe/SqlX/SqlX.rp.ts index 5d1b830d0..d224990bb 100644 --- a/src/SelfServe/SqlX/SqlX.rp.ts +++ b/src/SelfServe/SqlX/SqlX.rp.ts @@ -6,6 +6,7 @@ import { RefreshResult } from "../SelfServeTypes"; import SqlX from "./SqlX"; import { FetchPricesResponse, + PriceMapAndCurrencyCode, RegionsResponse, SqlxServiceResource, UpdateDedicatedGatewayRequestParameters, @@ -178,18 +179,18 @@ const getFetchPricesPathForRegion = (subscriptionId: string): string => { return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`; }; -export const getPriceMap = async (regions: Array): Promise>> => { +export const getPriceMapAndCurrencyCode = async (regions: Array): Promise => { const telemetryData = { feature: "Calculate approximate cost", - function: "getPriceMap", + function: "getPriceMapAndCurrencyCode", description: "fetch prices API call", selfServeClassName: SqlX.name, }; - const getPriceMapTimestamp = selfServeTraceStart(telemetryData); + const getPriceMapAndCurrencyCodeTimestamp = selfServeTraceStart(telemetryData); try { const priceMap = new Map>(); - + let currencyCode; for (const region of regions) { const regionPriceMap = new Map(); @@ -207,17 +208,21 @@ export const getPriceMap = async (regions: Array): Promise>; +let currencyCode: string; let regions: Array; const calculateCost = (skuName: string, instanceCount: number): Description => { @@ -237,7 +238,7 @@ const calculateCost = (skuName: string, instanceCount: number): Description => { selfServeTraceSuccess(telemetryData, calculateCostTimestamp); return { - textTKey: `${costPerHour} USD`, + textTKey: `${costPerHour} ${currencyCode}`, type: DescriptionType.Text, }; } catch (err) { @@ -346,7 +347,9 @@ export default class SqlX extends SelfServeBaseClass { }); regions = await getRegions(); - priceMap = await getPriceMap(regions); + const priceMapAndCurrencyCode = await getPriceMapAndCurrencyCode(regions); + priceMap = priceMapAndCurrencyCode.priceMap; + currencyCode = priceMapAndCurrencyCode.currencyCode; const response = await getCurrentProvisioningState(); if (response.status && response.status !== "Deleting") { diff --git a/src/SelfServe/SqlX/SqlxTypes.ts b/src/SelfServe/SqlX/SqlxTypes.ts index a150ccbb1..7ca2fe264 100644 --- a/src/SelfServe/SqlX/SqlxTypes.ts +++ b/src/SelfServe/SqlX/SqlxTypes.ts @@ -36,9 +36,15 @@ export type FetchPricesResponse = { Count: number; }; +export type PriceMapAndCurrencyCode = { + priceMap: Map>; + currencyCode: string; +}; + export type PriceItem = { retailPrice: number; skuName: string; + currencyCode: string; }; export type RegionsResponse = { From 3aa4bbe2668189be8a085b3c3c45c7658aee14cb Mon Sep 17 00:00:00 2001 From: Karthik chakravarthy <88904658+kcheekuri@users.noreply.github.com> Date: Tue, 9 Nov 2021 07:38:17 -0500 Subject: [PATCH 03/33] Users/kcheekuri/phoenix heart beat retry with delay (#1153) * Health check retry addition * format issue * Address comments * Test Check * Added await * code cleanup --- src/Common/Constants.ts | 2 + src/Contracts/DataModels.ts | 1 - .../SettingsComponent.test.tsx.snap | 16 ++- src/Explorer/Explorer.tsx | 2 +- .../CommandBarComponentButtonFactory.tsx | 4 +- .../CommandBar/ConnectionStatusComponent.tsx | 9 +- .../Notebook/NotebookContainerClient.ts | 98 ++++++++++++------- .../GitHubReposPanel.test.tsx.snap | 8 +- .../StringInputPane.test.tsx.snap | 8 +- src/Explorer/useSelectedNode.ts | 6 ++ src/Phoenix/PhoenixClient.ts | 59 ++++++----- 11 files changed, 145 insertions(+), 68 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 838cb5584..c627c65fd 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -369,6 +369,8 @@ export class Notebook { public static readonly memoryGuageToGB = 1048576; public static readonly lowMemoryThreshold = 0.8; public static readonly remainingTimeForAlert = 10; + public static readonly retryAttempts = 3; + public static readonly retryAttemptDelayMs = 5000; public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it."; public static readonly mongoShellTemporarilyDownMsg = "We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation."; diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 0f5db93ff..5f5d9cc01 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -438,7 +438,6 @@ export interface ContainerInfo { } export interface IProvisionData { - aadToken: string; subscriptionId: string; resourceGroup: string; dbAccountName: string; diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 6f01661ea..db013e13a 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -34,7 +34,13 @@ exports[`SettingsComponent renders 1`] = ` "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], - "phoenixClient": PhoenixClient {}, + "phoenixClient": PhoenixClient { + "retryOptions": Object { + "maxTimeout": 5000, + "minTimeout": 5000, + "retries": 3, + }, + }, "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], @@ -102,7 +108,13 @@ exports[`SettingsComponent renders 1`] = ` "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], - "phoenixClient": PhoenixClient {}, + "phoenixClient": PhoenixClient { + "retryOptions": Object { + "maxTimeout": 5000, + "minTimeout": 5000, + "retries": 3, + }, + }, "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 58ef9ce66..99c839a45 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -382,7 +382,6 @@ export default class Explorer { (notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined)) ) { const provisionData = { - aadToken: userContext.authorizationToken, subscriptionId: userContext.subscriptionId, resourceGroup: userContext.resourceGroup, dbAccountName: userContext.databaseAccount.name, @@ -401,6 +400,7 @@ export default class Explorer { useNotebook.getState().resetContainerConnection(connectionStatus); throw error; } + this.refreshCommandBarButtons(); this.refreshNotebookList(); this._isInitializingNotebooks = false; diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 97bf851c1..3a2495362 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -80,7 +80,9 @@ export function createStaticCommandBarButtons( } notebookButtons.push(createOpenTerminalButton(container)); - notebookButtons.push(createNotebookWorkspaceResetButton(container)); + if (selectedNodeState.isConnectedToContainer()) { + notebookButtons.push(createNotebookWorkspaceResetButton(container)); + } if ( (userContext.apiType === "Mongo" && useNotebook.getState().isShellEnabled && diff --git a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx index 631bd1b32..520608acd 100644 --- a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx +++ b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx @@ -69,7 +69,10 @@ export const ConnectionStatus: React.FC = ({ container }: Props): JSX.Ele }, [isActive, counter]); React.useEffect(() => { - if (connectionInfo && connectionInfo.status === ConnectionStatusType.Reconnect) { + 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]); @@ -102,6 +105,7 @@ export const ConnectionStatus: React.FC = ({ container }: Props): JSX.Ele } if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) { + stopTimer(); setIsActive(true); setStatusColor("status connecting is-animating"); setToolTipContent("Connecting to temporary workspace."); @@ -118,8 +122,7 @@ export const ConnectionStatus: React.FC = ({ container }: Props): JSX.Ele <> {}; private isResettingWorkspace: boolean; private phoenixClient: PhoenixClient; + private retryOptions: promiseRetry.Options; constructor(private onConnectionLost: () => void) { this.phoenixClient = new PhoenixClient(); + this.retryOptions = { + retries: Notebook.retryAttempts, + maxTimeout: Notebook.retryAttemptDelayMs, + minTimeout: Notebook.retryAttemptDelayMs, + }; const notebookServerInfo = useNotebook.getState().notebookServerInfo; if (notebookServerInfo?.notebookServerEndpoint) { this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs); @@ -47,10 +54,23 @@ export class NotebookContainerClient { * Heartbeat: each ping schedules another ping */ private scheduleHeartbeat(delayMs: number): void { - setTimeout(() => { - this.getMemoryUsage() - .then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo)) - .finally(() => this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs)); + setTimeout(async () => { + try { + const memoryUsageInfo = await this.getMemoryUsage(); + useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo); + const notebookServerInfo = useNotebook.getState().notebookServerInfo; + if (notebookServerInfo?.notebookServerEndpoint) { + this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs); + } + } catch (exception) { + if (NotebookUtil.isPhoenixEnabled()) { + const connectionStatus: ContainerConnectionInfo = { + status: ConnectionStatusType.Failed, + }; + useNotebook.getState().resetContainerConnection(connectionStatus); + useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); + } + } }, delayMs); } @@ -68,29 +88,10 @@ export class NotebookContainerClient { const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig(); try { - if (this.checkStatus()) { - 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, - }; - } - } - } - return undefined; + 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) { @@ -98,18 +99,44 @@ export class NotebookContainerClient { "Connection lost with Notebook server. Attempting to reconnect..." ); } - if (NotebookUtil.isPhoenixEnabled()) { - const connectionStatus: ContainerConnectionInfo = { - status: ConnectionStatusType.Failed, - }; - useNotebook.getState().resetContainerConnection(connectionStatus); - useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - } this.onConnectionLost(); return undefined; } } + private async _getMemoryAsync( + notebookServerEndpoint: string, + authToken: string + ): Promise { + if (this.checkStatus()) { + 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(); + } else { + return undefined; + } + } + private checkStatus(): boolean { if (NotebookUtil.isPhoenixEnabled()) { if (useNotebook.getState().containerStatus?.status === Constants.ContainerStatusType.Disconnected) { @@ -148,7 +175,6 @@ export class NotebookContainerClient { try { if (NotebookUtil.isPhoenixEnabled()) { const provisionData: IProvisionData = { - aadToken: userContext.authorizationToken, subscriptionId: userContext.subscriptionId, resourceGroup: userContext.resourceGroup, dbAccountName: userContext.databaseAccount.name, diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index e7ed75e05..a66023041 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -23,7 +23,13 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], - "phoenixClient": PhoenixClient {}, + "phoenixClient": PhoenixClient { + "retryOptions": Object { + "maxTimeout": 5000, + "minTimeout": 5000, + "retries": 3, + }, + }, "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index e83d2db32..e77d8c74d 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -13,7 +13,13 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], - "phoenixClient": PhoenixClient {}, + "phoenixClient": PhoenixClient { + "retryOptions": Object { + "maxTimeout": 5000, + "minTimeout": 5000, + "retries": 3, + }, + }, "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], diff --git a/src/Explorer/useSelectedNode.ts b/src/Explorer/useSelectedNode.ts index 15f953641..acbac86ea 100644 --- a/src/Explorer/useSelectedNode.ts +++ b/src/Explorer/useSelectedNode.ts @@ -1,3 +1,5 @@ +import { ConnectionStatusType } from "Common/Constants"; +import { useNotebook } from "Explorer/Notebook/useNotebook"; import create, { UseStore } from "zustand"; import * as ViewModels from "../Contracts/ViewModels"; import { useTabs } from "../hooks/useTabs"; @@ -12,6 +14,7 @@ export interface SelectedNodeState { collectionId?: string, subnodeKinds?: ViewModels.CollectionTabKind[] ) => boolean; + isConnectedToContainer: () => boolean; } export const useSelectedNode: UseStore = create((set, get) => ({ @@ -59,4 +62,7 @@ export const useSelectedNode: UseStore = create((set, get) => subnodeKinds.includes(selectedSubnodeKind) ); }, + isConnectedToContainer: (): boolean => { + return useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected; + }, })); diff --git a/src/Phoenix/PhoenixClient.ts b/src/Phoenix/PhoenixClient.ts index a52e021bc..b7fd39955 100644 --- a/src/Phoenix/PhoenixClient.ts +++ b/src/Phoenix/PhoenixClient.ts @@ -1,3 +1,4 @@ +import promiseRetry, { AbortError } from "p-retry"; import { ContainerStatusType, HttpHeaders, HttpStatusCodes, Notebook } from "../Common/Constants"; import { getErrorMessage } from "../Common/ErrorHandlingUtils"; import * as Logger from "../Common/Logger"; @@ -15,6 +16,11 @@ import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; export class PhoenixClient { private containerHealthHandler: NodeJS.Timeout; + private retryOptions: promiseRetry.Options = { + retries: Notebook.retryAttempts, + maxTimeout: Notebook.retryAttemptDelayMs, + minTimeout: Notebook.retryAttemptDelayMs, + }; public async allocateContainer(provisionData: IProvisionData): Promise> { return this.executeContainerAssignmentOperation(provisionData, "allocate"); @@ -64,26 +70,27 @@ export class PhoenixClient { private async getContainerStatusAsync(containerData: IContainerData): Promise { try { - const response = await window.fetch( - `${this.getPhoenixContainerPoolingEndPoint()}/${containerData.dbAccountName}/${containerData.forwardingId}`, - { - method: "GET", - headers: PhoenixClient.getHeaders(), + const runContainerStatusAsync = async () => { + const response = await window.fetch( + `${this.getPhoenixContainerPoolingEndPoint()}/${containerData.dbAccountName}/${containerData.forwardingId}`, + { + method: "GET", + headers: PhoenixClient.getHeaders(), + } + ); + if (response.status === HttpStatusCodes.OK) { + const containerStatus = await response.json(); + return { + durationLeftInMinutes: containerStatus?.durationLeftInMinutes, + notebookServerInfo: containerStatus?.notebookServerInfo, + status: ContainerStatusType.Active, + }; + } else if (response.status === HttpStatusCodes.NotFound) { + throw new AbortError(response.statusText); } - ); - if (response.status === HttpStatusCodes.OK) { - const containerStatus = await response.json(); - return { - durationLeftInMinutes: containerStatus?.durationLeftInMinutes, - notebookServerInfo: containerStatus?.notebookServerInfo, - status: ContainerStatusType.Active, - }; - } - return { - durationLeftInMinutes: undefined, - notebookServerInfo: undefined, - status: ContainerStatusType.Disconnected, + throw new Error(response.statusText); }; + return await promiseRetry(runContainerStatusAsync, this.retryOptions); } catch (error) { Logger.logError(getErrorMessage(error), "PhoenixClient/getContainerStatus"); return { @@ -95,10 +102,18 @@ export class PhoenixClient { } private async getContainerHealth(delayMs: number, containerData: { forwardingId: string; dbAccountName: string }) { - const containerInfo = await this.getContainerStatusAsync(containerData); - useNotebook.getState().setContainerStatus(containerInfo); - if (useNotebook.getState().containerStatus?.status === ContainerStatusType.Active) { - this.scheduleContainerHeartbeat(delayMs, containerData); + try { + const containerInfo = await this.getContainerStatusAsync(containerData); + useNotebook.getState().setContainerStatus(containerInfo); + if (useNotebook.getState().containerStatus?.status === ContainerStatusType.Active) { + this.scheduleContainerHeartbeat(delayMs, containerData); + } + } catch (exception) { + useNotebook.getState().setContainerStatus({ + durationLeftInMinutes: undefined, + notebookServerInfo: undefined, + status: ContainerStatusType.Disconnected, + }); } } From fa8be2bc0ff438ab1ebce0c9b0f3d781b29e5524 Mon Sep 17 00:00:00 2001 From: Srinath Narayanan Date: Wed, 10 Nov 2021 03:35:17 -0800 Subject: [PATCH 04/33] fixed quickstarts (#1157) --- src/Explorer/Explorer.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 99c839a45..f100d7b7a 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -1204,10 +1204,9 @@ export default class Explorer { } public async handleOpenFileAction(path: string): Promise { - if ( - userContext.features.phoenix === false && - !(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) - ) { + if (userContext.features.phoenix) { + await this.allocateContainer(); + } else if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) { this._openSetupNotebooksPaneForQuickstart(); } From 27a49e9aa9ccc659d6825c7792132df90faa6666 Mon Sep 17 00:00:00 2001 From: tarazou9 <40870773+tarazou9@users.noreply.github.com> Date: Wed, 10 Nov 2021 06:35:31 -0500 Subject: [PATCH 05/33] add juno test3 to allow list (#1158) * add juno test3 to allow list * remove extra line --- src/ConfigContext.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 289e650cb..224d28f8d 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -58,6 +58,7 @@ let configContext: Readonly = { allowedJunoOrigins: [ "https://juno-test.documents-dev.windows-int.net", "https://juno-test2.documents-dev.windows-int.net", + "https://juno-test3.documents-dev.windows-int.net", "https://tools.cosmos.azure.com", "https://tools-staging.cosmos.azure.com", "https://localhost", From 1155557af1c243eb7560087f5df25d4e4dff108f Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Wed, 10 Nov 2021 21:43:04 -0800 Subject: [PATCH 06/33] Check for -1 throughput cap value (#1159) --- src/Explorer/Controls/Settings/SettingsComponent.tsx | 7 ++++--- src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx | 4 ++-- src/Explorer/Explorer.tsx | 3 ++- .../Menus/CommandBar/CommandBarComponentButtonFactory.tsx | 3 ++- src/Explorer/SplashScreen/SplashScreen.tsx | 3 ++- src/Explorer/Tree/Collection.ts | 5 ++--- src/Explorer/Tree/Database.tsx | 3 ++- 7 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 132622e9f..7f4f8c64b 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -213,7 +213,8 @@ export class SettingsComponent extends React.Component = ({ totalThroughput *= numberOfRegions; setTotalThroughputUsed(totalThroughput); - if (throughputCap && throughputCap - totalThroughput < throughput) { + if (throughputCap && throughputCap !== -1 && throughputCap - totalThroughput < throughput) { setThroughputError( `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${ totalThroughput + throughput * numberOfRegions @@ -73,7 +73,7 @@ export const ThroughputInput: FunctionComponent = ({ }, []); const checkThroughputCap = (newThroughput: number): boolean => { - if (throughputCap && throughputCap - totalThroughputUsed < newThroughput) { + if (throughputCap && throughputCap !== -1 && throughputCap - totalThroughputUsed < newThroughput) { setThroughputError( `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${ totalThroughputUsed + newThroughput * numberOfRegions diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index f100d7b7a..9ea267ef9 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -1176,7 +1176,8 @@ export default class Explorer { ); } else { - userContext.databaseAccount?.properties.capacity?.totalThroughputLimit + const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; + throughputCap && throughputCap !== -1 ? await useDatabases.getState().loadAllOffers() : await useDatabases.getState().loadDatabaseOffers(); useSidePanel diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 3a2495362..4507d51a8 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -311,7 +311,8 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps { iconSrc: AddDatabaseIcon, iconAlt: label, onCommandClick: async () => { - if (userContext.databaseAccount?.properties.capacity?.totalThroughputLimit) { + const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; + if (throughputCap && throughputCap !== -1) { await useDatabases.getState().loadAllOffers(); } useSidePanel.getState().openSidePanel("New " + getDatabaseName(), ); diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index 10866cf6f..d6afda063 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -308,7 +308,8 @@ export class SplashScreen extends React.Component { title: "New " + getDatabaseName(), description: undefined, onClick: async () => { - if (userContext.databaseAccount?.properties.capacity?.totalThroughputLimit) { + const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; + if (throughputCap && throughputCap !== -1) { await useDatabases.getState().loadAllOffers(); } useSidePanel diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 5f836f7fb..58c06f521 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -576,9 +576,8 @@ export default class Collection implements ViewModels.Collection { public onSettingsClick = async (): Promise => { useSelectedNode.getState().setSelectedNode(this); - userContext.databaseAccount?.properties.capacity?.totalThroughputLimit - ? await useDatabases.getState().loadAllOffers() - : await this.loadOffer(); + const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; + throughputCap && throughputCap !== -1 ? await useDatabases.getState().loadAllOffers() : await this.loadOffer(); this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings); TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { description: "Settings node", diff --git a/src/Explorer/Tree/Database.tsx b/src/Explorer/Tree/Database.tsx index 0ea1130fb..156b5f275 100644 --- a/src/Explorer/Tree/Database.tsx +++ b/src/Explorer/Tree/Database.tsx @@ -66,7 +66,8 @@ export default class Database implements ViewModels.Database { dataExplorerArea: Constants.Areas.ResourceTree, }); - if (userContext.databaseAccount?.properties.capacity?.totalThroughputLimit) { + const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; + if (throughputCap && throughputCap !== -1) { await useDatabases.getState().loadAllOffers(); } From 94a03e5b03f9e3c6de22904bb6dd7c3a7e96d9ae Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Fri, 19 Nov 2021 09:55:10 -0800 Subject: [PATCH 07/33] Add Timestamp type to cassandra column types and wrap Timestamp value inside single quotes when creating queries (#1163) --- src/Explorer/Panes/Tables/Validators/EntityTableHelper.tsx | 2 ++ src/Explorer/Tables/Constants.ts | 1 + src/Explorer/Tables/TableDataClient.ts | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Explorer/Panes/Tables/Validators/EntityTableHelper.tsx b/src/Explorer/Panes/Tables/Validators/EntityTableHelper.tsx index b38f569e0..71cfa9c8b 100644 --- a/src/Explorer/Panes/Tables/Validators/EntityTableHelper.tsx +++ b/src/Explorer/Panes/Tables/Validators/EntityTableHelper.tsx @@ -33,6 +33,7 @@ const { Inet, Smallint, Tinyint, + Timestamp, } = TableConstants.CassandraType; export const cassandraOptions = [ { key: Text, text: Text }, @@ -50,6 +51,7 @@ export const cassandraOptions = [ { key: Inet, text: Inet }, { key: Smallint, text: Smallint }, { key: Tinyint, text: Tinyint }, + { key: Timestamp, text: Timestamp }, ]; export const imageProps: IImageProps = { diff --git a/src/Explorer/Tables/Constants.ts b/src/Explorer/Tables/Constants.ts index 9bb313b7a..c9917dcd8 100644 --- a/src/Explorer/Tables/Constants.ts +++ b/src/Explorer/Tables/Constants.ts @@ -19,6 +19,7 @@ export const CassandraType = { Float: "Float", Int: "Int", Text: "Text", + Timestamp: "Timestamp", Uuid: "Uuid", Varchar: "Varchar", Varint: "Varint", diff --git a/src/Explorer/Tables/TableDataClient.ts b/src/Explorer/Tables/TableDataClient.ts index afe31b711..62463abfd 100644 --- a/src/Explorer/Tables/TableDataClient.ts +++ b/src/Explorer/Tables/TableDataClient.ts @@ -535,7 +535,8 @@ export class CassandraAPIDataClient extends TableDataClient { dataType === TableConstants.CassandraType.Text || dataType === TableConstants.CassandraType.Inet || dataType === TableConstants.CassandraType.Ascii || - dataType === TableConstants.CassandraType.Varchar + dataType === TableConstants.CassandraType.Varchar || + dataType === TableConstants.CassandraType.Timestamp ); } From 42bdcaf8d1c1266154c15401e3225d36f6a87dd0 Mon Sep 17 00:00:00 2001 From: vaidankarswapnil <81285216+vaidankarswapnil@users.noreply.github.com> Date: Thu, 25 Nov 2021 09:30:06 +0530 Subject: [PATCH 08/33] =?UTF-8?q?Fix=20radio=20buttons=20present=20under?= =?UTF-8?q?=20'Settings'=20blade=20like=20=E2=80=98Custom=20and=20Unlimite?= =?UTF-8?q?d=E2=80=99=20along=20with=20its=20label=20=E2=80=98Page=20optio?= =?UTF-8?q?ns=E2=80=99=20are=20not=20enclosed=20in=20fieldset/legend=20tag?= =?UTF-8?q?=20(#1100)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix a11y setting pane radiobuttons issue * Update test snapshot issue --- less/documentDB.less | 40 +++++++++---------- .../Panes/SettingsPane/SettingsPane.tsx | 36 ++++++++++++++--- .../__snapshots__/SettingsPane.test.tsx.snap | 34 +++++++++++++--- 3 files changed, 79 insertions(+), 31 deletions(-) diff --git a/less/documentDB.less b/less/documentDB.less index 79e9435c3..72bbce880 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -2869,31 +2869,31 @@ a:link { } } -settings-pane { - .settingsSection { - border-bottom: 1px solid @BaseMedium; - margin-right: 24px; - padding: @MediumSpace 0px; +.settingsSection { + border-bottom: 1px solid @BaseMedium; + margin-right: 24px; + padding: @MediumSpace 0px; - &:first-child { - padding-top: 0px; - } + &:first-child { + padding-top: 0px; + padding-bottom: 10px; + } - &:last-child { - border-bottom: none; - } + &:last-child { + border-bottom: none; + } - .settingsSectionPart { - padding-left: 8px; - } + .settingsSectionPart { + padding-left: 8px; + } - .settingsSectionLabel { - margin-bottom: @DefaultSpace; - } + .settingsSectionLabel { + margin-bottom: @DefaultSpace; + margin-right: 5px; + } - .pageOptionsPart { - padding-bottom: @MediumSpace; - } + .pageOptionsPart { + padding-bottom: @MediumSpace; } } diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index 92d432f34..2a06ce497 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -1,4 +1,4 @@ -import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui/react"; +import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton, Stack, Text } from "@fluentui/react"; import * as Constants from "Common/Constants"; import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; import { configContext } from "ConfigContext"; @@ -113,20 +113,44 @@ export const SettingsPane: FunctionComponent = () => { const handleOnPageOptionChange = (ev: React.FormEvent, option: IChoiceGroupOption): void => { setPageOption(option.key); }; + + const choiceButtonStyles = { + flexContainer: [ + { + selectors: { + ".ms-ChoiceField-wrapper label": { + fontSize: 12, + paddingTop: 0, + }, + ".ms-ChoiceField": { + marginTop: 0, + }, + }, + }, + ], + }; return (
{shouldShowQueryPageOptions && (
-
-
- Page options +
+ + + Page options + Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page. -
- + +
{isCustomPageOptionSelected() && ( diff --git a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap index ccc2ad3a7..4b1d6f079 100644 --- a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap +++ b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap @@ -14,17 +14,24 @@ exports[`Settings Pane should render Default properly 1`] = ` className="settingsSection" >
-
- Page options + + Page options + Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page. -
+
Date: Mon, 29 Nov 2021 11:22:57 -0500 Subject: [PATCH 09/33] Add phoenix telemetry (#1164) * Add phoenix telemetry * Revert changes * Update trace logs --- src/Explorer/Explorer.tsx | 103 ++++++++++++--------- src/Shared/Telemetry/TelemetryConstants.ts | 3 +- 2 files changed, 59 insertions(+), 47 deletions(-) diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 9ea267ef9..a81e2fe19 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -7,7 +7,7 @@ import shallow from "zustand/shallow"; import { AuthType } from "../AuthType"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import * as Constants from "../Common/Constants"; -import { ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants"; +import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants"; import { readCollection } from "../Common/dataAccess/readCollection"; import { readDatabases } from "../Common/dataAccess/readDatabases"; import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils"; @@ -392,18 +392,36 @@ export default class Explorer { }; useNotebook.getState().setConnectionInfo(connectionStatus); try { + TelemetryProcessor.traceStart(Action.PhoenixConnection, { + dataExplorerArea: Areas.Notebook, + }); useNotebook.getState().setIsAllocating(true); const connectionInfo = await this.phoenixClient.allocateContainer(provisionData); + if (connectionInfo.status !== HttpStatusCodes.OK) { + throw new Error(`Received status code: ${connectionInfo?.status}`); + } + if (!connectionInfo?.data?.notebookServerUrl) { + throw new Error(`NotebookServerUrl is invalid!`); + } await this.setNotebookInfo(connectionInfo, connectionStatus); + TelemetryProcessor.traceSuccess(Action.PhoenixConnection, { + dataExplorerArea: Areas.Notebook, + }); } catch (error) { + TelemetryProcessor.traceFailure(Action.PhoenixConnection, { + dataExplorerArea: Areas.Notebook, + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }); connectionStatus.status = ConnectionStatusType.Failed; useNotebook.getState().resetContainerConnection(connectionStatus); throw error; + } finally { + useNotebook.getState().setIsAllocating(false); + this.refreshCommandBarButtons(); + this.refreshNotebookList(); + this._isInitializingNotebooks = false; } - this.refreshCommandBarButtons(); - this.refreshNotebookList(); - - this._isInitializingNotebooks = false; } } @@ -411,28 +429,22 @@ export default class Explorer { connectionInfo: IResponse, connectionStatus: DataModels.ContainerConnectionInfo ) { - if (connectionInfo.status === HttpStatusCodes.OK && connectionInfo.data && connectionInfo.data.notebookServerUrl) { - const containerData = { - forwardingId: connectionInfo.data.forwardingId, - dbAccountName: userContext.databaseAccount.name, - }; - await this.phoenixClient.initiateContainerHeartBeat(containerData); + const containerData = { + forwardingId: connectionInfo.data.forwardingId, + dbAccountName: userContext.databaseAccount.name, + }; + await this.phoenixClient.initiateContainerHeartBeat(containerData); - connectionStatus.status = ConnectionStatusType.Connected; - useNotebook.getState().setConnectionInfo(connectionStatus); - useNotebook.getState().setNotebookServerInfo({ - notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl, - authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken, - forwardingId: connectionInfo.data.forwardingId, - }); - this.notebookManager?.notebookClient - .getMemoryUsage() - .then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo)); - } else { - connectionStatus.status = ConnectionStatusType.Failed; - useNotebook.getState().resetContainerConnection(connectionStatus); - } - useNotebook.getState().setIsAllocating(false); + connectionStatus.status = ConnectionStatusType.Connected; + useNotebook.getState().setConnectionInfo(connectionStatus); + useNotebook.getState().setNotebookServerInfo({ + notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl, + authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken, + forwardingId: connectionInfo.data.forwardingId, + }); + this.notebookManager?.notebookClient + .getMemoryUsage() + .then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo)); } public resetNotebookWorkspace(): void { @@ -517,7 +529,9 @@ export default class Explorer { logConsoleError(error); return; } - + TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, { + dataExplorerArea: Areas.Notebook, + }); if (NotebookUtil.isPhoenixEnabled()) { useTabs.getState().closeAllNotebookTabs(true); connectionStatus = { @@ -526,27 +540,24 @@ export default class Explorer { useNotebook.getState().setConnectionInfo(connectionStatus); } const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace(); - if (connectionInfo && connectionInfo.status && connectionInfo.status === HttpStatusCodes.OK) { - if (NotebookUtil.isPhoenixEnabled() && connectionInfo.data && connectionInfo.data.notebookServerUrl) { - await this.setNotebookInfo(connectionInfo, connectionStatus); - useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - } - logConsoleInfo("Successfully reset notebook workspace"); - TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace); - } else { - logConsoleError(`Failed to reset notebook workspace`); - TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace); - if (NotebookUtil.isPhoenixEnabled()) { - connectionStatus = { - status: ConnectionStatusType.Reconnect, - }; - useNotebook.getState().resetContainerConnection(connectionStatus); - useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - } + if (connectionInfo?.status !== HttpStatusCodes.OK) { + throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`); } + if (!connectionInfo?.data?.notebookServerUrl) { + throw new Error(`Reset Workspace: NotebookServerUrl is invalid!`); + } + if (NotebookUtil.isPhoenixEnabled()) { + await this.setNotebookInfo(connectionInfo, connectionStatus); + useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); + } + logConsoleInfo("Successfully reset notebook workspace"); + TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, { + dataExplorerArea: Areas.Notebook, + }); } catch (error) { logConsoleError(`Failed to reset notebook workspace: ${error}`); - TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, { + TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, { + dataExplorerArea: Areas.Notebook, error: getErrorMessage(error), errorStack: getErrorStack(error), }); @@ -554,7 +565,7 @@ export default class Explorer { connectionStatus = { status: ConnectionStatusType.Failed, }; - useNotebook.getState().setConnectionInfo(connectionStatus); + useNotebook.getState().resetContainerConnection(connectionStatus); useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); } throw error; diff --git a/src/Shared/Telemetry/TelemetryConstants.ts b/src/Shared/Telemetry/TelemetryConstants.ts index 134340ad4..e9541b9f7 100644 --- a/src/Shared/Telemetry/TelemetryConstants.ts +++ b/src/Shared/Telemetry/TelemetryConstants.ts @@ -50,7 +50,6 @@ export enum Action { SubscriptionSwitch, TenantSwitch, DefaultTenantSwitch, - ResetNotebookWorkspace, CreateNotebookWorkspace, NotebookErrorNotification, CreateSparkCluster, @@ -82,6 +81,8 @@ export enum Action { NotebooksInsertTextCellBelowFromMenu, NotebooksMoveCellUpFromMenu, NotebooksMoveCellDownFromMenu, + PhoenixConnection, + PhoenixResetWorkspace, DeleteCellFromMenu, OpenTerminal, CreateMongoCollectionWithWildcardIndex, From 6b4d6f986e261babf8a861c54f69d12a94455258 Mon Sep 17 00:00:00 2001 From: Srinath Narayanan Date: Wed, 1 Dec 2021 03:38:38 +0530 Subject: [PATCH 10/33] added github test env client id (#1168) --- src/Common/Constants.ts | 8 ++++++++ src/ConfigContext.ts | 16 ++++++++++------ src/GitHub/GitHubOAuthService.ts | 4 ++-- src/Juno/JunoClient.ts | 3 ++- src/Utils/GitHubUtils.ts | 17 +++++++++++++++++ 5 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index c627c65fd..36dfa83a1 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -412,3 +412,11 @@ export class TerminalQueryParams { public static readonly SubscriptionId = "subscriptionId"; public static readonly TerminalEndpoint = "terminalEndpoint"; } + +export class JunoEndpoints { + public static readonly Test = "https://juno-test.documents-dev.windows-int.net"; + public static readonly Test2 = "https://juno-test2.documents-dev.windows-int.net"; + public static readonly Test3 = "https://juno-test3.documents-dev.windows-int.net"; + public static readonly Prod = "https://tools.cosmos.azure.com"; + public static readonly Stage = "https://tools-staging.cosmos.azure.com"; +} diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 224d28f8d..f89ed5d61 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -1,3 +1,5 @@ +import { JunoEndpoints } from "Common/Constants"; + export enum Platform { Portal = "Portal", Hosted = "Hosted", @@ -23,6 +25,7 @@ export interface ConfigContext { PROXY_PATH?: string; JUNO_ENDPOINT: string; GITHUB_CLIENT_ID: string; + GITHUB_TEST_ENV_CLIENT_ID: string; GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it. hostedExplorerURL: string; armAPIVersion?: string; @@ -52,15 +55,16 @@ let configContext: Readonly = { GRAPH_API_VERSION: "1.6", ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net", ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net", - GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306 + GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306 + GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772 JUNO_ENDPOINT: "https://tools.cosmos.azure.com", BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", allowedJunoOrigins: [ - "https://juno-test.documents-dev.windows-int.net", - "https://juno-test2.documents-dev.windows-int.net", - "https://juno-test3.documents-dev.windows-int.net", - "https://tools.cosmos.azure.com", - "https://tools-staging.cosmos.azure.com", + JunoEndpoints.Test, + JunoEndpoints.Test2, + JunoEndpoints.Test3, + JunoEndpoints.Prod, + JunoEndpoints.Stage, "https://localhost", ], }; diff --git a/src/GitHub/GitHubOAuthService.ts b/src/GitHub/GitHubOAuthService.ts index fb9288c7c..7a41076d7 100644 --- a/src/GitHub/GitHubOAuthService.ts +++ b/src/GitHub/GitHubOAuthService.ts @@ -1,8 +1,8 @@ import ko from "knockout"; import postRobot from "post-robot"; +import { GetGithubClientId } from "Utils/GitHubUtils"; import { HttpStatusCodes } from "../Common/Constants"; import { handleError } from "../Common/ErrorHandlingUtils"; -import { configContext } from "../ConfigContext"; import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent"; import { JunoClient } from "../Juno/JunoClient"; import { logConsoleInfo } from "../Utils/NotificationConsoleUtils"; @@ -55,7 +55,7 @@ export class GitHubOAuthService { const params = { scope, - client_id: configContext.GITHUB_CLIENT_ID, + client_id: GetGithubClientId(), redirect_uri: new URL("./connectToGitHub.html", window.location.href).href, state: this.resetState(), }; diff --git a/src/Juno/JunoClient.ts b/src/Juno/JunoClient.ts index 1a43cccc6..540c339e8 100644 --- a/src/Juno/JunoClient.ts +++ b/src/Juno/JunoClient.ts @@ -1,4 +1,5 @@ import ko from "knockout"; +import { GetGithubClientId } from "Utils/GitHubUtils"; import { HttpHeaders, HttpStatusCodes } from "../Common/Constants"; import { configContext } from "../ConfigContext"; import * as DataModels from "../Contracts/DataModels"; @@ -522,7 +523,7 @@ export class JunoClient { private static getGitHubClientParams(): URLSearchParams { const githubParams = new URLSearchParams({ - client_id: configContext.GITHUB_CLIENT_ID, + client_id: GetGithubClientId(), }); if (configContext.GITHUB_CLIENT_SECRET) { diff --git a/src/Utils/GitHubUtils.ts b/src/Utils/GitHubUtils.ts index 13e5f828b..59b14f9a8 100644 --- a/src/Utils/GitHubUtils.ts +++ b/src/Utils/GitHubUtils.ts @@ -1,4 +1,9 @@ // https://github.com///tree/ + +import { JunoEndpoints } from "Common/Constants"; +import { configContext } from "ConfigContext"; +import { userContext } from "UserContext"; + // The url when users visit a repo/branch on github.com export const RepoUriPattern = /https:\/\/github.com\/([^/]*)\/([^/]*)\/tree\/([^?]*)/; @@ -60,3 +65,15 @@ export function toContentUri(owner: string, repo: string, branch: string, path: export function toRawContentUri(owner: string, repo: string, branch: string, path: string): string { return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`; } + +export function GetGithubClientId(): string { + const junoEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT; + if ( + junoEndpoint === JunoEndpoints.Test || + junoEndpoint === JunoEndpoints.Test2 || + junoEndpoint === JunoEndpoints.Test3 + ) { + return configContext.GITHUB_TEST_ENV_CLIENT_ID; + } + return configContext.GITHUB_CLIENT_ID; +} From 5d235038ad729ecac1096f2b21593fb506b1a542 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Tue, 30 Nov 2021 15:36:35 -0800 Subject: [PATCH 11/33] Properly update table headers (#1166) --- src/Explorer/Tables/DataTable/TableEntityListViewModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts b/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts index f11730b2b..1f1a90d9d 100644 --- a/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts +++ b/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts @@ -431,7 +431,7 @@ export default class TableEntityListViewModel extends DataTableViewModel { if (newHeaders.length > 0) { // Any new columns found will be added into headers array, which will trigger a re-render of the DataTable. // So there is no need to call it here. - this.updateHeaders(newHeaders, /* notifyColumnChanges */ true); + this.updateHeaders(selectedHeadersUnion, /* notifyColumnChanges */ true); } else { if (columnSortOrder) { this.sortColumns(columnSortOrder, oSettings); From 203c2ac246e4f155f63ad2c8dc7945fab6f1e909 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Yadav <79906609+sunilyadav840@users.noreply.github.com> Date: Thu, 2 Dec 2021 09:16:48 +0530 Subject: [PATCH 12/33] fixed horizontal scroll issue on zoom 400% (#1165) Co-authored-by: sunilyadav --- less/documentDB.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/less/documentDB.less b/less/documentDB.less index 72bbce880..50b5eafd8 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -2077,7 +2077,7 @@ a:link { .resourceTreeAndTabs { display: flex; flex: 1 1 auto; - overflow-x: auto; + overflow-x: clip; overflow-y: auto; height: 100%; } From 667b1e1486011cc39feb6540ec21727f0e75fe37 Mon Sep 17 00:00:00 2001 From: Hardikkumar Nai <80053762+hardiknai-techm@users.noreply.github.com> Date: Fri, 3 Dec 2021 09:42:57 +0530 Subject: [PATCH 13/33] 1413651_Refresh_button_missing (#1169) --- less/documentDB.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/less/documentDB.less b/less/documentDB.less index 50b5eafd8..7ccaf4255 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -2245,7 +2245,7 @@ a:link { } .refreshColHeader { - padding: 3px 6px 6px 6px; + padding: 3px 6px 10px 0px !important; } .refreshColHeader:hover { From 8a8c023d7be983e862f2b20e0e307a38bf45add0 Mon Sep 17 00:00:00 2001 From: vaidankarswapnil <81285216+vaidankarswapnil@users.noreply.github.com> Date: Fri, 3 Dec 2021 09:43:45 +0530 Subject: [PATCH 14/33] Fix Keyboard focus New Database button (#1167) * Fix a11y new database button focus issue * Update test snapshot and other issues * fix issue for the menu button * Issue fixed in Splash screen --- src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx | 5 +++++ src/Explorer/SplashScreen/SplashScreen.tsx | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx index 0c3568ecf..4b0535175 100644 --- a/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx +++ b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx @@ -23,10 +23,12 @@ import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneFor export interface AddDatabasePaneProps { explorer: Explorer; + buttonElement?: HTMLElement; } export const AddDatabasePanel: FunctionComponent = ({ explorer: container, + buttonElement, }: AddDatabasePaneProps) => { const closeSidePanel = useSidePanel((state) => state.closeSidePanel); let throughput: number; @@ -78,6 +80,9 @@ export const AddDatabasePanel: FunctionComponent = ({ dataExplorerArea: Constants.Areas.ContextualPane, }; TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage); + if (buttonElement) { + buttonElement.focus(); + } }, []); const onSubmit = () => { diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index d6afda063..031214867 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -314,7 +314,10 @@ export class SplashScreen extends React.Component { } useSidePanel .getState() - .openSidePanel("New " + getDatabaseName(), ); + .openSidePanel( + "New " + getDatabaseName(), + + ); }, }); } From ada95eae1f57852120627454ec5fe64e27017f4b Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Wed, 8 Dec 2021 15:41:27 -0800 Subject: [PATCH 15/33] Fix execute sproc pane textfield focus issue (#1170) * Fix execute sproc pane textfield focus issue * Update snapshot --- .../ExecuteSprocParamsPane.tsx | 136 +- .../ExecuteSprocParamsPane/InputParameter.tsx | 5 +- .../ExecuteSprocParamsPane.test.tsx.snap | 9968 +++++++++-------- 3 files changed, 5050 insertions(+), 5059 deletions(-) diff --git a/src/Explorer/Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane.tsx b/src/Explorer/Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane.tsx index 064aadd5a..81653a025 100644 --- a/src/Explorer/Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane.tsx +++ b/src/Explorer/Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane.tsx @@ -1,6 +1,6 @@ import { IDropdownOption, IImageProps, Image, Stack, Text } from "@fluentui/react"; import { useBoolean } from "@fluentui/react-hooks"; -import React, { FunctionComponent, useState } from "react"; +import React, { FunctionComponent, useRef, useState } from "react"; import AddPropertyIcon from "../../../../images/Add-property.svg"; import { useSidePanel } from "../../../hooks/useSidePanel"; import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; @@ -25,19 +25,16 @@ interface UnwrappedExecuteSprocParam { export const ExecuteSprocParamsPane: FunctionComponent = ({ storedProcedure, }: ExecuteSprocParamsPaneProps): JSX.Element => { + const paramKeyValuesRef = useRef([{ key: "string", text: "" }]); + const partitionValueRef = useRef(); + const partitionKeyRef = useRef("string"); const closeSidePanel = useSidePanel((state) => state.closeSidePanel); + const [numberOfParams, setNumberOfParams] = useState(1); const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false); - const [paramKeyValues, setParamKeyValues] = useState([{ key: "string", text: "" }]); - const [partitionValue, setPartitionValue] = useState(); // Defaulting to undefined here is important. It is not the same partition key as "" - const [selectedKey, setSelectedKey] = React.useState({ key: "string", text: "" }); const [formError, setFormError] = useState(""); - const onPartitionKeyChange = (event: React.FormEvent, item: IDropdownOption): void => { - setSelectedKey(item); - }; - const validateUnwrappedParams = (): boolean => { - const unwrappedParams: UnwrappedExecuteSprocParam[] = paramKeyValues; + const unwrappedParams: UnwrappedExecuteSprocParam[] = paramKeyValuesRef.current; for (let i = 0; i < unwrappedParams.length; i++) { const { key: paramType, text: paramValue } = unwrappedParams[i]; if (paramType === "custom" && (paramValue === "" || paramValue === undefined)) { @@ -53,8 +50,9 @@ export const ExecuteSprocParamsPane: FunctionComponent { - const wrappedSprocParams: UnwrappedExecuteSprocParam[] = paramKeyValues; - const { key: partitionKey } = selectedKey; + const wrappedSprocParams: UnwrappedExecuteSprocParam[] = paramKeyValuesRef.current; + const partitionValue: string = partitionValueRef.current; + const partitionKey: string = partitionKeyRef.current; if (partitionKey === "custom" && (partitionValue === "" || partitionValue === undefined)) { setInvalidParamError(partitionValue); return; @@ -78,37 +76,21 @@ export const ExecuteSprocParamsPane: FunctionComponent { - const cloneParamKeyValue = [...paramKeyValues]; - cloneParamKeyValue.splice(indexToRemove, 1); - setParamKeyValues(cloneParamKeyValue); + paramKeyValuesRef.current.splice(indexToRemove, 1); + setNumberOfParams(numberOfParams - 1); }; const addNewParamAtIndex = (indexToAdd: number): void => { - const cloneParamKeyValue = [...paramKeyValues]; - cloneParamKeyValue.splice(indexToAdd, 0, { key: "string", text: "" }); - setParamKeyValues(cloneParamKeyValue); - }; - - const paramValueChange = (value: string, indexOfInput: number): void => { - const cloneParamKeyValue = [...paramKeyValues]; - cloneParamKeyValue[indexOfInput].text = value; - setParamKeyValues(cloneParamKeyValue); - }; - - const paramKeyChange = ( - _event: React.FormEvent, - selectedParam: IDropdownOption, - indexOfParam: number - ): void => { - const cloneParamKeyValue = [...paramKeyValues]; - cloneParamKeyValue[indexOfParam].key = selectedParam.key.toString(); - setParamKeyValues(cloneParamKeyValue); + paramKeyValuesRef.current.splice(indexToAdd, 0, { key: "string", text: "" }); + setNumberOfParams(numberOfParams + 1); }; const addNewParamAtLastIndex = (): void => { - const cloneParamKeyValue = [...paramKeyValues]; - cloneParamKeyValue.splice(cloneParamKeyValue.length, 0, { key: "string", text: "" }); - setParamKeyValues(cloneParamKeyValue); + paramKeyValuesRef.current.push({ + key: "string", + text: "", + }); + setNumberOfParams(numberOfParams + 1); }; const props: RightPaneFormProps = { @@ -118,46 +100,52 @@ export const ExecuteSprocParamsPane: FunctionComponent submit(), }; + const getInputParameterComponent = (): JSX.Element[] => { + const inputParameters: JSX.Element[] = []; + for (let i = 0; i < numberOfParams; i++) { + const paramKeyValue = paramKeyValuesRef.current[i]; + inputParameters.push( + deleteParamAtIndex(i)} + onAddNewParamKeyPress={() => addNewParamAtIndex(i + 1)} + onParamValueChange={(_event, newInput?: string) => (paramKeyValuesRef.current[i].text = newInput)} + onParamKeyChange={(_event, selectedParam: IDropdownOption) => + (paramKeyValuesRef.current[i].key = selectedParam.key.toString()) + } + paramValue={paramKeyValue.text} + selectedKey={paramKeyValue.key} + /> + ); + } + + return inputParameters; + }; + return ( -
-
- { - setPartitionValue(newInput); - }} - onParamKeyChange={onPartitionKeyChange} - paramValue={partitionValue} - selectedKey={selectedKey.key} - /> - {paramKeyValues.map((paramKeyValue, index) => ( - deleteParamAtIndex(index)} - onAddNewParamKeyPress={() => addNewParamAtIndex(index + 1)} - onParamValueChange={(event, newInput?: string) => { - paramValueChange(newInput, index); - }} - onParamKeyChange={(event: React.FormEvent, selectedParam: IDropdownOption) => { - paramKeyChange(event, selectedParam, index); - }} - paramValue={paramKeyValue && paramKeyValue.text} - selectedKey={paramKeyValue && paramKeyValue.key} - /> - ))} - - Add param - Add New Param - -
+
+ (partitionValueRef.current = newInput)} + onParamKeyChange={(_event: React.FormEvent, item: IDropdownOption) => + (partitionKeyRef.current = item.key.toString()) + } + paramValue={partitionValueRef.current} + selectedKey={partitionKeyRef.current} + /> + {getInputParameterComponent()} + addNewParamAtLastIndex()} tabIndex={0}> + Add param + Add New Param +
); diff --git a/src/Explorer/Panes/ExecuteSprocParamsPane/InputParameter.tsx b/src/Explorer/Panes/ExecuteSprocParamsPane/InputParameter.tsx index 677158b61..6fcea9328 100644 --- a/src/Explorer/Panes/ExecuteSprocParamsPane/InputParameter.tsx +++ b/src/Explorer/Panes/ExecuteSprocParamsPane/InputParameter.tsx @@ -55,7 +55,7 @@ export const InputParameter: FunctionComponent = ({ = ({ {isAddRemoveVisible && ( <> diff --git a/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap b/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap index 3f163902a..e5bc2cbef 100644 --- a/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap +++ b/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap @@ -17,4983 +17,351 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = ` onSubmit={[Function]} >
-
- - - + - - - - -
- - - -
- - - - - - -
-
-
-
- - -
-
- - - - - -
- -
-
-
-
-
-
-
-
- - - - - - - -
- - - -
- - - - - - -
-
-
-
- - -
-
- - - - - -
- -
-
-
-
-
-
- - -
- Delete param -
-
-
-
-
- - -
- Add param -
-
-
-
-
-
-
+ Partition key value + + +
- - -
- Add param -
-
-
- + + + + + + +
+ + + + - - Add New Param - - +
+
+ + + + + +
+ +
+
+
+ +
-
+ + + + + + + + +
+ + + +
+ + + + + + +
+
+
+
+ + +
+
+ + + + + +
+ +
+
+
+
+
+
+ + +
+ Delete param +
+
+
+
+
+ + +
+ Add param +
+
+
+
+
+
+
+ +
+ + +
+ Add param +
+
+
+ + + Add New Param + + +
+
Date: Sun, 12 Dec 2021 19:41:15 -0500 Subject: [PATCH 16/33] Bug Bash issues fixes (#1162) * Bug Bash issues fixes * Remove rename from root of Temporary Workspace context menu * Update comments * Update comments --- src/Explorer/Explorer.tsx | 10 ++++------ src/Explorer/Tree/ResourceTree.tsx | 5 +++++ src/Explorer/Tree/ResourceTreeAdapter.tsx | 5 +++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index a81e2fe19..95d5be20e 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -977,7 +977,7 @@ export default class Explorer { /** * This creates a new notebook file, then opens the notebook */ - public onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): void { + public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to create new notebook, but notebook is not enabled"; handleError(error, "Explorer/onNewNotebookClicked"); @@ -986,11 +986,9 @@ export default class Explorer { const isPhoenixEnabled = NotebookUtil.isPhoenixEnabled(); if (isPhoenixEnabled) { if (isGithubTree) { - async () => { - await this.allocateContainer(); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - }; + await this.allocateContainer(); + parent = parent || this.resourceTree.myNotebooksContentRoot; + this.createNewNoteBook(parent, isGithubTree); } else { useDialog.getState().showOkCancelModalDialog( Notebook.newNotebookModalTitle, diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index f31a4a52b..fcc82331e 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -397,6 +397,11 @@ export const ResourceTree: React.FC = ({ container }: Resourc }, ]; + //disallow renaming of temporary notebook workspace + if (item?.path === useNotebook.getState().notebookBasePath) { + items = items.filter((item) => item.label !== "Rename"); + } + // For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File" if (GitHubUtils.fromContentUri(item.path)) { items = items.filter( diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index a5e75ded3..1080da312 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -808,6 +808,11 @@ export class ResourceTreeAdapter implements ReactAdapter { }, ]; + //disallow renaming of temporary notebook workspace + if (item?.path === useNotebook.getState().notebookBasePath) { + items = items.filter((item) => item.label !== "Rename"); + } + // For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File" if (GitHubUtils.fromContentUri(item.path)) { items = items.filter( From b327bfd0d631e951c9181f2dfb12c4861213ad7d Mon Sep 17 00:00:00 2001 From: Karthik chakravarthy <88904658+kcheekuri@users.noreply.github.com> Date: Mon, 13 Dec 2021 09:23:33 -0500 Subject: [PATCH 17/33] Update Api end points and add brs for allowlist (#1161) * Update Api end points and add brs for allowlist --- configs/mpac.json | 3 +- configs/prod.json | 3 +- src/ConfigContext.ts | 2 + src/Contracts/DataModels.ts | 4 - .../NotebookViewerComponent.tsx | 5 +- src/Explorer/Explorer.tsx | 64 +++++---------- .../CommandBar/CommandBarComponentAdapter.tsx | 15 +--- .../CommandBarComponentButtonFactory.tsx | 5 +- .../Notebook/NotebookContainerClient.ts | 77 ++++--------------- src/Explorer/Notebook/NotebookUtil.ts | 13 ---- src/Explorer/Notebook/useNotebook.ts | 20 ++++- .../CopyNotebookPane/CopyNotebookPane.tsx | 3 +- src/Explorer/SplashScreen/SplashScreen.tsx | 4 +- src/Explorer/Tree/Collection.ts | 4 +- src/Explorer/Tree/ResourceTree.tsx | 25 +----- src/Phoenix/PhoenixClient.ts | 74 +++++++++++++----- src/Shared/Telemetry/TelemetryConstants.ts | 1 + src/Utils/GalleryUtils.ts | 3 +- 18 files changed, 130 insertions(+), 195 deletions(-) diff --git a/configs/mpac.json b/configs/mpac.json index 7c270c6d5..f0c3654f6 100644 --- a/configs/mpac.json +++ b/configs/mpac.json @@ -1,3 +1,4 @@ { - "JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com" + "JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com", + "isTerminalEnabled" : true } \ No newline at end of file diff --git a/configs/prod.json b/configs/prod.json index e2614b018..b6716b0c3 100644 --- a/configs/prod.json +++ b/configs/prod.json @@ -1,3 +1,4 @@ { - "JUNO_ENDPOINT": "https://tools.cosmos.azure.com" + "JUNO_ENDPOINT": "https://tools.cosmos.azure.com", + "isTerminalEnabled" : false } diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index f89ed5d61..61780c128 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -27,6 +27,7 @@ export interface ConfigContext { GITHUB_CLIENT_ID: string; GITHUB_TEST_ENV_CLIENT_ID: string; GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it. + isTerminalEnabled: boolean; hostedExplorerURL: string; armAPIVersion?: string; allowedJunoOrigins: string[]; @@ -59,6 +60,7 @@ let configContext: Readonly = { GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772 JUNO_ENDPOINT: "https://tools.cosmos.azure.com", BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", + isTerminalEnabled: false, allowedJunoOrigins: [ JunoEndpoints.Test, JunoEndpoints.Test2, diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 5f5d9cc01..64a9575d3 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -438,14 +438,10 @@ export interface ContainerInfo { } export interface IProvisionData { - subscriptionId: string; - resourceGroup: string; - dbAccountName: string; cosmosEndpoint: string; } export interface IContainerData { - dbAccountName: string; forwardingId: string; } diff --git a/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx b/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx index ccaf18b83..1a9348701 100644 --- a/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx +++ b/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx @@ -17,7 +17,6 @@ import Explorer from "../../Explorer"; import { NotebookClientV2 } from "../../Notebook/NotebookClientV2"; import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper"; import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer"; -import { NotebookUtil } from "../../Notebook/NotebookUtil"; import { useNotebook } from "../../Notebook/useNotebook"; import { Dialog, TextFieldProps, useDialog } from "../Dialog"; import { NotebookMetadataComponent } from "./NotebookMetadataComponent"; @@ -148,9 +147,7 @@ export class NotebookViewerComponent { - if (NotebookUtil.isPhoenixEnabled()) { + if (useNotebook.getState().isPhoenix) { await this.allocateContainer(); const notebookServerInfo = useNotebook.getState().notebookServerInfo; if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) { @@ -1214,7 +1197,7 @@ export default class Explorer { } public async handleOpenFileAction(path: string): Promise { - if (userContext.features.phoenix) { + if (useNotebook.getState().isPhoenix) { await this.allocateContainer(); } else if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) { this._openSetupNotebooksPaneForQuickstart(); @@ -1248,7 +1231,7 @@ export default class Explorer { } public openUploadFilePanel(parent?: NotebookContentItem): void { - if (NotebookUtil.isPhoenixEnabled()) { + if (useNotebook.getState().isPhoenix) { useDialog.getState().showOkCancelModalDialog( Notebook.newNotebookUploadModalTitle, undefined, @@ -1278,7 +1261,7 @@ export default class Explorer { } public getDownloadModalConent(fileName: string): JSX.Element { - if (NotebookUtil.isPhoenixEnabled()) { + if (useNotebook.getState().isPhoenix) { return ( <>

{Notebook.galleryNotebookDownloadContent1}

@@ -1302,22 +1285,17 @@ export default class Explorer { await useNotebook.getState().refreshNotebooksEnabledStateForAccount(); // TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount - const isNotebookEnabled = userContext.features.notebooksDownBanner || userContext.features.phoenix; + const isNotebookEnabled = userContext.features.notebooksDownBanner || useNotebook.getState().isPhoenix; useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled); - useNotebook.getState().setIsShellEnabled(userContext.features.phoenix && isPublicInternetAccessAllowed()); + useNotebook.getState().setIsShellEnabled(useNotebook.getState().isPhoenix && isPublicInternetAccessAllowed()); TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, { isNotebookEnabled, dataExplorerArea: Constants.Areas.Notebook, }); - if (!userContext.features.notebooksTemporarilyDown) { - if (isNotebookEnabled) { - await this.initNotebooks(userContext.databaseAccount); - } else if (this.notebookToImport) { - // if notebooks is not enabled but the user is trying to do a quickstart setup with notebooks, open the SetupNotebooksPane - this._openSetupNotebooksPaneForQuickstart(); - } + if (useNotebook.getState().isPhoenix) { + await this.initNotebooks(userContext.databaseAccount); } } } diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index 222fe2fa4..50454be7d 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -4,15 +4,12 @@ * and update any knockout observables passed from the parent. */ import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; +import { useNotebook } from "Explorer/Notebook/useNotebook"; import * as React from "react"; import create, { UseStore } from "zustand"; import { StyleConstants } from "../../../Common/Constants"; -import * as ViewModels from "../../../Contracts/ViewModels"; -import { useTabs } from "../../../hooks/useTabs"; -import { userContext } from "../../../UserContext"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import Explorer from "../../Explorer"; -import { NotebookUtil } from "../../Notebook/NotebookUtil"; import { useSelectedNode } from "../../useSelectedNode"; import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory"; import * as CommandBarUtil from "./CommandBarUtil"; @@ -56,18 +53,10 @@ export const CommandBar: React.FC = ({ container }: Props) => { const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor); uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); - if (NotebookUtil.isPhoenixEnabled()) { + if (useNotebook.getState().isPhoenix) { uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus")); } - if ( - userContext.features.phoenix === false && - userContext.features.notebooksTemporarilyDown === false && - useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2 - ) { - uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker")); - } - return (
{ - try { - const memoryUsageInfo = await this.getMemoryUsage(); - useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo); - const notebookServerInfo = useNotebook.getState().notebookServerInfo; - if (notebookServerInfo?.notebookServerEndpoint) { - this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs); - } - } catch (exception) { - if (NotebookUtil.isPhoenixEnabled()) { - const connectionStatus: ContainerConnectionInfo = { - status: ConnectionStatusType.Failed, - }; - useNotebook.getState().resetContainerConnection(connectionStatus); - useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - } + const memoryUsageInfo = await this.getMemoryUsage(); + useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo); + const notebookServerInfo = useNotebook.getState().notebookServerInfo; + if (notebookServerInfo?.notebookServerEndpoint) { + this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs); } }, delayMs); } @@ -108,7 +91,7 @@ export class NotebookContainerClient { notebookServerEndpoint: string, authToken: string ): Promise { - if (this.checkStatus()) { + if (this.shouldExecuteMemoryCall()) { const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, { method: "GET", headers: { @@ -131,24 +114,17 @@ export class NotebookContainerClient { } else if (response.status === HttpStatusCodes.NotFound) { throw new AbortError(response.statusText); } - throw new Error(); + throw new Error(response.statusText); } else { return undefined; } } - private checkStatus(): boolean { - if (NotebookUtil.isPhoenixEnabled()) { - if (useNotebook.getState().containerStatus?.status === Constants.ContainerStatusType.Disconnected) { - const connectionStatus: ContainerConnectionInfo = { - status: ConnectionStatusType.Reconnect, - }; - useNotebook.getState().resetContainerConnection(connectionStatus); - useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - return false; - } - } - return true; + private shouldExecuteMemoryCall(): boolean { + return ( + useNotebook.getState().containerStatus?.status === Constants.ContainerStatusType.Active && + useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected + ); } public async resetWorkspace(): Promise> { @@ -173,11 +149,8 @@ export class NotebookContainerClient { } try { - if (NotebookUtil.isPhoenixEnabled()) { + if (useNotebook.getState().isPhoenix) { const provisionData: IProvisionData = { - subscriptionId: userContext.subscriptionId, - resourceGroup: userContext.resourceGroup, - dbAccountName: userContext.databaseAccount.name, cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint, }; return await this.phoenixClient.resetContainer(provisionData); @@ -185,9 +158,6 @@ export class NotebookContainerClient { return null; } catch (error) { Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace"); - if (!NotebookUtil.isPhoenixEnabled()) { - await this.recreateNotebookWorkspaceAsync(); - } throw error; } } @@ -202,25 +172,6 @@ export class NotebookContainerClient { }; } - private async recreateNotebookWorkspaceAsync(): Promise { - const { databaseAccount } = userContext; - if (!databaseAccount?.id) { - throw new Error("DataExplorer not initialized"); - } - try { - await destroy(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default"); - await createOrUpdate( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - "default" - ); - } catch (error) { - Logger.logError(getErrorMessage(error), "NotebookContainerClient/recreateNotebookWorkspaceAsync"); - return Promise.reject(error); - } - } - private getHeaders(): HeadersInit { const authorizationHeader = getAuthorizationHeader(); return { diff --git a/src/Explorer/Notebook/NotebookUtil.ts b/src/Explorer/Notebook/NotebookUtil.ts index 68d562f38..b060533d7 100644 --- a/src/Explorer/Notebook/NotebookUtil.ts +++ b/src/Explorer/Notebook/NotebookUtil.ts @@ -3,7 +3,6 @@ import { AppState, selectors } from "@nteract/core"; import domtoimage from "dom-to-image"; import Html2Canvas from "html2canvas"; import path from "path"; -import { userContext } from "../../UserContext"; import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as StringUtils from "../../Utils/StringUtils"; import { SnapshotFragment } from "./NotebookComponent/types"; @@ -329,16 +328,4 @@ export class NotebookUtil { link.click(); document.body.removeChild(link); } - - public static getNotebookBtnTitle(fileName: string): string { - if (this.isPhoenixEnabled()) { - return `Download to ${fileName}`; - } else { - return `Download to my notebooks`; - } - } - - public static isPhoenixEnabled(): boolean { - return userContext.features.notebooksTemporarilyDown === false && userContext.features.phoenix === true; - } } diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index b4f90bfb0..bc2a74372 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -1,4 +1,5 @@ import { cloneDeep } from "lodash"; +import { PhoenixClient } from "Phoenix/PhoenixClient"; import create, { UseStore } from "zustand"; import { AuthType } from "../../AuthType"; import * as Constants from "../../Common/Constants"; @@ -17,7 +18,6 @@ import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils"; import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem"; import NotebookManager from "./NotebookManager"; -import { NotebookUtil } from "./NotebookUtil"; interface NotebookState { isNotebookEnabled: boolean; @@ -37,6 +37,7 @@ interface NotebookState { isAllocating: boolean; isRefreshed: boolean; containerStatus: ContainerInfo; + isPhoenix: boolean; setIsNotebookEnabled: (isNotebookEnabled: boolean) => void; setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void; setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void; @@ -58,6 +59,8 @@ interface NotebookState { resetContainerConnection: (connectionStatus: ContainerConnectionInfo) => void; setIsRefreshed: (isAllocating: boolean) => void; setContainerStatus: (containerStatus: ContainerInfo) => void; + getPhoenixStatus: () => Promise; + setIsPhoenix: (isPhoenix: boolean) => void; } export const useNotebook: UseStore = create((set, get) => ({ @@ -92,6 +95,7 @@ export const useNotebook: UseStore = create((set, get) => ({ durationLeftInMinutes: undefined, notebookServerInfo: undefined, }, + isPhoenix: undefined, setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }), setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }), setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => @@ -104,6 +108,7 @@ export const useNotebook: UseStore = create((set, get) => ({ setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }), setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }), refreshNotebooksEnabledStateForAccount: async (): Promise => { + await get().getPhoenixStatus(); const { databaseAccount, authType } = userContext; if ( authType === AuthType.EncryptedToken || @@ -196,7 +201,7 @@ export const useNotebook: UseStore = create((set, get) => ({ isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); }, initializeNotebooksTree: async (notebookManager: NotebookManager): Promise => { - const notebookFolderName = NotebookUtil.isPhoenixEnabled() === true ? "Temporary Notebooks" : "My Notebooks"; + const notebookFolderName = get().isPhoenix ? "Temporary Notebooks" : "My Notebooks"; set({ notebookFolderName }); const myNotebooksContentRoot = { name: get().notebookFolderName, @@ -292,4 +297,15 @@ export const useNotebook: UseStore = create((set, get) => ({ }, setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }), setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }), + getPhoenixStatus: async () => { + if (get().isPhoenix === undefined) { + let isPhoenix = false; + if (userContext.features.phoenix) { + const phoenixClient = new PhoenixClient(); + isPhoenix = await phoenixClient.isDbAcountWhitelisted(); + } + set({ isPhoenix }); + } + }, + setIsPhoenix: (isPhoenix: boolean) => set({ isPhoenix }), })); diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx index 960f25845..3971abe7e 100644 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx +++ b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx @@ -5,7 +5,6 @@ import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; import { useSidePanel } from "../../../hooks/useSidePanel"; import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient"; -import { userContext } from "../../../UserContext"; import * as GitHubUtils from "../../../Utils/GitHubUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import Explorer from "../../Explorer"; @@ -76,7 +75,7 @@ export const CopyNotebookPane: FunctionComponent = ({ selectedLocation.owner, selectedLocation.repo )} - ${selectedLocation.branch}`; - } else if (selectedLocation.type === "MyNotebooks" && userContext.features.phoenix) { + } else if (selectedLocation.type === "MyNotebooks" && useNotebook.getState().isPhoenix) { destination = useNotebook.getState().notebookFolderName; } diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index 031214867..c414dc09b 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -84,9 +84,7 @@ export class SplashScreen extends React.Component { const mainItems = this.createMainItems(); const commonTaskItems = this.createCommonTaskItems(); let recentItems = this.createRecentItems(); - if (userContext.features.notebooksTemporarilyDown) { - recentItems = recentItems.filter((item) => item.description !== "Notebook"); - } + recentItems = recentItems.filter((item) => item.description !== "Notebook"); const tipsItems = this.createTipsItems(); const onClearRecent = this.clearMostRecent; diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 58c06f521..8cfa4ef13 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -1,5 +1,5 @@ import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos"; -import { NotebookUtil } from "Explorer/Notebook/NotebookUtil"; +import { useNotebook } from "Explorer/Notebook/useNotebook"; import * as ko from "knockout"; import * as _ from "underscore"; import * as Constants from "../../Common/Constants"; @@ -529,7 +529,7 @@ export default class Collection implements ViewModels.Collection { }; public onSchemaAnalyzerClick = async () => { - if (NotebookUtil.isPhoenixEnabled()) { + if (useNotebook.getState().isPhoenix) { await this.container.allocateContainer(); } useSelectedNode.getState().setSelectedNode(this); diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index fcc82331e..1e64aa26d 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -130,9 +130,8 @@ export const ResourceTree: React.FC = ({ container }: Resourc if ( myNotebooksContentRoot && - ((NotebookUtil.isPhoenixEnabled() && - useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected) || - userContext.features.phoenix === false) + useNotebook.getState().isPhoenix && + useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected ) { notebooksTree.children.push(buildMyNotebooksTree()); } @@ -166,15 +165,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc const myNotebooksTree: TreeNode = buildNotebookDirectoryNode( myNotebooksContentRoot, (item: NotebookContentItem) => { - container.openNotebook(item).then((hasOpened) => { - if ( - hasOpened && - userContext.features.notebooksTemporarilyDown === false && - userContext.features.phoenix === false - ) { - mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); - } - }); + container.openNotebook(item); } ); @@ -189,15 +180,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode( gitHubNotebooksContentRoot, (item: NotebookContentItem) => { - container.openNotebook(item).then((hasOpened) => { - if ( - hasOpened && - userContext.features.notebooksTemporarilyDown === false && - userContext.features.phoenix === false - ) { - mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); - } - }); + container.openNotebook(item); }, true ); diff --git a/src/Phoenix/PhoenixClient.ts b/src/Phoenix/PhoenixClient.ts index b7fd39955..55bd8a226 100644 --- a/src/Phoenix/PhoenixClient.ts +++ b/src/Phoenix/PhoenixClient.ts @@ -1,9 +1,18 @@ import promiseRetry, { AbortError } from "p-retry"; -import { ContainerStatusType, HttpHeaders, HttpStatusCodes, Notebook } from "../Common/Constants"; +import { Action } from "Shared/Telemetry/TelemetryConstants"; +import { + Areas, + ConnectionStatusType, + ContainerStatusType, + HttpHeaders, + HttpStatusCodes, + Notebook, +} from "../Common/Constants"; import { getErrorMessage } from "../Common/ErrorHandlingUtils"; import * as Logger from "../Common/Logger"; import { configContext } from "../ConfigContext"; import { + ContainerConnectionInfo, ContainerInfo, IContainerData, IPhoenixConnectionInfoResult, @@ -11,6 +20,7 @@ import { IResponse, } from "../Contracts/DataModels"; import { useNotebook } from "../Explorer/Notebook/useNotebook"; +import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../UserContext"; import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; @@ -35,8 +45,8 @@ export class PhoenixClient { operation: string ): Promise> { try { - const response = await fetch(`${this.getPhoenixContainerPoolingEndPoint()}/${operation}`, { - method: "POST", + const response = await fetch(`${this.getPhoenixControlPlanePathPrefix()}/containerconnections`, { + method: operation === "allocate" ? "POST" : "PATCH", headers: PhoenixClient.getHeaders(), body: JSON.stringify(provisionData), }); @@ -55,7 +65,7 @@ export class PhoenixClient { } } - public async initiateContainerHeartBeat(containerData: { forwardingId: string; dbAccountName: string }) { + public async initiateContainerHeartBeat(containerData: IContainerData) { if (this.containerHealthHandler) { clearTimeout(this.containerHealthHandler); } @@ -72,7 +82,7 @@ export class PhoenixClient { try { const runContainerStatusAsync = async () => { const response = await window.fetch( - `${this.getPhoenixContainerPoolingEndPoint()}/${containerData.dbAccountName}/${containerData.forwardingId}`, + `${this.getPhoenixControlPlanePathPrefix()}/${containerData.forwardingId}`, { method: "GET", headers: PhoenixClient.getHeaders(), @@ -86,13 +96,32 @@ export class PhoenixClient { 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), + }); + useNotebook.getState().resetContainerConnection(connectionStatus); + useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); throw new AbortError(response.statusText); } throw new Error(response.statusText); }; return await promiseRetry(runContainerStatusAsync, this.retryOptions); } catch (error) { - Logger.logError(getErrorMessage(error), "PhoenixClient/getContainerStatus"); + TelemetryProcessor.traceFailure(Action.PhoenixHeartBeat, { + dataExplorerArea: Areas.Notebook, + }); + Logger.logError(getErrorMessage(error), ""); + const connectionStatus: ContainerConnectionInfo = { + status: ConnectionStatusType.Failed, + }; + useNotebook.getState().resetContainerConnection(connectionStatus); + useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); return { durationLeftInMinutes: undefined, notebookServerInfo: undefined, @@ -101,19 +130,24 @@ export class PhoenixClient { } } - private async getContainerHealth(delayMs: number, containerData: { forwardingId: string; dbAccountName: string }) { + private async getContainerHealth(delayMs: number, containerData: IContainerData) { + const containerInfo = await this.getContainerStatusAsync(containerData); + useNotebook.getState().setContainerStatus(containerInfo); + if (useNotebook.getState().containerStatus?.status === ContainerStatusType.Active) { + this.scheduleContainerHeartbeat(delayMs, containerData); + } + } + + public async isDbAcountWhitelisted(): Promise { try { - const containerInfo = await this.getContainerStatusAsync(containerData); - useNotebook.getState().setContainerStatus(containerInfo); - if (useNotebook.getState().containerStatus?.status === ContainerStatusType.Active) { - this.scheduleContainerHeartbeat(delayMs, containerData); - } - } catch (exception) { - useNotebook.getState().setContainerStatus({ - durationLeftInMinutes: undefined, - notebookServerInfo: undefined, - status: ContainerStatusType.Disconnected, + const response = await window.fetch(`${this.getPhoenixControlPlanePathPrefix()}`, { + method: "GET", + headers: PhoenixClient.getHeaders(), }); + return response.status === HttpStatusCodes.OK; + } catch (error) { + Logger.logError(getErrorMessage(error), "PhoenixClient/IsDbAcountWhitelisted"); + return false; } } @@ -129,8 +163,10 @@ export class PhoenixClient { return phoenixEndpoint; } - public getPhoenixContainerPoolingEndPoint(): string { - return `${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer`; + public getPhoenixControlPlanePathPrefix(): string { + return `${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer/cosmosaccounts${ + userContext.databaseAccount.id + }`; } private static getHeaders(): HeadersInit { diff --git a/src/Shared/Telemetry/TelemetryConstants.ts b/src/Shared/Telemetry/TelemetryConstants.ts index e9541b9f7..71bf6cb5a 100644 --- a/src/Shared/Telemetry/TelemetryConstants.ts +++ b/src/Shared/Telemetry/TelemetryConstants.ts @@ -82,6 +82,7 @@ export enum Action { NotebooksMoveCellUpFromMenu, NotebooksMoveCellDownFromMenu, PhoenixConnection, + PhoenixHeartBeat, PhoenixResetWorkspace, DeleteCellFromMenu, OpenTerminal, diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index 060b24f3f..11e0925e5 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -10,7 +10,6 @@ import { SortBy, } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; import Explorer from "../Explorer/Explorer"; -import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil"; import { useNotebook } from "../Explorer/Notebook/useNotebook"; import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; @@ -229,7 +228,7 @@ export function downloadItem( undefined, "Download", async () => { - if (NotebookUtil.isPhoenixEnabled()) { + if (useNotebook.getState().isPhoenix) { await container.allocateContainer(); } const notebookServerInfo = useNotebook.getState().notebookServerInfo; From b7daadee20ce0efbc3955e93697f50cf973c5b3e Mon Sep 17 00:00:00 2001 From: Karthik chakravarthy <88904658+kcheekuri@users.noreply.github.com> Date: Tue, 14 Dec 2021 09:02:49 -0500 Subject: [PATCH 18/33] Hide commandbar btns when phoenix flag is false (#1174) * Hide commandbar btns when phoenix flag is false * Showing notebooks based on phoenix --- .../Menus/CommandBar/CommandBarComponentButtonFactory.tsx | 2 +- src/Explorer/Tree/ResourceTree.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 5797a0bfc..b4427c0ac 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -99,7 +99,7 @@ export function createStaticCommandBarButtons( } notebookButtons.forEach((btn) => { - if (userContext.features.notebooksTemporarilyDown) { + if (!useNotebook.getState().isPhoenix) { if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) { applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg); } else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) { diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index 1e64aa26d..3b548630f 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -121,7 +121,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc children: [], }; - if (userContext.features.notebooksTemporarilyDown) { + if (!useNotebook.getState().isPhoenix) { notebooksTree.children.push(buildNotebooksTemporarilyDownTree()); } else { if (galleryContentRoot) { From d97b9913783eb9b0a4c2223869a5ce61ae551668 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Yadav <79906609+sunilyadav840@users.noreply.github.com> Date: Thu, 16 Dec 2021 01:24:39 +0530 Subject: [PATCH 19/33] fixed screenreader copy issue (#1173) Co-authored-by: sunilyadav --- src/Explorer/OpenFullScreen.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Explorer/OpenFullScreen.tsx b/src/Explorer/OpenFullScreen.tsx index 7dabbc107..d9f4f9508 100644 --- a/src/Explorer/OpenFullScreen.tsx +++ b/src/Explorer/OpenFullScreen.tsx @@ -25,6 +25,7 @@ export const OpenFullScreen: React.FunctionComponent = () => { { copyToClipboard(readWriteUrl); setIsReadWriteUrlCopy(true); @@ -43,6 +44,7 @@ export const OpenFullScreen: React.FunctionComponent = () => { { setIsReadUrlCopy(true); copyToClipboard(readUrl); From 6351e2bcd285a6fdf39d536a7b2eb1788c8b046f Mon Sep 17 00:00:00 2001 From: Sunil Kumar Yadav <79906609+sunilyadav840@users.noreply.github.com> Date: Thu, 16 Dec 2021 01:26:40 +0530 Subject: [PATCH 20/33] fixed unshared collection error for cassandra (#1172) * fixed unshared collection error for cassandra * fixed shared props value Co-authored-by: sunilyadav --- .../CassandraAddCollectionPane/CassandraAddCollectionPane.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx index 9aa4faf6b..c2b310500 100644 --- a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx +++ b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx @@ -334,7 +334,7 @@ export const CassandraAddCollectionPane: FunctionComponent (tableThroughput = throughput)} setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)} setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)} From de58f570cda9b5276f6029c0324da0ffba1949dd Mon Sep 17 00:00:00 2001 From: vaidankarswapnil <81285216+vaidankarswapnil@users.noreply.github.com> Date: Thu, 16 Dec 2021 01:52:15 +0530 Subject: [PATCH 21/33] =?UTF-8?q?Fix=20Radio=20buttons=20present=20under?= =?UTF-8?q?=20'Settings'=20blade=20like=20=E2=80=98Custom=20and=20Unlimite?= =?UTF-8?q?d=E2=80=99=20along=20with=20its=20label=20=E2=80=98Page=20optio?= =?UTF-8?q?ns=E2=80=99=20are=20not=20enclosed=20in=20fieldset/legend=20tag?= =?UTF-8?q?=20(#1175)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix a11y setting pane radiobuttons issue * Update test snapshot issue * Implemented fieldset and legend for ChoiceGroup in HTML * cleanup --- less/documentDB.less | 8 +++++ .../Panes/SettingsPane/SettingsPane.tsx | 32 +++++++++++-------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/less/documentDB.less b/less/documentDB.less index 7ccaf4255..bc36d1483 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -2895,6 +2895,14 @@ a:link { .pageOptionsPart { padding-bottom: @MediumSpace; } + + .legendLabel { + border-bottom: 0px; + width: auto; + font-size: @mediumFontSize; + display: inline !important; + float: left; + } } // TODO: Remove these styles once we refactor all buttons to use the command button component diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index 2a06ce497..9abc63f82 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -1,4 +1,4 @@ -import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton, Stack, Text } from "@fluentui/react"; +import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui/react"; import * as Constants from "Common/Constants"; import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; import { configContext } from "ConfigContext"; @@ -115,9 +115,15 @@ export const SettingsPane: FunctionComponent = () => { }; const choiceButtonStyles = { + root: { + clear: "both", + }, flexContainer: [ { selectors: { + ".ms-ChoiceFieldGroup root-133": { + clear: "both", + }, ".ms-ChoiceField-wrapper label": { fontSize: 12, paddingTop: 0, @@ -135,22 +141,22 @@ export const SettingsPane: FunctionComponent = () => { {shouldShowQueryPageOptions && (
- - - Page options - +
+ + Page Options + Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page. - - + +
{isCustomPageOptionSelected() && ( From 529202ba7e9ac2b875f7d8ae488ad55f64f5a502 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Thu, 16 Dec 2021 14:51:18 -0800 Subject: [PATCH 22/33] Add support for date type to cassandra column types (#1176) --- .../__snapshots__/SettingsPane.test.tsx.snap | 79 ++++++++++--------- .../Tables/Validators/EntityTableHelper.tsx | 2 + src/Explorer/Tables/Constants.ts | 1 + src/Explorer/Tables/TableDataClient.ts | 3 +- 4 files changed, 46 insertions(+), 39 deletions(-) diff --git a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap index 4b1d6f079..928e7f7a7 100644 --- a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap +++ b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap @@ -16,54 +16,57 @@ exports[`Settings Pane should render Default properly 1`] = `
- - + - Page options - + Page Options + Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page. - - + /> +
Date: Wed, 22 Dec 2021 13:15:12 -0500 Subject: [PATCH 23/33] Removal of feature flag notebooksTemporarilyDown (#1178) * Removal of feature flag notebooksTemporarilyDown * Update flag * Add Vnet/Firewall check for enabling phoenix --- src/Explorer/ContextMenuButtonFactory.tsx | 1 - .../CommandBar/CommandBarComponentButtonFactory.tsx | 13 ++----------- src/Explorer/Notebook/useNotebook.ts | 3 ++- src/Explorer/SplashScreen/SplashScreen.tsx | 2 +- src/Explorer/Tree/ResourceTree.tsx | 2 +- src/Platform/Hosted/extractFeatures.ts | 2 -- 6 files changed, 6 insertions(+), 17 deletions(-) diff --git a/src/Explorer/ContextMenuButtonFactory.tsx b/src/Explorer/ContextMenuButtonFactory.tsx index a52254cc3..70d02e5aa 100644 --- a/src/Explorer/ContextMenuButtonFactory.tsx +++ b/src/Explorer/ContextMenuButtonFactory.tsx @@ -83,7 +83,6 @@ export const createCollectionContextMenuButton = ( items.push({ iconSrc: HostedTerminalIcon, - isDisabled: useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown, onClick: () => { const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); if (useNotebook.getState().isShellEnabled) { diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index b4427c0ac..8c9edb0cf 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -111,7 +111,7 @@ export function createStaticCommandBarButtons( buttons.push(btn); }); } else { - if (!isRunningOnNationalCloud() && !userContext.features.notebooksTemporarilyDown) { + if (!isRunningOnNationalCloud() && useNotebook.getState().isPhoenix) { buttons.push(createDivider()); buttons.push(createEnableNotebooksButton(container)); } @@ -169,9 +169,7 @@ export function createContextCommandBarButtons( onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); if (useNotebook.getState().isShellEnabled) { - if (!userContext.features.notebooksTemporarilyDown) { - container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); - } + container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); } else { selectedCollection && selectedCollection.onNewMongoShellClick(); } @@ -179,13 +177,6 @@ export function createContextCommandBarButtons( commandButtonLabel: label, ariaLabel: label, hasPopup: true, - tooltipText: - useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown - ? Constants.Notebook.mongoShellTemporarilyDownMsg - : undefined, - disabled: - (selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") || - (useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown), }; buttons.push(newMongoShellBtn); } diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index bc2a74372..6a0edd8f9 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -1,3 +1,4 @@ +import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { cloneDeep } from "lodash"; import { PhoenixClient } from "Phoenix/PhoenixClient"; import create, { UseStore } from "zustand"; @@ -302,7 +303,7 @@ export const useNotebook: UseStore = create((set, get) => ({ let isPhoenix = false; if (userContext.features.phoenix) { const phoenixClient = new PhoenixClient(); - isPhoenix = await phoenixClient.isDbAcountWhitelisted(); + isPhoenix = isPublicInternetAccessAllowed() && (await phoenixClient.isDbAcountWhitelisted()); } set({ isPhoenix }); } diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index c414dc09b..729e77d5e 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -221,7 +221,7 @@ export class SplashScreen extends React.Component { }); } - if (useNotebook.getState().isNotebookEnabled && !userContext.features.notebooksTemporarilyDown) { + if (useNotebook.getState().isPhoenix) { heroes.push({ iconSrc: NewNotebookIcon, title: "New Notebook", diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index 3b548630f..17d464404 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -516,7 +516,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc isNotebookEnabled && userContext.apiType === "Mongo" && isPublicInternetAccessAllowed() && - !userContext.features.notebooksTemporarilyDown + useNotebook.getState().isPhoenix ) { children.push({ label: "Schema (Preview)", diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 655b9430a..3d7931578 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -33,7 +33,6 @@ export type Features = { readonly ttl90Days: boolean; readonly mongoProxyEndpoint?: string; readonly mongoProxyAPIs?: string; - readonly notebooksTemporarilyDown: boolean; readonly enableThroughputCap: boolean; }; @@ -84,7 +83,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear autoscaleDefault: "true" === get("autoscaledefault"), partitionKeyDefault: "true" === get("partitionkeytest"), partitionKeyDefault2: "true" === get("pkpartitionkeytest"), - notebooksTemporarilyDown: "true" === get("notebookstemporarilydown", "true"), phoenix: "true" === get("phoenix"), notebooksDownBanner: "true" === get("notebooksDownBanner"), enableThroughputCap: "true" === get("enablethroughputcap"), From 025d5010b4092d9e0375cc338814edb9d5868adc Mon Sep 17 00:00:00 2001 From: Karthik chakravarthy <88904658+kcheekuri@users.noreply.github.com> Date: Wed, 22 Dec 2021 13:15:33 -0500 Subject: [PATCH 24/33] Add Pop-up on save click for temporary notebooks (#1177) * Disable auto save for notebooks * Changing auto save interval * Remove auto save tabwise * Remove auto save tabwise-1 * update file --- src/Common/Constants.ts | 4 +- .../NotebookComponentBootstrapper.tsx | 55 +++++++++++++++++-- src/Explorer/Notebook/NotebookUtil.ts | 18 ++++++ 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 36dfa83a1..6a1316681 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -365,7 +365,7 @@ export class Notebook { public static readonly containerStatusHeartbeatDelayMs = 30000; public static readonly kernelRestartInitialDelayMs = 1000; public static readonly kernelRestartMaxDelayMs = 20000; - public static readonly autoSaveIntervalMs = 120000; + public static readonly autoSaveIntervalMs = 300000; public static readonly memoryGuageToGB = 1048576; public static readonly lowMemoryThreshold = 0.8; public static readonly remainingTimeForAlert = 10; @@ -378,7 +378,7 @@ export class Notebook { "We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation."; public static saveNotebookModalTitle = "Save notebook in temporary workspace"; public static saveNotebookModalContent = - "This notebook will be saved in the temporary workspace and will be removed when the session expires. To save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your local machine before the session ends."; + "This notebook will be saved in the temporary workspace and will be removed when the session expires."; public static newNotebookModalTitle = "Create notebook in temporary workspace"; public static newNotebookUploadModalTitle = "Upload notebook to temporary workspace"; public static newNotebookModalContent1 = diff --git a/src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx b/src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx index 068c63a2f..7f1237bf6 100644 --- a/src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx +++ b/src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { Link } from "@fluentui/react"; import { CellId, CellType, ImmutableNotebook } from "@nteract/commutable"; // Vendor modules import { @@ -14,13 +15,15 @@ import "@nteract/styles/editor-overrides.css"; import "@nteract/styles/global-variables.css"; import "codemirror/addon/hint/show-hint.css"; import "codemirror/lib/codemirror.css"; +import { Notebook } from "Common/Constants"; +import { useDialog } from "Explorer/Controls/Dialog"; import * as Immutable from "immutable"; import * as React from "react"; import { Provider } from "react-redux"; import "react-table/react-table.css"; import { AnyAction, Store } from "redux"; import { NotebookClientV2 } from "../NotebookClientV2"; -import { NotebookUtil } from "../NotebookUtil"; +import { NotebookContentProviderType, NotebookUtil } from "../NotebookUtil"; import * as NteractUtil from "../NTeractUtil"; import * as CdbActions from "./actions"; import { NotebookComponent } from "./NotebookComponent"; @@ -99,6 +102,10 @@ export class NotebookComponentBootstrapper { }; } + public getNotebookPath(): string { + return this.getStore().getState().core.entities.contents.byRef.get(this.contentRef)?.filepath; + } + public setContent(name: string, content: unknown): void { this.getStore().dispatch( actions.fetchContentFulfilled({ @@ -130,11 +137,32 @@ export class NotebookComponentBootstrapper { /* Notebook operations. See nteract/packages/connected-components/src/notebook-menu/index.tsx */ public notebookSave(): void { - this.getStore().dispatch( - actions.save({ - contentRef: this.contentRef, - }) - ); + if ( + NotebookUtil.getContentProviderType(this.getNotebookPath()) === + NotebookContentProviderType.JupyterContentProviderType + ) { + useDialog.getState().showOkCancelModalDialog( + Notebook.saveNotebookModalTitle, + undefined, + "Save", + async () => { + this.getStore().dispatch( + actions.save({ + contentRef: this.contentRef, + }) + ); + }, + "Cancel", + undefined, + this.getSaveNotebookSubText() + ); + } else { + this.getStore().dispatch( + actions.save({ + contentRef: this.contentRef, + }) + ); + } } public notebookChangeKernel(kernelSpecName: string): void { @@ -341,4 +369,19 @@ export class NotebookComponentBootstrapper { protected getStore(): Store { return this.notebookClient.getStore(); } + + private getSaveNotebookSubText(): JSX.Element { + return ( + <> +

{Notebook.saveNotebookModalContent}

+
+

+ {Notebook.newNotebookModalContent2} + + {Notebook.learnMore} + +

+ + ); + } } diff --git a/src/Explorer/Notebook/NotebookUtil.ts b/src/Explorer/Notebook/NotebookUtil.ts index b060533d7..b44719beb 100644 --- a/src/Explorer/Notebook/NotebookUtil.ts +++ b/src/Explorer/Notebook/NotebookUtil.ts @@ -5,11 +5,17 @@ import Html2Canvas from "html2canvas"; import path from "path"; import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as StringUtils from "../../Utils/StringUtils"; +import * as InMemoryContentProviderUtils from "../Notebook/NotebookComponent/ContentProviders/InMemoryContentProviderUtils"; import { SnapshotFragment } from "./NotebookComponent/types"; import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem"; // Must match rx-jupyter' FileType export type FileType = "directory" | "file" | "notebook"; +export enum NotebookContentProviderType { + GitHubContentProviderType, + InMemoryContentProviderType, + JupyterContentProviderType, +} // Utilities for notebooks export class NotebookUtil { public static UntrustedNotebookRunHint = "Please trust notebook first before running any code cells"; @@ -126,6 +132,18 @@ export class NotebookUtil { return relativePath.split("/").pop(); } + public static getContentProviderType(path: string): NotebookContentProviderType { + if (InMemoryContentProviderUtils.fromContentUri(path)) { + return NotebookContentProviderType.InMemoryContentProviderType; + } + + if (GitHubUtils.fromContentUri(path)) { + return NotebookContentProviderType.GitHubContentProviderType; + } + + return NotebookContentProviderType.JupyterContentProviderType; + } + public static replaceName(path: string, newName: string): string { const contentInfo = GitHubUtils.fromContentUri(path); if (contentInfo) { From e61f9f2a389e9501a64fec337983c75bdc50b35a Mon Sep 17 00:00:00 2001 From: Sunil Kumar Yadav <79906609+sunilyadav840@users.noreply.github.com> Date: Tue, 28 Dec 2021 01:10:12 +0530 Subject: [PATCH 25/33] Fixed inconsistent use of collection id and name (#1179) Co-authored-by: sunilyadav --- src/Explorer/Panes/AddCollectionPanel.tsx | 2 +- .../DeleteCollectionConfirmationPane.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index a5d6651d5..94250b2d7 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -279,7 +279,7 @@ export class AddCollectionPanel extends React.Component - {`${getCollectionName()} ${userContext.apiType === "Mongo" ? "name" : "id"}`} + {`${getCollectionName()} id`} => { const collection = useSelectedNode.getState().findSelectedCollection(); if (!collection || inputCollectionName !== collection.id()) { - const errorMessage = "Input " + collectionName + " name does not match the selected " + collectionName; + const errorMessage = "Input " + collectionName + " id does not match the selected " + collectionName; setFormError(errorMessage); NotificationConsoleUtils.logConsoleError( `Error while deleting ${collectionName} ${collection.id()}: ${errorMessage}` From b19144f7926520afa8b8ee5afeac20be0e247c6b Mon Sep 17 00:00:00 2001 From: Sunil Kumar Yadav <79906609+sunilyadav840@users.noreply.github.com> Date: Tue, 28 Dec 2021 01:11:42 +0530 Subject: [PATCH 26/33] Fixed querytab corresponding command bar (#1180) Co-authored-by: sunilyadav --- src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index 01865246f..605122eee 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -364,13 +364,11 @@ export default class QueryTabComponent extends React.Component { - if (!this.isCloseClicked) { - useCommandBar.getState().setContextButtons(this.getTabsButtons()); - } else { - this.isCloseClicked = false; - } - }, 0); + if (!this.isCloseClicked) { + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + } else { + this.isCloseClicked = false; + } } public onExecuteQueryClick = async (): Promise => { @@ -875,9 +873,11 @@ export default class QueryTabComponent extends React.Component
From c7ceda3a3ef9abc9a66c64f6e059542c59393a26 Mon Sep 17 00:00:00 2001 From: Karthik chakravarthy <88904658+kcheekuri@users.noreply.github.com> Date: Mon, 10 Jan 2022 09:38:54 -0500 Subject: [PATCH 27/33] Disable auto save for notebooks under temporary workspace (#1181) * disable autosave only for temporary notebooks * Add override epic description --- .../Notebook/NotebookComponent/epics.ts | 56 ++++++++++++++++++- .../Notebook/NotebookComponent/store.ts | 15 +++-- 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/Explorer/Notebook/NotebookComponent/epics.ts b/src/Explorer/Notebook/NotebookComponent/epics.ts index da2bc35a8..22f214bbc 100644 --- a/src/Explorer/Notebook/NotebookComponent/epics.ts +++ b/src/Explorer/Notebook/NotebookComponent/epics.ts @@ -12,11 +12,12 @@ import { ServerConfig as JupyterServerConfig, } from "@nteract/core"; import { Channels, childOf, createMessage, JupyterMessage, message, ofMessageType } from "@nteract/messaging"; +import { defineConfigOption } from "@nteract/mythic-configuration"; import { RecordOf } from "immutable"; -import { AnyAction } from "redux"; +import { Action, AnyAction } from "redux"; import { ofType, StateObservable } from "redux-observable"; import { kernels, sessions } from "rx-jupyter"; -import { concat, EMPTY, from, merge, Observable, Observer, of, Subject, Subscriber, timer } from "rxjs"; +import { concat, EMPTY, from, interval, merge, Observable, Observer, of, Subject, Subscriber, timer } from "rxjs"; import { catchError, concatMap, @@ -41,7 +42,7 @@ import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationCons import { useDialog } from "../../Controls/Dialog"; import * as FileSystemUtil from "../FileSystemUtil"; import * as cdbActions from "../NotebookComponent/actions"; -import { NotebookUtil } from "../NotebookUtil"; +import { NotebookContentProviderType, NotebookUtil } from "../NotebookUtil"; import * as CdbActions from "./actions"; import * as TextFile from "./contents/file/text-file"; import { CdbAppState } from "./types"; @@ -948,6 +949,54 @@ const resetCellStatusOnExecuteCanceledEpic = ( ); }; +const { selector: autoSaveInterval } = defineConfigOption({ + key: "autoSaveInterval", + label: "Auto-save interval", + defaultValue: 120_000, +}); + +/** + * Override autoSaveCurrentContentEpic to disable auto save for notebooks under temporary workspace. + * @param action$ + */ +export function autoSaveCurrentContentEpic( + action$: Observable, + state$: StateObservable +): Observable { + return state$.pipe( + map((state) => autoSaveInterval(state)), + switchMap((time) => interval(time)), + mergeMap(() => { + const state = state$.value; + return from( + selectors + .contentByRef(state) + .filter( + /* + * Only save contents that are files or notebooks with + * a filepath already set. + */ + (content) => (content.type === "file" || content.type === "notebook") && content.filepath !== "" + ) + .keys() + ); + }), + filter((contentRef: ContentRef) => { + const model = selectors.model(state$.value, { contentRef }); + const content = selectors.content(state$.value, { contentRef }); + if ( + model && + model.type === "notebook" && + NotebookUtil.getContentProviderType(content.filepath) !== NotebookContentProviderType.JupyterContentProviderType + ) { + return selectors.notebook.isDirty(model); + } + return false; + }), + map((contentRef: ContentRef) => actions.save({ contentRef })) + ); +} + export const allEpics = [ addInitialCodeCellEpic, focusInitialCodeCellEpic, @@ -965,4 +1014,5 @@ export const allEpics = [ traceNotebookInfoEpic, traceNotebookKernelEpic, resetCellStatusOnExecuteCanceledEpic, + autoSaveCurrentContentEpic, ]; diff --git a/src/Explorer/Notebook/NotebookComponent/store.ts b/src/Explorer/Notebook/NotebookComponent/store.ts index d0f49f99e..fdba9d452 100644 --- a/src/Explorer/Notebook/NotebookComponent/store.ts +++ b/src/Explorer/Notebook/NotebookComponent/store.ts @@ -1,12 +1,12 @@ -import { AppState, epics as coreEpics, reducers, IContentProvider } from "@nteract/core"; -import { compose, Store, AnyAction, Middleware, Dispatch, MiddlewareAPI } from "redux"; -import { Epic } from "redux-observable"; -import { allEpics } from "./epics"; -import { coreReducer, cdbReducer } from "./reducers"; -import { catchError } from "rxjs/operators"; -import { Observable } from "rxjs"; +import { AppState, epics as coreEpics, IContentProvider, reducers } from "@nteract/core"; import { configuration } from "@nteract/mythic-configuration"; import { makeConfigureStore } from "@nteract/myths"; +import { AnyAction, compose, Dispatch, Middleware, MiddlewareAPI, Store } from "redux"; +import { Epic } from "redux-observable"; +import { Observable } from "rxjs"; +import { catchError } from "rxjs/operators"; +import { allEpics } from "./epics"; +import { cdbReducer, coreReducer } from "./reducers"; import { CdbAppState } from "./types"; const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; @@ -81,7 +81,6 @@ export const getCoreEpics = (autoStartKernelOnNotebookOpen: boolean): Epic[] => // This list needs to be consistent and in sync with core.allEpics until we figure // out how to safely filter out the ones we are overriding here. const filteredCoreEpics = [ - coreEpics.autoSaveCurrentContentEpic, coreEpics.executeCellEpic, coreEpics.executeFocusedCellEpic, coreEpics.executeCellAfterKernelLaunchEpic, From 591782195d5cb84497cd6792413f4626ce213c71 Mon Sep 17 00:00:00 2001 From: Srinath Narayanan Date: Mon, 10 Jan 2022 10:10:41 -0800 Subject: [PATCH 28/33] added phoenixfeatures flag (#1184) --- src/Common/Constants.ts | 3 +- src/Explorer/Explorer.tsx | 28 ++++++++++--------- .../CommandBar/CommandBarComponentAdapter.tsx | 2 +- .../CommandBarComponentButtonFactory.tsx | 20 +++++++------ .../Notebook/NotebookContainerClient.ts | 2 +- src/Explorer/Notebook/useNotebook.ts | 25 +++++++++++------ .../CopyNotebookPane/CopyNotebookPane.tsx | 2 +- src/Explorer/SplashScreen/SplashScreen.tsx | 2 +- src/Explorer/Tree/Collection.ts | 2 +- src/Explorer/Tree/ResourceTree.tsx | 6 ++-- src/Platform/Hosted/extractFeatures.ts | 6 ++-- src/Utils/GalleryUtils.ts | 2 +- src/hooks/useKnockoutExplorer.ts | 7 +++-- 13 files changed, 63 insertions(+), 44 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 6a1316681..e40977ded 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -96,7 +96,8 @@ export class Flights { public static readonly AutoscaleTest = "autoscaletest"; public static readonly PartitionKeyTest = "partitionkeytest"; public static readonly PKPartitionKeyTest = "pkpartitionkeytest"; - public static readonly Phoenix = "phoenix"; + public static readonly PhoenixNotebooks = "phoenixnotebooks"; + public static readonly PhoenixFeatures = "phoenixfeatures"; public static readonly NotebooksDownBanner = "notebooksdownbanner"; } diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index b04a20313..d1319f5e4 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -439,7 +439,7 @@ export default class Explorer { ); return; } - const dialogContent = useNotebook.getState().isPhoenix + const dialogContent = useNotebook.getState().isPhoenixNotebooks ? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?" : "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?"; @@ -516,7 +516,7 @@ export default class Explorer { TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, { dataExplorerArea: Areas.Notebook, }); - if (useNotebook.getState().isPhoenix) { + if (useNotebook.getState().isPhoenixNotebooks) { useTabs.getState().closeAllNotebookTabs(true); connectionStatus = { status: ConnectionStatusType.Connecting, @@ -530,7 +530,7 @@ export default class Explorer { if (!connectionInfo?.data?.notebookServerUrl) { throw new Error(`Reset Workspace: NotebookServerUrl is invalid!`); } - if (useNotebook.getState().isPhoenix) { + if (useNotebook.getState().isPhoenixNotebooks) { await this.setNotebookInfo(connectionInfo, connectionStatus); useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); } @@ -545,7 +545,7 @@ export default class Explorer { error: getErrorMessage(error), errorStack: getErrorStack(error), }); - if (useNotebook.getState().isPhoenix) { + if (useNotebook.getState().isPhoenixNotebooks) { connectionStatus = { status: ConnectionStatusType.Failed, }; @@ -743,7 +743,7 @@ export default class Explorer { if (!notebookContentItem || !notebookContentItem.path) { throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`); } - if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenix) { + if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) { await this.allocateContainer(); } @@ -967,7 +967,7 @@ export default class Explorer { handleError(error, "Explorer/onNewNotebookClicked"); throw new Error(error); } - if (useNotebook.getState().isPhoenix) { + if (useNotebook.getState().isPhoenixNotebooks) { if (isGithubTree) { await this.allocateContainer(); parent = parent || this.resourceTree.myNotebooksContentRoot; @@ -1056,7 +1056,7 @@ export default class Explorer { } public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise { - if (useNotebook.getState().isPhoenix) { + if (useNotebook.getState().isPhoenixFeatures) { await this.allocateContainer(); const notebookServerInfo = useNotebook.getState().notebookServerInfo; if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) { @@ -1197,7 +1197,7 @@ export default class Explorer { } public async handleOpenFileAction(path: string): Promise { - if (useNotebook.getState().isPhoenix) { + if (useNotebook.getState().isPhoenixNotebooks) { await this.allocateContainer(); } else if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) { this._openSetupNotebooksPaneForQuickstart(); @@ -1231,7 +1231,7 @@ export default class Explorer { } public openUploadFilePanel(parent?: NotebookContentItem): void { - if (useNotebook.getState().isPhoenix) { + if (useNotebook.getState().isPhoenixNotebooks) { useDialog.getState().showOkCancelModalDialog( Notebook.newNotebookUploadModalTitle, undefined, @@ -1261,7 +1261,7 @@ export default class Explorer { } public getDownloadModalConent(fileName: string): JSX.Element { - if (useNotebook.getState().isPhoenix) { + if (useNotebook.getState().isPhoenixNotebooks) { return ( <>

{Notebook.galleryNotebookDownloadContent1}

@@ -1285,16 +1285,18 @@ export default class Explorer { await useNotebook.getState().refreshNotebooksEnabledStateForAccount(); // TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount - const isNotebookEnabled = userContext.features.notebooksDownBanner || useNotebook.getState().isPhoenix; + const isNotebookEnabled = userContext.features.notebooksDownBanner || useNotebook.getState().isPhoenixNotebooks; useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled); - useNotebook.getState().setIsShellEnabled(useNotebook.getState().isPhoenix && isPublicInternetAccessAllowed()); + useNotebook + .getState() + .setIsShellEnabled(useNotebook.getState().isPhoenixFeatures && isPublicInternetAccessAllowed()); TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, { isNotebookEnabled, dataExplorerArea: Constants.Areas.Notebook, }); - if (useNotebook.getState().isPhoenix) { + if (useNotebook.getState().isPhoenixNotebooks) { await this.initNotebooks(userContext.databaseAccount); } } diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index 50454be7d..8827994e4 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -53,7 +53,7 @@ export const CommandBar: React.FC = ({ container }: Props) => { const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor); uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); - if (useNotebook.getState().isPhoenix) { + if (useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) { uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus")); } diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 8c9edb0cf..7397322f8 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -78,10 +78,10 @@ export function createStaticCommandBarButtons( if (container.notebookManager?.gitHubOAuthService) { notebookButtons.push(createManageGitHubAccountButton(container)); } - if (useNotebook.getState().isPhoenix && configContext.isTerminalEnabled) { + if (useNotebook.getState().isPhoenixFeatures && configContext.isTerminalEnabled) { notebookButtons.push(createOpenTerminalButton(container)); } - if (selectedNodeState.isConnectedToContainer()) { + if (useNotebook.getState().isPhoenixNotebooks && selectedNodeState.isConnectedToContainer()) { notebookButtons.push(createNotebookWorkspaceResetButton(container)); } if ( @@ -99,19 +99,21 @@ export function createStaticCommandBarButtons( } notebookButtons.forEach((btn) => { - if (!useNotebook.getState().isPhoenix) { - if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) { + if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) { + if (!useNotebook.getState().isPhoenixFeatures) { applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg); - } else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) { - applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg); - } else { - applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg); } + } else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) { + if (!useNotebook.getState().isPhoenixFeatures) { + applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg); + } + } else if (!useNotebook.getState().isPhoenixNotebooks) { + applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg); } buttons.push(btn); }); } else { - if (!isRunningOnNationalCloud() && useNotebook.getState().isPhoenix) { + if (!isRunningOnNationalCloud() && useNotebook.getState().isPhoenixNotebooks) { buttons.push(createDivider()); buttons.push(createEnableNotebooksButton(container)); } diff --git a/src/Explorer/Notebook/NotebookContainerClient.ts b/src/Explorer/Notebook/NotebookContainerClient.ts index 3479b2f2f..8b3c5500a 100644 --- a/src/Explorer/Notebook/NotebookContainerClient.ts +++ b/src/Explorer/Notebook/NotebookContainerClient.ts @@ -149,7 +149,7 @@ export class NotebookContainerClient { } try { - if (useNotebook.getState().isPhoenix) { + if (useNotebook.getState().isPhoenixNotebooks) { const provisionData: IProvisionData = { cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint, }; diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index 6a0edd8f9..1515f281f 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -38,7 +38,8 @@ interface NotebookState { isAllocating: boolean; isRefreshed: boolean; containerStatus: ContainerInfo; - isPhoenix: boolean; + isPhoenixNotebooks: boolean; + isPhoenixFeatures: boolean; setIsNotebookEnabled: (isNotebookEnabled: boolean) => void; setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void; setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void; @@ -61,7 +62,8 @@ interface NotebookState { setIsRefreshed: (isAllocating: boolean) => void; setContainerStatus: (containerStatus: ContainerInfo) => void; getPhoenixStatus: () => Promise; - setIsPhoenix: (isPhoenix: boolean) => void; + setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => void; + setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void; } export const useNotebook: UseStore = create((set, get) => ({ @@ -96,7 +98,8 @@ export const useNotebook: UseStore = create((set, get) => ({ durationLeftInMinutes: undefined, notebookServerInfo: undefined, }, - isPhoenix: undefined, + isPhoenixNotebooks: undefined, + isPhoenixFeatures: undefined, setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }), setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }), setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => @@ -202,7 +205,7 @@ export const useNotebook: UseStore = create((set, get) => ({ isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); }, initializeNotebooksTree: async (notebookManager: NotebookManager): Promise => { - const notebookFolderName = get().isPhoenix ? "Temporary Notebooks" : "My Notebooks"; + const notebookFolderName = get().isPhoenixNotebooks ? "Temporary Notebooks" : "My Notebooks"; set({ notebookFolderName }); const myNotebooksContentRoot = { name: get().notebookFolderName, @@ -299,14 +302,20 @@ export const useNotebook: UseStore = create((set, get) => ({ setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }), setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }), getPhoenixStatus: async () => { - if (get().isPhoenix === undefined) { + if (get().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) { let isPhoenix = false; - if (userContext.features.phoenix) { + if (userContext.features.phoenixNotebooks || userContext.features.phoenixFeatures) { const phoenixClient = new PhoenixClient(); isPhoenix = isPublicInternetAccessAllowed() && (await phoenixClient.isDbAcountWhitelisted()); } - set({ isPhoenix }); + + const isPhoenixNotebooks = userContext.features.phoenixNotebooks && isPhoenix; + const isPhoenixFeatures = userContext.features.phoenixFeatures && isPhoenix; + + set({ isPhoenixNotebooks: isPhoenixNotebooks }); + set({ isPhoenixFeatures: isPhoenixFeatures }); } }, - setIsPhoenix: (isPhoenix: boolean) => set({ isPhoenix }), + setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }), + setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }), })); diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx index 3971abe7e..39351c948 100644 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx +++ b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx @@ -75,7 +75,7 @@ export const CopyNotebookPane: FunctionComponent = ({ selectedLocation.owner, selectedLocation.repo )} - ${selectedLocation.branch}`; - } else if (selectedLocation.type === "MyNotebooks" && useNotebook.getState().isPhoenix) { + } else if (selectedLocation.type === "MyNotebooks" && useNotebook.getState().isPhoenixNotebooks) { destination = useNotebook.getState().notebookFolderName; } diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index 729e77d5e..fcaf49b0b 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -221,7 +221,7 @@ export class SplashScreen extends React.Component { }); } - if (useNotebook.getState().isPhoenix) { + if (useNotebook.getState().isPhoenixNotebooks) { heroes.push({ iconSrc: NewNotebookIcon, title: "New Notebook", diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 8cfa4ef13..31156c61c 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -529,7 +529,7 @@ export default class Collection implements ViewModels.Collection { }; public onSchemaAnalyzerClick = async () => { - if (useNotebook.getState().isPhoenix) { + if (useNotebook.getState().isPhoenixFeatures) { await this.container.allocateContainer(); } useSelectedNode.getState().setSelectedNode(this); diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index 17d464404..181fd07d6 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -121,7 +121,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc children: [], }; - if (!useNotebook.getState().isPhoenix) { + if (!useNotebook.getState().isPhoenixNotebooks) { notebooksTree.children.push(buildNotebooksTemporarilyDownTree()); } else { if (galleryContentRoot) { @@ -130,7 +130,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc if ( myNotebooksContentRoot && - useNotebook.getState().isPhoenix && + useNotebook.getState().isPhoenixNotebooks && useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected ) { notebooksTree.children.push(buildMyNotebooksTree()); @@ -516,7 +516,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc isNotebookEnabled && userContext.apiType === "Mongo" && isPublicInternetAccessAllowed() && - useNotebook.getState().isPhoenix + useNotebook.getState().isPhoenixFeatures ) { children.push({ label: "Schema (Preview)", diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 3d7931578..1f17d0aa9 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -11,7 +11,8 @@ export type Features = { autoscaleDefault: boolean; partitionKeyDefault: boolean; partitionKeyDefault2: boolean; - phoenix: boolean; + phoenixNotebooks: boolean; + phoenixFeatures: boolean; notebooksDownBanner: boolean; readonly enableSDKoperations: boolean; readonly enableSpark: boolean; @@ -83,7 +84,8 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear autoscaleDefault: "true" === get("autoscaledefault"), partitionKeyDefault: "true" === get("partitionkeytest"), partitionKeyDefault2: "true" === get("pkpartitionkeytest"), - phoenix: "true" === get("phoenix"), + phoenixNotebooks: "true" === get("phoenixnotebooks"), + phoenixFeatures: "true" === get("phoenixfeatures"), notebooksDownBanner: "true" === get("notebooksDownBanner"), enableThroughputCap: "true" === get("enablethroughputcap"), }; diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index 11e0925e5..635d46e2f 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -228,7 +228,7 @@ export function downloadItem( undefined, "Download", async () => { - if (useNotebook.getState().isPhoenix) { + if (useNotebook.getState().isPhoenixNotebooks) { await container.allocateContainer(); } const notebookServerInfo = useNotebook.getState().notebookServerInfo; diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 30445225c..319840b33 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -339,8 +339,11 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { if (inputs.flights.indexOf(Flights.PKPartitionKeyTest) !== -1) { userContext.features.partitionKeyDefault2 = true; } - if (inputs.flights.indexOf(Flights.Phoenix) !== -1) { - userContext.features.phoenix = 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; From b765cae0881a18a4726e1fe9c0c265e7cfd9bccd Mon Sep 17 00:00:00 2001 From: Srinath Narayanan Date: Mon, 10 Jan 2022 11:58:35 -0800 Subject: [PATCH 29/33] Close mongo and casssandra terminal tabs once the shells are exited (#1183) * initial commit for closing terminal * added extra case * lint changes and hostee explorer fixes * fixed lint errors * fixed compile error * fixed review comments --- src/Contracts/ExplorerContracts.ts | 1 + .../NotebookTerminalComponent.test.tsx | 4 ++ .../Notebook/NotebookTerminalComponent.tsx | 2 + src/Explorer/Explorer.tsx | 2 +- src/Explorer/Tabs/TerminalTab.tsx | 7 ++- src/Terminal/JupyterLabAppFactory.ts | 43 ++++++++++++++++++- src/Terminal/TerminalProps.ts | 1 + src/Terminal/index.ts | 10 ++++- src/hooks/useKnockoutExplorer.ts | 35 ++++++++++++--- 9 files changed, 95 insertions(+), 10 deletions(-) diff --git a/src/Contracts/ExplorerContracts.ts b/src/Contracts/ExplorerContracts.ts index d1c3dba58..09a271194 100644 --- a/src/Contracts/ExplorerContracts.ts +++ b/src/Contracts/ExplorerContracts.ts @@ -33,6 +33,7 @@ export enum MessageTypes { CreateWorkspace, CreateSparkPool, RefreshDatabaseAccount, + CloseTab, } export { Versions, ActionContracts, Diagnostics }; diff --git a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx index d9747d6ee..fdc93adea 100644 --- a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx +++ b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx @@ -55,6 +55,7 @@ describe("NotebookTerminalComponent", () => { const props: NotebookTerminalComponentProps = { databaseAccount: testAccount, notebookServerInfo: testNotebookServerInfo, + tabId: undefined, }; const wrapper = shallow(); @@ -65,6 +66,7 @@ describe("NotebookTerminalComponent", () => { const props: NotebookTerminalComponentProps = { databaseAccount: testMongo32Account, notebookServerInfo: testMongoNotebookServerInfo, + tabId: undefined, }; const wrapper = shallow(); @@ -75,6 +77,7 @@ describe("NotebookTerminalComponent", () => { const props: NotebookTerminalComponentProps = { databaseAccount: testMongo36Account, notebookServerInfo: testMongoNotebookServerInfo, + tabId: undefined, }; const wrapper = shallow(); @@ -85,6 +88,7 @@ describe("NotebookTerminalComponent", () => { const props: NotebookTerminalComponentProps = { databaseAccount: testCassandraAccount, notebookServerInfo: testCassandraNotebookServerInfo, + tabId: undefined, }; const wrapper = shallow(); diff --git a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx index 637f24192..9df968226 100644 --- a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx +++ b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx @@ -12,6 +12,7 @@ import * as StringUtils from "../../../Utils/StringUtils"; export interface NotebookTerminalComponentProps { notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo; databaseAccount: DataModels.DatabaseAccount; + tabId: string; } export class NotebookTerminalComponent extends React.Component { @@ -55,6 +56,7 @@ export class NotebookTerminalComponent extends React.Component tab.tabTitle() === title) as TerminalTab[]; + .getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle().startsWith(title)) as TerminalTab[]; let index = 1; if (terminalTabs.length > 0) { diff --git a/src/Explorer/Tabs/TerminalTab.tsx b/src/Explorer/Tabs/TerminalTab.tsx index 6f318f3cf..1c010d6b3 100644 --- a/src/Explorer/Tabs/TerminalTab.tsx +++ b/src/Explorer/Tabs/TerminalTab.tsx @@ -25,7 +25,8 @@ class NotebookTerminalComponentAdapter implements ReactAdapter { public parameters: ko.Computed; constructor( private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo, - private getDatabaseAccount: () => DataModels.DatabaseAccount + private getDatabaseAccount: () => DataModels.DatabaseAccount, + private getTabId: () => string ) {} public renderComponent(): JSX.Element { @@ -33,6 +34,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter { ) : ( @@ -50,7 +52,8 @@ export default class TerminalTab extends TabsBase { this.container = options.container; this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter( () => this.getNotebookServerInfo(options), - () => userContext?.databaseAccount + () => userContext?.databaseAccount, + () => this.tabId ); this.notebookTerminalComponentAdapter.parameters = ko.computed(() => { if ( diff --git a/src/Terminal/JupyterLabAppFactory.ts b/src/Terminal/JupyterLabAppFactory.ts index a1eef5bbf..639469285 100644 --- a/src/Terminal/JupyterLabAppFactory.ts +++ b/src/Terminal/JupyterLabAppFactory.ts @@ -2,15 +2,48 @@ * JupyterLab applications based on jupyterLab components */ import { ServerConnection, TerminalManager } from "@jupyterlab/services"; +import { IMessage } from "@jupyterlab/services/lib/terminal/terminal"; import { Terminal } from "@jupyterlab/terminal"; import { Panel, Widget } from "@phosphor/widgets"; +import { userContext } from "UserContext"; export class JupyterLabAppFactory { - public static async createTerminalApp(serverSettings: ServerConnection.ISettings) { + private isShellClosed: boolean; + private onShellExited: () => void; + private checkShellClosed: ((content: string | undefined) => boolean | undefined) | undefined; + + constructor(closeTab: () => void) { + this.onShellExited = closeTab; + this.isShellClosed = false; + this.checkShellClosed = undefined; + + switch (userContext.apiType) { + case "Mongo": + this.checkShellClosed = JupyterLabAppFactory.isMongoShellClosed; + break; + case "Cassandra": + this.checkShellClosed = JupyterLabAppFactory.isCassandraShellClosed; + break; + } + } + + public async createTerminalApp(serverSettings: ServerConnection.ISettings) { const manager = new TerminalManager({ serverSettings: serverSettings, }); const session = await manager.startNew(); + session.messageReceived.connect(async (_, message: IMessage) => { + const content = message.content && message.content[0]?.toString(); + if (this.checkShellClosed && message.type == "stdout") { + //Close the terminal tab once the shell closed messages are received + if (this.checkShellClosed(content)) { + this.isShellClosed = true; + } else if (content?.includes("cosmosuser@") && this.isShellClosed) { + this.onShellExited(); + } + } + }, this); + const term = new Terminal(session, { theme: "dark", shutdownOnClose: true }); if (!term) { @@ -38,4 +71,12 @@ export class JupyterLabAppFactory { panel.dispose(); }); } + + private static isMongoShellClosed(content: string | undefined) { + return content?.endsWith("bye\r\n") || (content?.includes("Stopped") && content?.includes("mongo --host")); + } + + private static isCassandraShellClosed(content: string | undefined) { + return content == "\r\n" || (content?.includes("Stopped") && content?.includes("cqlsh")); + } } diff --git a/src/Terminal/TerminalProps.ts b/src/Terminal/TerminalProps.ts index 4fe3c539c..5122a6cb7 100644 --- a/src/Terminal/TerminalProps.ts +++ b/src/Terminal/TerminalProps.ts @@ -10,4 +10,5 @@ export interface TerminalProps { authType: AuthType; apiType: ApiType; subscriptionId: string; + tabId: string; } diff --git a/src/Terminal/index.ts b/src/Terminal/index.ts index dae3059b5..f71792fab 100644 --- a/src/Terminal/index.ts +++ b/src/Terminal/index.ts @@ -1,5 +1,6 @@ import { ServerConnection } from "@jupyterlab/services"; import "@jupyterlab/terminal/style/index.css"; +import { MessageTypes } from "Contracts/ExplorerContracts"; import postRobot from "post-robot"; import { HttpHeaders } from "../Common/Constants"; import { Action } from "../Shared/Telemetry/TelemetryConstants"; @@ -54,13 +55,20 @@ const initTerminal = async (props: TerminalProps) => { const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data); try { - await JupyterLabAppFactory.createTerminalApp(serverSettings); + await new JupyterLabAppFactory(() => closeTab(props.tabId)).createTerminalApp(serverSettings); TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime); } catch (error) { TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime); } }; +const closeTab = (tabId: string): void => { + window.parent.postMessage( + { type: MessageTypes.CloseTab, data: { tabId: tabId }, signature: "pcIframe" }, + window.document.referrer + ); +}; + const main = async (): Promise => { postRobot.on( "props", diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 319840b33..245fc1fdb 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -1,3 +1,4 @@ +import { useTabs } from "hooks/useTabs"; import { useEffect, useState } from "react"; import { applyExplorerBindings } from "../applyExplorerBindings"; import { AuthType } from "../AuthType"; @@ -69,16 +70,38 @@ export function useKnockoutExplorer(platform: Platform): Explorer { async function configureHosted(): Promise { const win = (window as unknown) as HostedExplorerChildFrame; + let explorer: Explorer; if (win.hostedConfig.authType === AuthType.EncryptedToken) { - return configureHostedWithEncryptedToken(win.hostedConfig); + explorer = configureHostedWithEncryptedToken(win.hostedConfig); } else if (win.hostedConfig.authType === AuthType.ResourceToken) { - return configureHostedWithResourceToken(win.hostedConfig); + explorer = configureHostedWithResourceToken(win.hostedConfig); } else if (win.hostedConfig.authType === AuthType.ConnectionString) { - return configureHostedWithConnectionString(win.hostedConfig); + explorer = configureHostedWithConnectionString(win.hostedConfig); } else if (win.hostedConfig.authType === AuthType.AAD) { - return configureHostedWithAAD(win.hostedConfig); + explorer = await configureHostedWithAAD(win.hostedConfig); + } else { + throw new Error(`Unknown hosted config: ${win.hostedConfig}`); } - throw new Error(`Unknown hosted config: ${win.hostedConfig}`); + + window.addEventListener( + "message", + (event) => { + if (isInvalidParentFrameOrigin(event)) { + return; + } + + if (!shouldProcessMessage(event)) { + return; + } + + if (event.data?.type === MessageTypes.CloseTab) { + useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId); + } + }, + false + ); + + return explorer; } async function configureHostedWithAAD(config: AAD): Promise { @@ -261,6 +284,8 @@ async function configurePortal(): Promise { } } else if (shouldForwardMessage(message, event.origin)) { sendMessage(message); + } else if (event.data?.type === MessageTypes.CloseTab) { + useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId); } }, false From 79b6f3cf2fb470d72f918485530c5f65568aee18 Mon Sep 17 00:00:00 2001 From: Srinath Narayanan Date: Tue, 11 Jan 2022 04:54:27 -0800 Subject: [PATCH 30/33] FIxed bugs in JupyterLabAppFactory (#1187) * initial commit for closing terminal * added extra case * lint changes and hostee explorer fixes * fixed lint errors * fixed compile error * fixed review comments * modified mongo shell logic * added cassandra hell changes * fixed compile error --- src/Terminal/JupyterLabAppFactory.ts | 41 ++++++++++++++++------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/Terminal/JupyterLabAppFactory.ts b/src/Terminal/JupyterLabAppFactory.ts index 639469285..d4edb9c0e 100644 --- a/src/Terminal/JupyterLabAppFactory.ts +++ b/src/Terminal/JupyterLabAppFactory.ts @@ -8,21 +8,33 @@ import { Panel, Widget } from "@phosphor/widgets"; import { userContext } from "UserContext"; export class JupyterLabAppFactory { - private isShellClosed: boolean; + private isShellStarted: boolean | undefined; + private checkShellStarted: ((content: string | undefined) => void) | undefined; private onShellExited: () => void; - private checkShellClosed: ((content: string | undefined) => boolean | undefined) | undefined; + + private isShellExited(content: string | undefined) { + return content?.includes("cosmosuser@"); + } + + private isMongoShellStarted(content: string | undefined) { + this.isShellStarted = content?.includes("MongoDB shell version"); + } + + private isCassandraShellStarted(content: string | undefined) { + this.isShellStarted = content?.includes("Connected to") && content?.includes("cqlsh"); + } constructor(closeTab: () => void) { this.onShellExited = closeTab; - this.isShellClosed = false; - this.checkShellClosed = undefined; + this.isShellStarted = false; + this.checkShellStarted = undefined; switch (userContext.apiType) { case "Mongo": - this.checkShellClosed = JupyterLabAppFactory.isMongoShellClosed; + this.checkShellStarted = this.isMongoShellStarted; break; case "Cassandra": - this.checkShellClosed = JupyterLabAppFactory.isCassandraShellClosed; + this.checkShellStarted = this.isCassandraShellStarted; break; } } @@ -34,11 +46,12 @@ export class JupyterLabAppFactory { const session = await manager.startNew(); session.messageReceived.connect(async (_, message: IMessage) => { const content = message.content && message.content[0]?.toString(); - if (this.checkShellClosed && message.type == "stdout") { + + if (this.checkShellStarted && message.type == "stdout") { //Close the terminal tab once the shell closed messages are received - if (this.checkShellClosed(content)) { - this.isShellClosed = true; - } else if (content?.includes("cosmosuser@") && this.isShellClosed) { + if (!this.isShellStarted) { + this.checkShellStarted(content); + } else if (this.isShellExited(content)) { this.onShellExited(); } } @@ -71,12 +84,4 @@ export class JupyterLabAppFactory { panel.dispose(); }); } - - private static isMongoShellClosed(content: string | undefined) { - return content?.endsWith("bye\r\n") || (content?.includes("Stopped") && content?.includes("mongo --host")); - } - - private static isCassandraShellClosed(content: string | undefined) { - return content == "\r\n" || (content?.includes("Stopped") && content?.includes("cqlsh")); - } } From e70fa01a8bf2ffe2812eea1621ac4ced257a7095 Mon Sep 17 00:00:00 2001 From: Deborah Chen Date: Tue, 11 Jan 2022 10:45:41 -0800 Subject: [PATCH 31/33] Updating text to match backend value for large partition keys(#1186) --- src/Explorer/Panes/AddCollectionPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index 94250b2d7..a005ec253 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -667,7 +667,7 @@ export class AddCollectionPanel extends React.Component Date: Thu, 13 Jan 2022 11:35:20 -0800 Subject: [PATCH 32/33] Add headers to unblock merge private preview customers (#1190) --- src/Common/CosmosClient.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index 14aa882fa..7a85d9bd5 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -1,5 +1,6 @@ import * as Cosmos from "@azure/cosmos"; import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos"; +import { CosmosHeaders } from "@azure/cosmos/dist-esm"; import { configContext, Platform } from "../ConfigContext"; import { userContext } from "../UserContext"; import { logConsoleError } from "../Utils/NotificationConsoleUtils"; @@ -77,10 +78,21 @@ export async function getTokenFromAuthService(verb: string, resourceType: string } } +// The Capability is a bitmap, which cosmosdb backend decodes as per the below enum +enum SDKSupportedCapabilities { + None = 0, + PartitionMerge = 1 << 0, +} + let _client: Cosmos.CosmosClient; export function client(): Cosmos.CosmosClient { if (_client) return _client; + + let _defaultHeaders: CosmosHeaders = {}; + _defaultHeaders["x-ms-cosmos-sdk-supported-capabilities"] = + SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge; + const options: Cosmos.CosmosClientOptions = { endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called key: userContext.masterKey, @@ -89,6 +101,7 @@ export function client(): Cosmos.CosmosClient { enableEndpointDiscovery: false, }, userAgentSuffix: "Azure Portal", + defaultHeaders: _defaultHeaders, }; if (configContext.PROXY_PATH !== undefined) { From de5df90f75dd53168af3ba471dc142e66a5aa771 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Thu, 13 Jan 2022 15:27:43 -0500 Subject: [PATCH 33/33] Removing serverless check to show synapse link options (#1188) * removing serverless check to show synapse link enablement * fixing tests * fixing fomatting Co-authored-by: Asier Isayas --- .../CommandBarComponentButtonFactory.test.ts | 17 +---------------- .../CommandBarComponentButtonFactory.tsx | 5 ----- src/Explorer/Panes/AddCollectionPanel.tsx | 4 ---- 3 files changed, 1 insertion(+), 25 deletions(-) diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts index d6005b731..b5cca5db6 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts @@ -31,28 +31,13 @@ describe("CommandBarComponentButtonFactory tests", () => { }); }); - it("Account is not serverless - button should be visible", () => { + it("Button should be visible", () => { const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const enableAzureSynapseLinkBtn = buttons.find( (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel ); expect(enableAzureSynapseLinkBtn).toBeDefined(); }); - - it("Account is serverless - button should be hidden", () => { - updateUserContext({ - databaseAccount: { - properties: { - capabilities: [{ name: "EnableServerless" }], - }, - } as DatabaseAccount, - }); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const enableAzureSynapseLinkBtn = buttons.find( - (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel - ); - expect(enableAzureSynapseLinkBtn).toBeUndefined(); - }); }); describe("Enable notebook button", () => { diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 7397322f8..0780f9e61 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -25,7 +25,6 @@ import { useSidePanel } from "../../../hooks/useSidePanel"; import { JunoClient } from "../../../Juno/JunoClient"; import { userContext } from "../../../UserContext"; import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils"; -import { isServerlessAccount } from "../../../Utils/CapabilityUtils"; import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import Explorer from "../../Explorer"; @@ -274,10 +273,6 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo return undefined; } - if (isServerlessAccount()) { - return undefined; - } - if (userContext?.databaseAccount?.properties?.enableAnalyticalStorage) { return undefined; } diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index a005ec253..1722d97fd 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -887,10 +887,6 @@ export class AddCollectionPanel extends React.Component