From 39dd293fc118d716390aee3b522bf7eab2a543a9 Mon Sep 17 00:00:00 2001 From: Zachary Foster Date: Mon, 30 Aug 2021 15:21:32 -0400 Subject: [PATCH 01/54] Fetch aad token against tenant's authority (#1004) --- src/hooks/useAADAuth.ts | 1 + src/hooks/useKnockoutExplorer.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/hooks/useAADAuth.ts b/src/hooks/useAADAuth.ts index 630521f2d..5964707fd 100644 --- a/src/hooks/useAADAuth.ts +++ b/src/hooks/useAADAuth.ts @@ -56,6 +56,7 @@ export function useAADAuth(): ReturnType { }); setTenantId(response.tenantId); setAccount(response.account); + localStorage.setItem("cachedTenantId", response.tenantId); }, [account, tenantId] ); diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 686391376..24c100eb8 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -98,9 +98,11 @@ async function configureHostedWithAAD(config: AAD): Promise { const msalInstance = getMsalInstance(); const cachedAccount = msalInstance.getAllAccounts()?.[0]; msalInstance.setActiveAccount(cachedAccount); + const cachedTenantId = localStorage.getItem("cachedTenantId"); const aadTokenResponse = await msalInstance.acquireTokenSilent({ forceRefresh: true, scopes: [hrefEndpoint], + authority: `${configContext.AAD_ENDPOINT}${cachedTenantId}`, }); aadToken = aadTokenResponse.accessToken; } From 95c9b7ee3156a571a428a16943b0d3c37438fda2 Mon Sep 17 00:00:00 2001 From: kcheekuri <88904658+kcheekuri@users.noreply.github.com> Date: Sat, 4 Sep 2021 02:04:26 -0400 Subject: [PATCH 02/54] Users/kcheekuri/aciconatinerpooling (#1008) * initial changes for CP * Added container unprovisioning * Added postgreSQL terminal * changed postgres terminal -> shell * Initialize Container Request payload change * added postgres button * Added notebookServerInfo * Feature flag enabling and integration with phoenix * Remove postgre implementations * fix issues * fix format issues * fix format issues-1 * fix format issues-2 * fix format issues-3 * fix format issues-4 * fix format issues-5 * connection status component * connection status component-1 * connection status component-2 * connection status component-3 * address issues * removal of ms * removal of ms * removal of ms-1 * removal of time after connected * removal of time after connected * removing unnecessary code Co-authored-by: Srinath Narayanan Co-authored-by: Bala Lakshmi Narayanasami --- src/Common/Constants.ts | 8 ++ src/Contracts/DataModels.ts | 7 ++ src/Explorer/Controls/Dialog.tsx | 3 +- .../SettingsComponent.test.tsx.snap | 2 + src/Explorer/Explorer.tsx | 72 ++++++++++++----- .../CommandBar/CommandBarComponentAdapter.tsx | 2 + .../CommandBarComponentButtonFactory.tsx | 5 +- .../Menus/CommandBar/CommandBarUtil.tsx | 8 ++ .../CommandBar/ConnectionStatusComponent.less | 79 +++++++++++++++++++ .../CommandBar/ConnectionStatusComponent.tsx | 77 ++++++++++++++++++ .../Notebook/NotebookComponent/epics.ts | 2 +- .../Notebook/NotebookContainerClient.ts | 2 +- .../Notebook/NotebookContentClient.ts | 6 +- src/Explorer/Notebook/useNotebook.ts | 20 +++-- .../CopyNotebookPane/CopyNotebookPane.tsx | 3 + .../CopyNotebookPaneComponent.tsx | 6 +- .../GitHubReposPanel.test.tsx.snap | 1 + .../StringInputPane.test.tsx.snap | 1 + src/Explorer/Tree/ResourceTree.tsx | 29 +++++-- src/Explorer/Tree/ResourceTreeAdapter.tsx | 19 ++--- src/Main.tsx | 1 + src/Phoenix/PhoenixClient.ts | 70 ++++++++++++++++ src/Platform/Hosted/extractFeatures.ts | 2 + src/Utils/GalleryUtils.ts | 7 +- src/hooks/useKnockoutExplorer.ts | 3 + 25 files changed, 375 insertions(+), 60 deletions(-) create mode 100644 src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less create mode 100644 src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx create mode 100644 src/Phoenix/PhoenixClient.ts diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 293e1b3e8..6e13f07b5 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -96,6 +96,7 @@ export class Flights { public static readonly AutoscaleTest = "autoscaletest"; public static readonly PartitionKeyTest = "partitionkeytest"; public static readonly PKPartitionKeyTest = "pkpartitionkeytest"; + public static readonly Phoenix = "phoenix"; } export class AfecFeatures { @@ -337,6 +338,13 @@ export enum ConflictOperationType { Delete = "delete", } +export enum ConnectionStatusType { + Connecting = "Connecting", + Allocating = "Allocating", + Connected = "Connected", + Failed = "Connection Failed", +} + export const EmulatorMasterKey = //[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")] "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index efd5ffb78..a8957c662 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -1,3 +1,5 @@ +import { ConnectionStatusType } from "../Common/Constants"; + export interface DatabaseAccount { id: string; name: string; @@ -496,3 +498,8 @@ export interface MemoryUsageInfo { freeKB: number; totalKB: number; } + +export interface ContainerConnectionInfo { + status: ConnectionStatusType; + //need to add ram and rom info +} diff --git a/src/Explorer/Controls/Dialog.tsx b/src/Explorer/Controls/Dialog.tsx index 449627d13..362e95517 100644 --- a/src/Explorer/Controls/Dialog.tsx +++ b/src/Explorer/Controls/Dialog.tsx @@ -181,8 +181,7 @@ export const Dialog: FC = () => { text: secondaryButtonText, onClick: onSecondaryButtonClick, } - : {}; - + : undefined; return visible ? ( {choiceGroupProps && } diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index b1164fe05..6f01661ea 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -34,6 +34,7 @@ exports[`SettingsComponent renders 1`] = ` "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], + "phoenixClient": PhoenixClient {}, "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], @@ -101,6 +102,7 @@ exports[`SettingsComponent renders 1`] = ` "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], + "phoenixClient": PhoenixClient {}, "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 40321286f..2cd3e79da 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -4,6 +4,7 @@ import _ from "underscore"; import { AuthType } from "../AuthType"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import * as Constants from "../Common/Constants"; +import { ConnectionStatusType } from "../Common/Constants"; import { readCollection } from "../Common/dataAccess/readCollection"; import { readDatabases } from "../Common/dataAccess/readDatabases"; import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility"; @@ -16,6 +17,7 @@ import { GitHubOAuthService } from "../GitHub/GitHubOAuthService"; import { useSidePanel } from "../hooks/useSidePanel"; import { useTabs } from "../hooks/useTabs"; import { IGalleryItem } from "../Juno/JunoClient"; +import { PhoenixClient } from "../Phoenix/PhoenixClient"; import * as ExplorerSettings from "../Shared/ExplorerSettings"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; @@ -87,12 +89,13 @@ export default class Explorer { }; private static readonly MaxNbDatabasesToAutoExpand = 5; - + private phoenixClient: PhoenixClient; constructor() { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { dataExplorerArea: Constants.Areas.ResourceTree, }); this._isInitializingNotebooks = false; + this.phoenixClient = new PhoenixClient(); useNotebook.subscribe( () => this.refreshCommandBarButtons(), (state) => state.isNotebooksEnabledForAccount @@ -343,19 +346,43 @@ export default class Explorer { return; } this._isInitializingNotebooks = true; + if (userContext.features.phoenix) { + const connectionStatus: DataModels.ContainerConnectionInfo = { + status: ConnectionStatusType.Allocating, + }; + useNotebook.getState().setConnectionInfo(connectionStatus); + const provisionData = { + cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint, + resourceId: userContext.databaseAccount.id, + dbAccountName: userContext.databaseAccount.name, + aadToken: userContext.authorizationToken, + resourceGroup: userContext.resourceGroup, + subscriptionId: userContext.subscriptionId, + }; + const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData); + if (connectionInfo.data && connectionInfo.data.notebookServerUrl) { + connectionStatus.status = ConnectionStatusType.Connected; + useNotebook.getState().setConnectionInfo(connectionStatus); - await this.ensureNotebookWorkspaceRunning(); - const connectionInfo = await listConnectionInfo( - userContext.subscriptionId, - userContext.resourceGroup, - databaseAccount.name, - "default" - ); + useNotebook.getState().setNotebookServerInfo({ + notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl, + authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken, + }); + } + } else { + await this.ensureNotebookWorkspaceRunning(); + const connectionInfo = await listConnectionInfo( + userContext.subscriptionId, + userContext.resourceGroup, + databaseAccount.name, + "default" + ); - useNotebook.getState().setNotebookServerInfo({ - notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint, - authToken: userContext.features.notebookServerToken || connectionInfo.authToken, - }); + useNotebook.getState().setNotebookServerInfo({ + notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint, + authToken: userContext.features.notebookServerToken || connectionInfo.authToken, + }); + } useNotebook.getState().initializeNotebooksTree(this.notebookManager); @@ -364,7 +391,7 @@ export default class Explorer { this._isInitializingNotebooks = false; } - public resetNotebookWorkspace() { + public resetNotebookWorkspace(): void { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) { handleError( "Attempt to reset notebook workspace, but notebook is not enabled", @@ -389,7 +416,6 @@ export default class Explorer { if (!databaseAccount) { return false; } - try { const { value: workspaces } = await listByDatabaseAccount( userContext.subscriptionId, @@ -906,7 +932,7 @@ export default class Explorer { await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item); } - public openNotebookTerminal(kind: ViewModels.TerminalKind) { + public openNotebookTerminal(kind: ViewModels.TerminalKind): void { let title: string; switch (kind) { @@ -1026,7 +1052,10 @@ export default class Explorer { } public async handleOpenFileAction(path: string): Promise { - if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) { + if ( + userContext.features.phoenix === false && + !(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) + ) { this._openSetupNotebooksPaneForQuickstart(); } @@ -1072,10 +1101,13 @@ export default class Explorer { ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(); await useNotebook.getState().refreshNotebooksEnabledStateForAccount(); - const isNotebookEnabled: boolean = - userContext.authType !== AuthType.ResourceToken && - ((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) || - userContext.features.enableNotebooks); + let isNotebookEnabled = true; + if (!userContext.features.phoenix) { + isNotebookEnabled = + userContext.authType !== AuthType.ResourceToken && + ((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) || + userContext.features.enableNotebooks); + } useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled); useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed()); diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index 9755075a8..452d7e881 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -54,6 +54,8 @@ export const CommandBar: React.FC = ({ container }: Props) => { const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor); uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); + uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus("connectionStatus")); + if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) { uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker")); } diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 623c8250d..3e9e35b54 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -80,8 +80,9 @@ export function createStaticCommandBarButtons( } notebookButtons.push(createOpenTerminalButton(container)); - - notebookButtons.push(createNotebookWorkspaceResetButton(container)); + if (userContext.features.phoenix === false) { + notebookButtons.push(createNotebookWorkspaceResetButton(container)); + } if ( (userContext.apiType === "Mongo" && useNotebook.getState().isShellEnabled && diff --git a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx index 748657816..6549ce597 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx @@ -13,6 +13,7 @@ import { StyleConstants } from "../../../Common/Constants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; +import { ConnectionStatus } from "./ConnectionStatusComponent"; import { MemoryTracker } from "./MemoryTrackerComponent"; /** @@ -201,3 +202,10 @@ export const createMemoryTracker = (key: string): ICommandBarItemProps => { onRender: () => , }; }; + +export const createConnectionStatus = (key: string): ICommandBarItemProps => { + return { + key, + onRender: () => , + }; +}; diff --git a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less new file mode 100644 index 000000000..cb9b8b656 --- /dev/null +++ b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less @@ -0,0 +1,79 @@ +@import "../../../../less/Common/Constants"; + +.connectionStatusContainer { + cursor: default; + align-items: center; + margin: 0 9px; + border: 1px; + min-height: 44px; + + > span { + padding-right: 12px; + font-size: 13px; + font-family: @DataExplorerFont; + color: @DefaultFontColor; + } +} +.connectionStatusFailed{ + color: #bd1919; +} +.ring-container { + position: relative; +} + +.ringringGreen { + border: 3px solid green; + border-radius: 30px; + height: 18px; + width: 18px; + position: absolute; + margin: .4285em 0em 0em 0.07477em; + animation: pulsate 3s ease-out; + animation-iteration-count: infinite; + opacity: 0.0 +} +.ringringYellow{ + border: 3px solid #ffbf00; + border-radius: 30px; + height: 18px; + width: 18px; + position: absolute; + margin: .4285em 0em 0em 0.07477em; + animation: pulsate 3s ease-out; + animation-iteration-count: infinite; + opacity: 0.0 +} +.ringringRed{ + border: 3px solid #bd1919; + border-radius: 30px; + height: 18px; + width: 18px; + position: absolute; + margin: .4285em 0em 0em 0.07477em; + animation: pulsate 3s ease-out; + animation-iteration-count: infinite; + opacity: 0.0 +} +@keyframes pulsate { + 0% {-webkit-transform: scale(0.1, 0.1); opacity: 0.0;} + 15% {opacity: 0.8;} + 25% {opacity: 0.6;} + 45% {opacity: 0.4;} + 70% {opacity: 0.3;} + 100% {-webkit-transform: scale(.7, .7); opacity: 0.1;} +} +.locationGreenDot{ + font-size: 20px; + margin-right: 0.07em; + color: green; +} +.locationYellowDot{ + font-size: 20px; + margin-right: 0.07em; + color: #ffbf00; +} +.locationRedDot{ + font-size: 20px; + margin-right: 0.07em; + color: #bd1919; +} \ No newline at end of file diff --git a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx new file mode 100644 index 000000000..7e4cfa04d --- /dev/null +++ b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx @@ -0,0 +1,77 @@ +import { Icon, ProgressIndicator, Spinner, SpinnerSize, Stack, TooltipHost } from "@fluentui/react"; +import * as React from "react"; +import { ConnectionStatusType } from "../../../Common/Constants"; +import { useNotebook } from "../../Notebook/useNotebook"; +import "../CommandBar/ConnectionStatusComponent.less"; + +export const ConnectionStatus: React.FC = (): JSX.Element => { + const [second, setSecond] = React.useState("00"); + const [minute, setMinute] = React.useState("00"); + const [isActive, setIsActive] = React.useState(false); + const [counter, setCounter] = React.useState(0); + const [statusColor, setStatusColor] = React.useState("locationYellowDot"); + const [statusColorAnimation, setStatusColorAnimation] = React.useState("ringringYellow"); + const toolTipContent = "Hosted runtime status."; + React.useEffect(() => { + let intervalId: NodeJS.Timeout; + + if (isActive) { + intervalId = setInterval(() => { + const secondCounter = counter % 60; + const minuteCounter = Math.floor(counter / 60); + const computedSecond: string = String(secondCounter).length === 1 ? `0${secondCounter}` : `${secondCounter}`; + const computedMinute: string = String(minuteCounter).length === 1 ? `0${minuteCounter}` : `${minuteCounter}`; + + setSecond(computedSecond); + setMinute(computedMinute); + + setCounter((counter) => counter + 1); + }, 1000); + } + return () => clearInterval(intervalId); + }, [isActive, counter]); + + const stopTimer = () => { + setIsActive(false); + setCounter(0); + setSecond("00"); + setMinute("00"); + }; + + const connectionInfo = useNotebook((state) => state.connectionInfo); + if (!connectionInfo) { + return ( + + Connecting + + + ); + } + if (connectionInfo && connectionInfo.status === ConnectionStatusType.Allocating && isActive === false) { + setIsActive(true); + } else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) { + stopTimer(); + setStatusColor("locationGreenDot"); + setStatusColorAnimation("ringringGreen"); + } else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) { + stopTimer(); + setStatusColor("locationRedDot"); + setStatusColorAnimation("ringringRed"); + } + return ( + + +
+
+ +
+ + {connectionInfo.status} + + {connectionInfo.status === ConnectionStatusType.Allocating && isActive && ( + + )} +
+
+ ); +}; diff --git a/src/Explorer/Notebook/NotebookComponent/epics.ts b/src/Explorer/Notebook/NotebookComponent/epics.ts index 9e6616a06..da2bc35a8 100644 --- a/src/Explorer/Notebook/NotebookComponent/epics.ts +++ b/src/Explorer/Notebook/NotebookComponent/epics.ts @@ -109,7 +109,7 @@ const formWebSocketURL = (serverConfig: NotebookServiceConfig, kernelId: string, const q = params.toString(); const suffix = q !== "" ? `?${q}` : ""; - const url = (serverConfig.endpoint || "") + `api/kernels/${kernelId}/channels${suffix}`; + const url = (serverConfig.endpoint.slice(0, -1) || "") + `api/kernels/${kernelId}/channels${suffix}`; return url.replace(/^http(s)?/, "ws$1"); }; diff --git a/src/Explorer/Notebook/NotebookContainerClient.ts b/src/Explorer/Notebook/NotebookContainerClient.ts index 0e50106a7..91cedca06 100644 --- a/src/Explorer/Notebook/NotebookContainerClient.ts +++ b/src/Explorer/Notebook/NotebookContainerClient.ts @@ -56,7 +56,7 @@ export class NotebookContainerClient { const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig(); try { - const response = await fetch(`${notebookServerEndpoint}/api/metrics/memory`, { + const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, { method: "GET", headers: { Authorization: authToken, diff --git a/src/Explorer/Notebook/NotebookContentClient.ts b/src/Explorer/Notebook/NotebookContentClient.ts index 5ca408c2a..a0e368020 100644 --- a/src/Explorer/Notebook/NotebookContentClient.ts +++ b/src/Explorer/Notebook/NotebookContentClient.ts @@ -36,7 +36,10 @@ export class NotebookContentClient { * * @param parent parent folder */ - public createNewNotebookFile(parent: NotebookContentItem, isGithubTree?: boolean): Promise { + public async createNewNotebookFile( + parent: NotebookContentItem, + isGithubTree?: boolean + ): Promise { if (!parent || parent.type !== NotebookContentItemType.Directory) { throw new Error(`Parent must be a directory: ${parent}`); } @@ -99,7 +102,6 @@ export class NotebookContentClient { if (!parent || parent.type !== NotebookContentItemType.Directory) { throw new Error(`Parent must be a directory: ${parent}`); } - const filepath = NotebookUtil.getFilePath(parent.path, name); if (await this.checkIfFilepathExists(filepath)) { throw new Error(`File already exists: ${filepath}`); diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index 5a2c97b3e..348f7278e 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -28,6 +28,8 @@ interface NotebookState { myNotebooksContentRoot: NotebookContentItem; gitHubNotebooksContentRoot: NotebookContentItem; galleryContentRoot: NotebookContentItem; + connectionInfo: DataModels.ContainerConnectionInfo; + NotebookFolderName: string; setIsNotebookEnabled: (isNotebookEnabled: boolean) => void; setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void; setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void; @@ -43,6 +45,7 @@ interface NotebookState { deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void; initializeNotebooksTree: (notebookManager: NotebookManager) => Promise; initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void; + setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => void; } export const useNotebook: UseStore = create((set, get) => ({ @@ -65,6 +68,8 @@ export const useNotebook: UseStore = create((set, get) => ({ myNotebooksContentRoot: undefined, gitHubNotebooksContentRoot: undefined, galleryContentRoot: undefined, + connectionInfo: undefined, + NotebookFolderName: userContext.features.phoenix ? "My Notebooks Scratch" : "My Notebooks", setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }), setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }), setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => @@ -169,7 +174,7 @@ export const useNotebook: UseStore = create((set, get) => ({ }, initializeNotebooksTree: async (notebookManager: NotebookManager): Promise => { const myNotebooksContentRoot = { - name: "My Notebooks", + name: get().NotebookFolderName, path: get().notebookBasePath, type: NotebookContentItemType.Directory, }; @@ -178,13 +183,11 @@ export const useNotebook: UseStore = create((set, get) => ({ path: "Gallery", type: NotebookContentItemType.File, }; - const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn() - ? { - name: "GitHub repos", - path: "PsuedoDir", - type: NotebookContentItemType.Directory, - } - : undefined; + const gitHubNotebooksContentRoot = { + name: "GitHub repos", + path: "PsuedoDir", + type: NotebookContentItemType.Directory, + }; set({ myNotebooksContentRoot, galleryContentRoot, @@ -246,4 +249,5 @@ export const useNotebook: UseStore = create((set, get) => ({ set({ gitHubNotebooksContentRoot }); } }, + setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => set({ connectionInfo }), })); diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx index 3c63c00ae..d55a1feaa 100644 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx +++ b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx @@ -5,6 +5,7 @@ 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"; @@ -75,6 +76,8 @@ export const CopyNotebookPane: FunctionComponent = ({ selectedLocation.owner, selectedLocation.repo )} - ${selectedLocation.branch}`; + } else if (selectedLocation.type === "MyNotebooks" && userContext.features.phoenix) { + destination = "My Notebooks Scratch"; } clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`); diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx index 47bac0e2e..66ef0d584 100644 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx +++ b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx @@ -12,6 +12,7 @@ import { import React, { FormEvent, FunctionComponent } from "react"; import { IPinnedRepo } from "../../../Juno/JunoClient"; import * as GitHubUtils from "../../../Utils/GitHubUtils"; +import { useNotebook } from "../../Notebook/useNotebook"; import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter"; interface Location { @@ -46,11 +47,10 @@ export const CopyNotebookPaneComponent: FunctionComponent const getDropDownOptions = (): IDropdownOption[] => { const options: IDropdownOption[] = []; - options.push({ key: "MyNotebooks-Item", - text: ResourceTreeAdapter.MyNotebooksTitle, - title: ResourceTreeAdapter.MyNotebooksTitle, + text: useNotebook.getState().NotebookFolderName, + title: useNotebook.getState().NotebookFolderName, data: { type: "MyNotebooks", } as Location, diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index 7406a14ee..e7ed75e05 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -23,6 +23,7 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], + "phoenixClient": PhoenixClient {}, "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 80bb050de..0c1815afe 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -13,6 +13,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], + "phoenixClient": PhoenixClient {}, "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index 172c9caf9..9f5396ed2 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -131,14 +131,14 @@ export const ResourceTree: React.FC = ({ container }: Resourc if (myNotebooksContentRoot) { notebooksTree.children.push(buildMyNotebooksTree()); } - if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { // collapse all other notebook nodes notebooksTree.children.forEach((node) => (node.isExpanded = false)); - notebooksTree.children.push(buildGitHubNotebooksTree()); + notebooksTree.children.push(buildGitHubNotebooksTree(true)); + } else if (container.notebookManager && !container.notebookManager.gitHubOAuthService.isLoggedIn()) { + notebooksTree.children.push(buildGitHubNotebooksTree(false)); } } - return notebooksTree; }; @@ -178,7 +178,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc return myNotebooksTree; }; - const buildGitHubNotebooksTree = (): TreeNode => { + const buildGitHubNotebooksTree = (isConnected: boolean): TreeNode => { const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode( gitHubNotebooksContentRoot, (item: NotebookContentItem) => { @@ -190,8 +190,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc }, true ); - - gitHubNotebooksTree.contextMenu = [ + const manageGitContextMenu: TreeNodeMenuItem[] = [ { label: "Manage GitHub settings", onClick: () => @@ -216,7 +215,23 @@ export const ResourceTree: React.FC = ({ container }: Resourc }, }, ]; - + const connectGitContextMenu: TreeNodeMenuItem[] = [ + { + label: "Connect to GitHub", + onClick: () => + useSidePanel + .getState() + .openSidePanel( + "Connect to GitHub", + + ), + }, + ]; + gitHubNotebooksTree.contextMenu = isConnected ? manageGitContextMenu : connectGitContextMenu; gitHubNotebooksTree.isExpanded = true; gitHubNotebooksTree.isAlphaSorted = true; diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 6a9352db2..7941446d3 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -45,6 +45,7 @@ import UserDefinedFunction from "./UserDefinedFunction"; export class ResourceTreeAdapter implements ReactAdapter { public static readonly MyNotebooksTitle = "My Notebooks"; + public static readonly MyNotebooksScratchTitle = "My Notebooks Scratch"; public static readonly GitHubReposTitle = "GitHub repos"; private static readonly DataTitle = "DATA"; @@ -130,9 +131,8 @@ export class ResourceTreeAdapter implements ReactAdapter { path: "Gallery", type: NotebookContentItemType.File, }; - this.myNotebooksContentRoot = { - name: ResourceTreeAdapter.MyNotebooksTitle, + name: useNotebook.getState().NotebookFolderName, path: useNotebook.getState().notebookBasePath, type: NotebookContentItemType.Directory, }; @@ -146,16 +146,11 @@ export class ResourceTreeAdapter implements ReactAdapter { }) ); } - - if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - this.gitHubNotebooksContentRoot = { - name: ResourceTreeAdapter.GitHubReposTitle, - path: ResourceTreeAdapter.PseudoDirPath, - type: NotebookContentItemType.Directory, - }; - } else { - this.gitHubNotebooksContentRoot = undefined; - } + this.gitHubNotebooksContentRoot = { + name: ResourceTreeAdapter.GitHubReposTitle, + path: ResourceTreeAdapter.PseudoDirPath, + type: NotebookContentItemType.Directory, + }; return Promise.all(refreshTasks); } diff --git a/src/Main.tsx b/src/Main.tsx index 80d008b17..c38012b64 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -37,6 +37,7 @@ import "./Explorer/Controls/TreeComponent/treeComponent.less"; import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less"; import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter"; +import "./Explorer/Menus/CommandBar/ConnectionStatusComponent.less"; import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less"; import "./Explorer/Menus/NotificationConsole/NotificationConsole.less"; import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; diff --git a/src/Phoenix/PhoenixClient.ts b/src/Phoenix/PhoenixClient.ts new file mode 100644 index 000000000..155672cd3 --- /dev/null +++ b/src/Phoenix/PhoenixClient.ts @@ -0,0 +1,70 @@ +import { ConnectionStatusType, HttpHeaders, HttpStatusCodes } from "../Common/Constants"; +import { configContext } from "../ConfigContext"; +import * as DataModels from "../Contracts/DataModels"; +import { useNotebook } from "../Explorer/Notebook/useNotebook"; +import { userContext } from "../UserContext"; +import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; + +export interface IPhoenixResponse { + status: number; + data: T; +} +export interface IPhoenixConnectionInfoResult { + readonly notebookAuthToken?: string; + readonly notebookServerUrl?: string; +} +export interface IProvosionData { + cosmosEndpoint: string; + resourceId: string; + dbAccountName: string; + aadToken: string; + resourceGroup: string; + subscriptionId: string; +} +export class PhoenixClient { + public async containerConnectionInfo( + provisionData: IProvosionData + ): Promise> { + const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/provision`, { + method: "POST", + headers: PhoenixClient.getHeaders(), + body: JSON.stringify(provisionData), + }); + let data: IPhoenixConnectionInfoResult; + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + } else { + const connectionStatus: DataModels.ContainerConnectionInfo = { + status: ConnectionStatusType.Failed, + }; + useNotebook.getState().setConnectionInfo(connectionStatus); + } + + return { + status: response.status, + data, + }; + } + + public static getPhoenixEndpoint(): string { + const phoenixEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT; + if (configContext.allowedJunoOrigins.indexOf(new URL(phoenixEndpoint).origin) === -1) { + const error = `${phoenixEndpoint} not allowed as juno endpoint`; + console.error(error); + throw new Error(error); + } + + return phoenixEndpoint; + } + + public getPhoenixContainerPoolingEndPoint(): string { + return `${PhoenixClient.getPhoenixEndpoint()}/api/containerpooling`; + } + private static getHeaders(): HeadersInit { + const authorizationHeader = getAuthorizationHeader(); + return { + [authorizationHeader.header]: authorizationHeader.token, + [HttpHeaders.contentType]: "application/json", + }; + } +} diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 6e4992fc6..29c106a6e 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -11,6 +11,7 @@ export type Features = { autoscaleDefault: boolean; partitionKeyDefault: boolean; partitionKeyDefault2: boolean; + phoenix: boolean; readonly enableSDKoperations: boolean; readonly enableSpark: boolean; readonly enableTtl: boolean; @@ -76,5 +77,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear partitionKeyDefault: "true" === get("partitionkeytest"), partitionKeyDefault2: "true" === get("pkpartitionkeytest"), notebooksTemporarilyDown: "true" === get("notebookstemporarilydown", "true"), + phoenix: "true" === get("phoenix"), }; } diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index d39a74a04..db3f081de 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -10,6 +10,7 @@ import { SortBy, } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; import Explorer from "../Explorer/Explorer"; +import { useNotebook } from "../Explorer/Notebook/useNotebook"; import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { trace, traceFailure, traceStart, traceSuccess } from "../Shared/Telemetry/TelemetryProcessor"; @@ -223,11 +224,13 @@ export function downloadItem( const name = data.name; useDialog.getState().showOkCancelModalDialog( - "Download to My Notebooks", + `Download to ${useNotebook.getState().NotebookFolderName}`, `Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`, "Download", async () => { - const clearInProgressMessage = logConsoleProgress(`Downloading ${name} to My Notebooks`); + const clearInProgressMessage = logConsoleProgress( + `Downloading ${name} to ${useNotebook.getState().NotebookFolderName}` + ); const startKey = traceStart(Action.NotebooksGalleryDownload, { notebookId: data.id, downloadCount: data.downloads, diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 24c100eb8..6e45e26d8 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -339,6 +339,9 @@ 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; + } } } From 65882ea8316010ba89a0e38565e7e5f5c521f7f9 Mon Sep 17 00:00:00 2001 From: Meha Kaushik Date: Tue, 7 Sep 2021 23:42:39 -0700 Subject: [PATCH 03/54] Self-Server for GraphAPI Compute (#1017) * Self-Server for GraphAPI Compute * Update GraphAPICompute.json --- src/Localization/en/GraphAPICompute.json | 57 +++ .../GraphAPICompute/GraphAPICompute.rp.ts | 200 +++++++++ .../GraphAPICompute/GraphAPICompute.tsx | 423 ++++++++++++++++++ .../GraphAPICompute/GraphAPICompute.types.ts | 65 +++ src/SelfServe/SelfServe.tsx | 8 + src/SelfServe/SelfServeUtils.tsx | 1 + 6 files changed, 754 insertions(+) create mode 100644 src/Localization/en/GraphAPICompute.json create mode 100644 src/SelfServe/GraphAPICompute/GraphAPICompute.rp.ts create mode 100644 src/SelfServe/GraphAPICompute/GraphAPICompute.tsx create mode 100644 src/SelfServe/GraphAPICompute/GraphAPICompute.types.ts diff --git a/src/Localization/en/GraphAPICompute.json b/src/Localization/en/GraphAPICompute.json new file mode 100644 index 000000000..a55102f37 --- /dev/null +++ b/src/Localization/en/GraphAPICompute.json @@ -0,0 +1,57 @@ +{ + "GraphAPIDescription": "Provision a Graph API Compute for your Azure Cosmos DB account.", + "GraphAPICompute": "Graph GraphAPI Compute", + "Provisioned": "Provisioned", + "Deprovisioned": "Deprovisioned", + "Compute": "Compute", + "GremlinV2": "GremlinV2", + "LearnAboutCompute": "Learn more about GraphAPI Compute.", + "DeprovisioningDetailsText": "Learn more about deprovisioning the GraphAPI Compute.", + "ComputePricing": "Learn more about GraphAPI Compute pricing.", + "SKUs": "SKUs", + "SKUsPlaceHolder": "Select SKUs", + "NumberOfInstances": "Number of instances", + "CosmosD4s": "Cosmos.D4s (General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory)", + "CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)", + "CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)", + "CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)", + "CreateMessage": "Graph GraphAPI Compute resource is being created.", + "CreateInitializeTitle": "Provisioning resource", + "CreateInitializeMessage": "GraphAPI Compute resource will be provisioned.", + "CreateSuccessTitle": "Resource provisioned", + "CreateSuccesseMessage": "GraphAPI Compute resource provisioned.", + "CreateFailureTitle": "Failed to provision resource", + "CreateFailureMessage": "GraphAPI Compute resource provisioning failed.", + "UpdateMessage": "GraphAPI Compute resource is being updated.", + "UpdateInitializeTitle": "Updating resource", + "UpdateInitializeMessage": "GraphAPI Compute resource will be updated.", + "UpdateSuccessTitle": "Resource updated", + "UpdateSuccesseMessage": "GraphAPI Compute resource updated.", + "UpdateFailureTitle": "Failed to update resource", + "UpdateFailureMessage": "GraphAPI Compute resource updation failed.", + "DeleteMessage": "GraphAPI Compute resource is being deleted.", + "DeleteInitializeTitle": "Deleting resource", + "DeleteInitializeMessage": "GraphAPI Compute resource will be deleted.", + "DeleteSuccessTitle": "Resource deleted", + "DeleteSuccesseMessage": "GraphAPI Compute resource deleted.", + "DeleteFailureTitle": "Failed to delete resource", + "DeleteFailureMessage": "GraphAPI Compute resource deletion failed.", + "CannotSave": "Cannot save the changes to the GraphAPI Compute resource at the moment.", + "GraphAccountEndpoint": "Graph Account Endpoint", + "CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory", + "CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory", + "CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory", + "ApproximateCost": "Approximate Cost Per Hour", + "CostText": "Hourly cost of the GraphAPI Compute resource depends on the SKU selection, number of instances per region, and number of regions.", + "ConnectionString": "Connection String", + "ConnectionStringText": "To use the GraphAPI Compute, use the connection string shown in ", + "KeysBlade": "the keys blade.", + "MetricsString": "Metrics", + "MetricsText": "Monitor the CPU and memory usage for the GraphAPI Compute instances in ", + "MetricsBlade": "the metrics blade.", + "MonitorUsage": "Monitor Usage", + "ResizingDecisionText": "Number of instances has to be 1 during provisioning. Instances can only be incremented by 1 at once. ", + "ResizingDecisionLink": "Learn more about GraphAPI Compute sizing.", + "WarningBannerOnUpdate": "Adding or modifying GraphAPI Compute instances may affect your bill.", + "WarningBannerOnDelete": "After deprovisioning the GraphAPI Compute, you will not be able to connect to the Graph API account." +} diff --git a/src/SelfServe/GraphAPICompute/GraphAPICompute.rp.ts b/src/SelfServe/GraphAPICompute/GraphAPICompute.rp.ts new file mode 100644 index 000000000..a80ebff4e --- /dev/null +++ b/src/SelfServe/GraphAPICompute/GraphAPICompute.rp.ts @@ -0,0 +1,200 @@ +import { configContext } from "../../ConfigContext"; +import { userContext } from "../../UserContext"; +import { armRequestWithoutPolling } from "../../Utils/arm/request"; +import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor"; +import { RefreshResult } from "../SelfServeTypes"; +import GraphAPICompute from "./GraphAPICompute"; +import { + FetchPricesResponse, + RegionsResponse, + GraphAPIComputeServiceResource, + UpdateComputeRequestParameters, +} from "./GraphAPICompute.types"; + +const apiVersion = "2021-04-01-preview"; +const gremlinV2 = "GremlinV2"; + +export enum ResourceStatus { + Running = "Running", + Creating = "Creating", + Updating = "Updating", + Deleting = "Deleting", +} + +export interface ComputeResponse { + sku: string; + instances: number; + status: string; + endpoint: string; +} + +export const getPath = (subscriptionId: string, resourceGroup: string, name: string): string => { + return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}/services/${gremlinV2}`; +}; + +export const updateComputeResource = async (sku: string, instances: number): Promise => { + const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); + const body: UpdateComputeRequestParameters = { + properties: { + instanceSize: sku, + instanceCount: instances, + serviceType: gremlinV2, + }, + }; + const telemetryData = { ...body, httpMethod: "PUT", selfServeClassName: GraphAPICompute.name }; + const updateTimeStamp = selfServeTraceStart(telemetryData); + let armRequestResult; + try { + armRequestResult = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path, + method: "PUT", + apiVersion, + body, + }); + selfServeTraceSuccess(telemetryData, updateTimeStamp); + } catch (e) { + const failureTelemetry = { ...body, e, selfServeClassName: GraphAPICompute.name }; + selfServeTraceFailure(failureTelemetry, updateTimeStamp); + throw e; + } + return armRequestResult?.operationStatusUrl; +}; + +export const deleteComputeResource = async (): Promise => { + const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); + const telemetryData = { httpMethod: "DELETE", selfServeClassName: GraphAPICompute.name }; + const deleteTimeStamp = selfServeTraceStart(telemetryData); + let armRequestResult; + try { + armRequestResult = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path, + method: "DELETE", + apiVersion, + }); + selfServeTraceSuccess(telemetryData, deleteTimeStamp); + } catch (e) { + const failureTelemetry = { e, selfServeClassName: GraphAPICompute.name }; + selfServeTraceFailure(failureTelemetry, deleteTimeStamp); + throw e; + } + return armRequestResult?.operationStatusUrl; +}; + +export const getComputeResource = async (): Promise => { + const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); + const telemetryData = { httpMethod: "GET", selfServeClassName: GraphAPICompute.name }; + const getResourceTimeStamp = selfServeTraceStart(telemetryData); + let armRequestResult; + try { + armRequestResult = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path, + method: "GET", + apiVersion, + }); + selfServeTraceSuccess(telemetryData, getResourceTimeStamp); + } catch (e) { + const failureTelemetry = { e, selfServeClassName: GraphAPICompute.name }; + selfServeTraceFailure(failureTelemetry, getResourceTimeStamp); + throw e; + } + return armRequestResult?.result; +}; + +export const getCurrentProvisioningState = async (): Promise => { + try { + const response = await getComputeResource(); + return { + sku: response.properties.instanceSize, + instances: response.properties.instanceCount, + status: response.properties.status, + endpoint: response.properties.GraphAPIComputeEndPoint, + }; + } catch (e) { + return { sku: undefined, instances: undefined, status: undefined, endpoint: undefined }; + } +}; + +export const refreshComputeProvisioning = async (): Promise => { + try { + const response = await getComputeResource(); + if (response.properties.status === ResourceStatus.Running.toString()) { + return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined }; + } else if (response.properties.status === ResourceStatus.Creating.toString()) { + return { isUpdateInProgress: true, updateInProgressMessageTKey: "CreateMessage" }; + } else if (response.properties.status === ResourceStatus.Deleting.toString()) { + return { isUpdateInProgress: true, updateInProgressMessageTKey: "DeleteMessage" }; + } else { + return { isUpdateInProgress: true, updateInProgressMessageTKey: "UpdateMessage" }; + } + } catch { + //TODO differentiate between different failures + return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined }; + } +}; + +const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: string): string => { + return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`; +}; + +export const getReadRegions = async (): Promise> => { + try { + const readRegions = new Array(); + + const response = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path: getGeneralPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name), + method: "GET", + apiVersion: "2021-04-01-preview", + }); + + if (response.result.location !== undefined) { + readRegions.push(response.result.location.replace(" ", "").toLowerCase()); + } else { + for (const location of response.result.locations) { + readRegions.push(location.locationName.replace(" ", "").toLowerCase()); + } + } + return readRegions; + } catch (err) { + return new Array(); + } +}; + +const getFetchPricesPathForRegion = (subscriptionId: string): string => { + return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`; +}; + +export const getPriceMap = async (regions: Array): Promise>> => { + try { + const priceMap = new Map>(); + + for (const region of regions) { + const regionPriceMap = new Map(); + + const response = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path: getFetchPricesPathForRegion(userContext.subscriptionId), + method: "POST", + apiVersion: "2020-01-01-preview", + queryParams: { + filter: + "armRegionName eq '" + + region + + "' and serviceFamily eq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'", + }, + }); + + for (const item of response.result.Items) { + regionPriceMap.set(item.skuName, item.retailPrice); + } + priceMap.set(region, regionPriceMap); + } + + return priceMap; + } catch (err) { + return undefined; + } +}; diff --git a/src/SelfServe/GraphAPICompute/GraphAPICompute.tsx b/src/SelfServe/GraphAPICompute/GraphAPICompute.tsx new file mode 100644 index 000000000..677395838 --- /dev/null +++ b/src/SelfServe/GraphAPICompute/GraphAPICompute.tsx @@ -0,0 +1,423 @@ +import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators"; +import { selfServeTrace } from "../SelfServeTelemetryProcessor"; +import { + ChoiceItem, + Description, + DescriptionType, + Info, + InputType, + NumberUiType, + OnSaveResult, + RefreshResult, + SelfServeBaseClass, + SmartUiInput, +} from "../SelfServeTypes"; +import { BladeType, generateBladeLink } from "../SelfServeUtils"; +import { + deleteComputeResource, + getCurrentProvisioningState, + getPriceMap, + getReadRegions, + refreshComputeProvisioning, + updateComputeResource, +} from "./GraphAPICompute.rp"; + +const costPerHourDefaultValue: Description = { + textTKey: "CostText", + type: DescriptionType.Text, + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", + textTKey: "ComputePricing", + }, +}; + +const connectionStringValue: Description = { + textTKey: "ConnectionStringText", + type: DescriptionType.Text, + link: { + href: generateBladeLink(BladeType.SqlKeys), + textTKey: "KeysBlade", + }, +}; + +const metricsStringValue: Description = { + textTKey: "MetricsText", + type: DescriptionType.Text, + link: { + href: generateBladeLink(BladeType.Metrics), + textTKey: "MetricsBlade", + }, +}; + +const CosmosD4s = "Cosmos.D4s"; +const CosmosD8s = "Cosmos.D8s"; +const CosmosD16s = "Cosmos.D16s"; + +const onSKUChange = (newValue: InputType, currentValues: Map): Map => { + currentValues.set("sku", { value: newValue }); + currentValues.set("costPerHour", { + value: calculateCost(newValue as string, currentValues.get("instances").value as number), + }); + + return currentValues; +}; + +const onNumberOfInstancesChange = ( + newValue: InputType, + currentValues: Map, + baselineValues: Map +): Map => { + currentValues.set("instances", { value: newValue }); + const ComputeOriginallyEnabled = baselineValues.get("enableCompute")?.value as boolean; + const baselineInstances = baselineValues.get("instances")?.value as number; + if (!ComputeOriginallyEnabled || baselineInstances !== newValue) { + currentValues.set("warningBanner", { + value: { + textTKey: "WarningBannerOnUpdate", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", + textTKey: "ComputePricing", + }, + } as Description, + hidden: false, + }); + } else { + currentValues.set("warningBanner", undefined); + } + + currentValues.set("costPerHour", { + value: calculateCost(currentValues.get("sku").value as string, newValue as number), + }); + + return currentValues; +}; + +const onEnableComputeChange = ( + newValue: InputType, + currentValues: Map, + baselineValues: ReadonlyMap +): Map => { + currentValues.set("enableCompute", { value: newValue }); + const ComputeOriginallyEnabled = baselineValues.get("enableCompute")?.value as boolean; + if (ComputeOriginallyEnabled === newValue) { + currentValues.set("sku", baselineValues.get("sku")); + currentValues.set("instances", baselineValues.get("instances")); + currentValues.set("costPerHour", baselineValues.get("costPerHour")); + currentValues.set("warningBanner", baselineValues.get("warningBanner")); + currentValues.set("connectionString", baselineValues.get("connectionString")); + currentValues.set("metricsString", baselineValues.get("metricsString")); + return currentValues; + } + + currentValues.set("warningBanner", undefined); + if (newValue === true) { + currentValues.set("warningBanner", { + value: { + textTKey: "WarningBannerOnUpdate", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", //needs updating + textTKey: "ComputePricing", + }, + } as Description, + hidden: false, + }); + + currentValues.set("costPerHour", { + value: calculateCost(baselineValues.get("sku").value as string, baselineValues.get("instances").value as number), + hidden: false, + }); + } else { + currentValues.set("warningBanner", { + value: { + textTKey: "WarningBannerOnDelete", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", // needs updating + textTKey: "DeprovisioningDetailsText", + }, + } as Description, + hidden: false, + }); + + currentValues.set("costPerHour", { value: costPerHourDefaultValue, hidden: true }); + } + const sku = currentValues.get("sku"); + const hideAttributes = newValue === undefined || !(newValue as boolean); + currentValues.set("sku", { + value: sku.value, + hidden: hideAttributes, + disabled: ComputeOriginallyEnabled, + }); + currentValues.set("instances", { + value: 1, + hidden: hideAttributes, + disabled: true, + }); + + currentValues.set("connectionString", { + value: connectionStringValue, + hidden: !newValue || !ComputeOriginallyEnabled, + }); + + currentValues.set("metricsString", { + value: metricsStringValue, + hidden: !newValue || !ComputeOriginallyEnabled, + }); + + return currentValues; +}; + +const skuDropDownItems: ChoiceItem[] = [ + { labelTKey: "CosmosD4s", key: CosmosD4s }, + { labelTKey: "CosmosD8s", key: CosmosD8s }, + { labelTKey: "CosmosD16s", key: CosmosD16s }, +]; + +const getSkus = async (): Promise => { + return skuDropDownItems; +}; + +const NumberOfInstancesDropdownInfo: Info = { + messageTKey: "ResizingDecisionText", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-size", // todo + textTKey: "ResizingDecisionLink", + }, +}; + +const getInstancesMin = async (): Promise => { + return 1; +}; + +const getInstancesMax = async (): Promise => { + return 5; +}; + +const ApproximateCostDropDownInfo: Info = { + messageTKey: "CostText", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", //todo + textTKey: "ComputePricing", + }, +}; + +let priceMap: Map>; +let regions: Array; + +const calculateCost = (skuName: string, instanceCount: number): Description => { + try { + let costPerHour = 0; + for (const region of regions) { + const incrementalCost = priceMap.get(region).get(skuName.replace("Cosmos.", "")); + if (incrementalCost === undefined) { + throw new Error("Value not found in map."); + } + costPerHour += incrementalCost; + } + + costPerHour *= instanceCount; + costPerHour = Math.round(costPerHour * 100) / 100; + + return { + textTKey: `${costPerHour} USD`, + type: DescriptionType.Text, + }; + } catch (err) { + return costPerHourDefaultValue; + } +}; + +@IsDisplayable() +@RefreshOptions({ retryIntervalInMs: 20000 }) +export default class GraphAPICompute extends SelfServeBaseClass { + public onRefresh = async (): Promise => { + return await refreshComputeProvisioning(); + }; + + public onSave = async ( + currentValues: Map, + baselineValues: Map + ): Promise => { + selfServeTrace({ selfServeClassName: GraphAPICompute.name }); + + const ComputeCurrentlyEnabled = currentValues.get("enableCompute")?.value as boolean; + const ComputeOriginallyEnabled = baselineValues.get("enableCompute")?.value as boolean; + + currentValues.set("warningBanner", undefined); + + if (ComputeOriginallyEnabled) { + if (!ComputeCurrentlyEnabled) { + const operationStatusUrl = await deleteComputeResource(); + return { + operationStatusUrl: operationStatusUrl, + portalNotification: { + initialize: { + titleTKey: "DeleteInitializeTitle", + messageTKey: "DeleteInitializeMessage", + }, + success: { + titleTKey: "DeleteSuccessTitle", + messageTKey: "DeleteSuccesseMessage", + }, + failure: { + titleTKey: "DeleteFailureTitle", + messageTKey: "DeleteFailureMessage", + }, + }, + }; + } else { + const sku = currentValues.get("sku")?.value as string; + const instances = currentValues.get("instances").value as number; + const operationStatusUrl = await updateComputeResource(sku, instances); + return { + operationStatusUrl: operationStatusUrl, + portalNotification: { + initialize: { + titleTKey: "UpdateInitializeTitle", + messageTKey: "UpdateInitializeMessage", + }, + success: { + titleTKey: "UpdateSuccessTitle", + messageTKey: "UpdateSuccesseMessage", + }, + failure: { + titleTKey: "UpdateFailureTitle", + messageTKey: "UpdateFailureMessage", + }, + }, + }; + } + } else { + const sku = currentValues.get("sku")?.value as string; + const instances = currentValues.get("instances").value as number; + const operationStatusUrl = await updateComputeResource(sku, instances); + return { + operationStatusUrl: operationStatusUrl, + portalNotification: { + initialize: { + titleTKey: "CreateInitializeTitle", + messageTKey: "CreateInitializeMessage", + }, + success: { + titleTKey: "CreateSuccessTitle", + messageTKey: "CreateSuccesseMessage", + }, + failure: { + titleTKey: "CreateFailureTitle", + messageTKey: "CreateFailureMessage", + }, + }, + }; + } + }; + + public initialize = async (): Promise> => { + // Based on the RP call enableCompute will be true if it has not yet been enabled and false if it has. + const defaults = new Map(); + defaults.set("enableCompute", { value: false }); + defaults.set("sku", { value: CosmosD4s, hidden: true }); + defaults.set("instances", { value: 1, hidden: true }); + defaults.set("costPerHour", undefined); + defaults.set("connectionString", undefined); + defaults.set("metricsString", { + value: undefined, + hidden: true, + }); + + regions = await getReadRegions(); + priceMap = await getPriceMap(regions); + const response = await getCurrentProvisioningState(); + if (response.status && response.status === "Creating") { + defaults.set("enableCompute", { value: true }); + defaults.set("sku", { value: response.sku, disabled: true }); + defaults.set("instances", { value: response.instances, disabled: true }); + defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) }); + defaults.set("connectionString", { + value: connectionStringValue, + hidden: true, + }); + defaults.set("metricsString", { + value: metricsStringValue, + hidden: true, + }); + } else if (response.status && response.status !== "Deleting") { + defaults.set("enableCompute", { value: true }); + defaults.set("sku", { value: response.sku, disabled: true }); + defaults.set("instances", { value: response.instances }); + defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) }); + defaults.set("connectionString", { + value: connectionStringValue, + hidden: false, + }); + defaults.set("metricsString", { + value: metricsStringValue, + hidden: false, + }); + } + + defaults.set("warningBanner", undefined); + return defaults; + }; + + @Values({ + isDynamicDescription: true, + }) + warningBanner: string; + + @Values({ + description: { + textTKey: "GraphAPIDescription", + type: DescriptionType.Text, + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", //todo + textTKey: "LearnAboutCompute", + }, + }, + }) + description: string; + + @OnChange(onEnableComputeChange) + @Values({ + labelTKey: "Compute", + trueLabelTKey: "Provisioned", + falseLabelTKey: "Deprovisioned", + }) + enableCompute: boolean; + + @OnChange(onSKUChange) + @Values({ + labelTKey: "SKUs", + choices: getSkus, + placeholderTKey: "SKUsPlaceHolder", + }) + sku: ChoiceItem; + + @OnChange(onNumberOfInstancesChange) + @PropertyInfo(NumberOfInstancesDropdownInfo) + @Values({ + labelTKey: "NumberOfInstances", + min: getInstancesMin, + max: getInstancesMax, + step: 1, + uiType: NumberUiType.Spinner, + }) + instances: number; + + @PropertyInfo(ApproximateCostDropDownInfo) + @Values({ + labelTKey: "ApproximateCost", + isDynamicDescription: true, + }) + costPerHour: string; + + @Values({ + labelTKey: "ConnectionString", + isDynamicDescription: true, + }) + connectionString: string; + + @Values({ + labelTKey: "MonitorUsage", + description: metricsStringValue, + }) + metricsString: string; +} diff --git a/src/SelfServe/GraphAPICompute/GraphAPICompute.types.ts b/src/SelfServe/GraphAPICompute/GraphAPICompute.types.ts new file mode 100644 index 000000000..76e61c435 --- /dev/null +++ b/src/SelfServe/GraphAPICompute/GraphAPICompute.types.ts @@ -0,0 +1,65 @@ +export enum Regions { + NorthCentralUS = "NorthCentralUS", + WestUS = "WestUS", + EastUS2 = "EastUS2", +} + +export interface AccountProps { + regions: Regions; + enableLogging: boolean; + accountName: string; + collectionThroughput: number; + dbThroughput: number; +} + +export type GraphAPIComputeServiceResource = { + id: string; + name: string; + type: string; + properties: GraphAPIComputeServiceProps; + locations: GraphAPIComputeServiceLocations; +}; +export type GraphAPIComputeServiceProps = { + serviceType: string; + creationTime: string; + status: string; + instanceSize: string; + instanceCount: number; + GraphAPIComputeEndPoint: string; +}; + +export type GraphAPIComputeServiceLocations = { + location: string; + status: string; + GraphAPIComputeEndpoint: string; +}; + +export type UpdateComputeRequestParameters = { + properties: UpdateComputeRequestProperties; +}; + +export type UpdateComputeRequestProperties = { + instanceSize: string; + instanceCount: number; + serviceType: string; +}; + +export type FetchPricesResponse = { + Items: Array; + NextPageLink: string | undefined; + Count: number; +}; + +export type PriceItem = { + retailPrice: number; + skuName: string; +}; + +export type RegionsResponse = { + locations: Array; + location: string; +}; + +export type RegionItem = { + locationName: string; +}; diff --git a/src/SelfServe/SelfServe.tsx b/src/SelfServe/SelfServe.tsx index 50f6eecf8..99e6e51c9 100644 --- a/src/SelfServe/SelfServe.tsx +++ b/src/SelfServe/SelfServe.tsx @@ -50,6 +50,14 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise Date: Wed, 8 Sep 2021 14:04:31 -0700 Subject: [PATCH 04/54] Sqlx approx cost bug fixes (#975) * function naming changed * bug fix: replacing multiple occurences of space correctly now --- src/SelfServe/SqlX/SqlX.rp.ts | 10 +++++----- src/SelfServe/SqlX/SqlX.tsx | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/SelfServe/SqlX/SqlX.rp.ts b/src/SelfServe/SqlX/SqlX.rp.ts index 61080763e..a7e788728 100644 --- a/src/SelfServe/SqlX/SqlX.rp.ts +++ b/src/SelfServe/SqlX/SqlX.rp.ts @@ -138,9 +138,9 @@ const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: str return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`; }; -export const getReadRegions = async (): Promise> => { +export const getRegions = async (): Promise> => { try { - const readRegions = new Array(); + const regions = new Array(); const response = await armRequestWithoutPolling({ host: configContext.ARM_ENDPOINT, @@ -150,13 +150,13 @@ export const getReadRegions = async (): Promise> => { }); if (response.result.location !== undefined) { - readRegions.push(response.result.location.replace(" ", "").toLowerCase()); + regions.push(response.result.location.split(" ").join("").toLowerCase()); } else { for (const location of response.result.locations) { - readRegions.push(location.locationName.replace(" ", "").toLowerCase()); + regions.push(location.locationName.split(" ").join("").toLowerCase()); } } - return readRegions; + return regions; } catch (err) { return new Array(); } diff --git a/src/SelfServe/SqlX/SqlX.tsx b/src/SelfServe/SqlX/SqlX.tsx index ca1177fa3..0d4c096da 100644 --- a/src/SelfServe/SqlX/SqlX.tsx +++ b/src/SelfServe/SqlX/SqlX.tsx @@ -17,7 +17,7 @@ import { deleteDedicatedGatewayResource, getCurrentProvisioningState, getPriceMap, - getReadRegions, + getRegions, refreshDedicatedGatewayProvisioning, updateDedicatedGatewayResource, } from "./SqlX.rp"; @@ -324,7 +324,7 @@ export default class SqlX extends SelfServeBaseClass { hidden: true, }); - regions = await getReadRegions(); + regions = await getRegions(); priceMap = await getPriceMap(regions); const response = await getCurrentProvisioningState(); From 7e4f030547f4d745c504573183d55da77c6cffa1 Mon Sep 17 00:00:00 2001 From: kcheekuri <88904658+kcheekuri@users.noreply.github.com> Date: Thu, 9 Sep 2021 14:02:00 -0400 Subject: [PATCH 05/54] =?UTF-8?q?Hidding=20container=20connection=20status?= =?UTF-8?q?=20behind=20the=20feature=20flag=20and=20initi=E2=80=A6=20(#101?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Hidding container connection status behind the feature flag and initializing scratch issue * maintaining connecting status UX part at notebooks context * Changing scratch name to temporary and showing only after connected --- src/Common/Constants.ts | 1 - src/Explorer/Explorer.tsx | 8 ---- .../CommandBar/CommandBarComponentAdapter.tsx | 9 +++- .../CommandBar/ConnectionStatusComponent.tsx | 13 ++--- src/Explorer/Notebook/useNotebook.ts | 10 ++-- .../CopyNotebookPaneComponent.tsx | 4 +- src/Explorer/Tree/ResourceTree.tsx | 6 +-- src/Explorer/Tree/ResourceTreeAdapter.tsx | 2 +- src/Phoenix/PhoenixClient.ts | 47 ++++++++++++------- src/Utils/GalleryUtils.test.ts | 3 +- src/Utils/GalleryUtils.ts | 4 +- 11 files changed, 59 insertions(+), 48 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 6e13f07b5..bb8a2787b 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -340,7 +340,6 @@ export enum ConflictOperationType { export enum ConnectionStatusType { Connecting = "Connecting", - Allocating = "Allocating", Connected = "Connected", Failed = "Connection Failed", } diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 2cd3e79da..55f056df7 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -4,7 +4,6 @@ import _ from "underscore"; import { AuthType } from "../AuthType"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import * as Constants from "../Common/Constants"; -import { ConnectionStatusType } from "../Common/Constants"; import { readCollection } from "../Common/dataAccess/readCollection"; import { readDatabases } from "../Common/dataAccess/readDatabases"; import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility"; @@ -347,10 +346,6 @@ export default class Explorer { } this._isInitializingNotebooks = true; if (userContext.features.phoenix) { - const connectionStatus: DataModels.ContainerConnectionInfo = { - status: ConnectionStatusType.Allocating, - }; - useNotebook.getState().setConnectionInfo(connectionStatus); const provisionData = { cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint, resourceId: userContext.databaseAccount.id, @@ -361,9 +356,6 @@ export default class Explorer { }; const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData); if (connectionInfo.data && connectionInfo.data.notebookServerUrl) { - 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, diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index 452d7e881..a6d17e96a 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -9,6 +9,7 @@ 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 { useSelectedNode } from "../../useSelectedNode"; @@ -54,7 +55,13 @@ export const CommandBar: React.FC = ({ container }: Props) => { const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor); uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); - uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus("connectionStatus")); + if ( + userContext.features.notebooksTemporarilyDown === false && + userContext.features.phoenix === true && + useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2 + ) { + uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus("connectionStatus")); + } if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) { uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker")); diff --git a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx index 7e4cfa04d..34066b201 100644 --- a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx +++ b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx @@ -1,4 +1,4 @@ -import { Icon, ProgressIndicator, Spinner, SpinnerSize, Stack, TooltipHost } from "@fluentui/react"; +import { Icon, ProgressIndicator, Stack, TooltipHost } from "@fluentui/react"; import * as React from "react"; import { ConnectionStatusType } from "../../../Common/Constants"; import { useNotebook } from "../../Notebook/useNotebook"; @@ -40,14 +40,9 @@ export const ConnectionStatus: React.FC = (): JSX.Element => { const connectionInfo = useNotebook((state) => state.connectionInfo); if (!connectionInfo) { - return ( - - Connecting - - - ); + return <>; } - if (connectionInfo && connectionInfo.status === ConnectionStatusType.Allocating && isActive === false) { + if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) { setIsActive(true); } else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) { stopTimer(); @@ -68,7 +63,7 @@ export const ConnectionStatus: React.FC = (): JSX.Element => { {connectionInfo.status} - {connectionInfo.status === ConnectionStatusType.Allocating && isActive && ( + {connectionInfo.status === ConnectionStatusType.Connecting && isActive && ( )} diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index 348f7278e..eae023783 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -29,7 +29,7 @@ interface NotebookState { gitHubNotebooksContentRoot: NotebookContentItem; galleryContentRoot: NotebookContentItem; connectionInfo: DataModels.ContainerConnectionInfo; - NotebookFolderName: string; + notebookFolderName: string; setIsNotebookEnabled: (isNotebookEnabled: boolean) => void; setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void; setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void; @@ -38,6 +38,7 @@ interface NotebookState { setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void; setIsShellEnabled: (isShellEnabled: boolean) => void; setNotebookBasePath: (notebookBasePath: string) => void; + setNotebookFolderName: (notebookFolderName: string) => void; refreshNotebooksEnabledStateForAccount: () => Promise; findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem; insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void; @@ -69,7 +70,7 @@ export const useNotebook: UseStore = create((set, get) => ({ gitHubNotebooksContentRoot: undefined, galleryContentRoot: undefined, connectionInfo: undefined, - NotebookFolderName: userContext.features.phoenix ? "My Notebooks Scratch" : "My Notebooks", + notebookFolderName: undefined, setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }), setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }), setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => @@ -80,6 +81,7 @@ export const useNotebook: UseStore = create((set, get) => ({ setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }), setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }), setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }), + setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }), refreshNotebooksEnabledStateForAccount: async (): Promise => { const { databaseAccount, authType } = userContext; if ( @@ -173,8 +175,10 @@ export const useNotebook: UseStore = create((set, get) => ({ isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); }, initializeNotebooksTree: async (notebookManager: NotebookManager): Promise => { + const notebookFolderName = userContext.features.phoenix === true ? "Temporary Notebooks" : "My Notebooks"; + set({ notebookFolderName }); const myNotebooksContentRoot = { - name: get().NotebookFolderName, + name: get().notebookFolderName, path: get().notebookBasePath, type: NotebookContentItemType.Directory, }; diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx index 66ef0d584..1aaab131a 100644 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx +++ b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx @@ -49,8 +49,8 @@ export const CopyNotebookPaneComponent: FunctionComponent const options: IDropdownOption[] = []; options.push({ key: "MyNotebooks-Item", - text: useNotebook.getState().NotebookFolderName, - title: useNotebook.getState().NotebookFolderName, + text: useNotebook.getState().notebookFolderName, + title: useNotebook.getState().notebookFolderName, data: { type: "MyNotebooks", } as Location, diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index 9f5396ed2..8d6bece87 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -11,7 +11,7 @@ import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; import PublishIcon from "../../../images/notebook/publish_content.svg"; import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; -import { Areas, Notebook } from "../../Common/Constants"; +import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; @@ -128,15 +128,13 @@ export const ResourceTree: React.FC = ({ container }: Resourc notebooksTree.children.push(buildGalleryNotebooksTree()); } - if (myNotebooksContentRoot) { + if (myNotebooksContentRoot && useNotebook.getState().connectionInfo.status == ConnectionStatusType.Connected) { notebooksTree.children.push(buildMyNotebooksTree()); } if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { // collapse all other notebook nodes notebooksTree.children.forEach((node) => (node.isExpanded = false)); notebooksTree.children.push(buildGitHubNotebooksTree(true)); - } else if (container.notebookManager && !container.notebookManager.gitHubOAuthService.isLoggedIn()) { - notebooksTree.children.push(buildGitHubNotebooksTree(false)); } } return notebooksTree; diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 7941446d3..9c5da6b19 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -132,7 +132,7 @@ export class ResourceTreeAdapter implements ReactAdapter { type: NotebookContentItemType.File, }; this.myNotebooksContentRoot = { - name: useNotebook.getState().NotebookFolderName, + name: useNotebook.getState().notebookFolderName, path: useNotebook.getState().notebookBasePath, type: NotebookContentItemType.Directory, }; diff --git a/src/Phoenix/PhoenixClient.ts b/src/Phoenix/PhoenixClient.ts index 155672cd3..aa4ed9322 100644 --- a/src/Phoenix/PhoenixClient.ts +++ b/src/Phoenix/PhoenixClient.ts @@ -1,6 +1,6 @@ import { ConnectionStatusType, HttpHeaders, HttpStatusCodes } from "../Common/Constants"; import { configContext } from "../ConfigContext"; -import * as DataModels from "../Contracts/DataModels"; +import { ContainerConnectionInfo } from "../Contracts/DataModels"; import { useNotebook } from "../Explorer/Notebook/useNotebook"; import { userContext } from "../UserContext"; import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; @@ -25,25 +25,40 @@ export class PhoenixClient { public async containerConnectionInfo( provisionData: IProvosionData ): Promise> { - const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/provision`, { - method: "POST", - headers: PhoenixClient.getHeaders(), - body: JSON.stringify(provisionData), - }); - let data: IPhoenixConnectionInfoResult; - if (response.status === HttpStatusCodes.OK) { - data = await response.json(); - } else { - const connectionStatus: DataModels.ContainerConnectionInfo = { + try { + const connectionStatus: ContainerConnectionInfo = { + status: ConnectionStatusType.Connecting, + }; + useNotebook.getState().setConnectionInfo(connectionStatus); + const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/provision`, { + method: "POST", + headers: PhoenixClient.getHeaders(), + body: JSON.stringify(provisionData), + }); + let data: IPhoenixConnectionInfoResult; + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + if (data && data.notebookServerUrl) { + connectionStatus.status = ConnectionStatusType.Connected; + useNotebook.getState().setConnectionInfo(connectionStatus); + } + } else { + connectionStatus.status = ConnectionStatusType.Failed; + useNotebook.getState().setConnectionInfo(connectionStatus); + } + + return { + status: response.status, + data, + }; + } catch (error) { + const connectionStatus: ContainerConnectionInfo = { status: ConnectionStatusType.Failed, }; useNotebook.getState().setConnectionInfo(connectionStatus); + console.error(error); + throw error; } - - return { - status: response.status, - data, - }; } public static getPhoenixEndpoint(): string { diff --git a/src/Utils/GalleryUtils.test.ts b/src/Utils/GalleryUtils.test.ts index 4df3651cd..2ba4ac7e3 100644 --- a/src/Utils/GalleryUtils.test.ts +++ b/src/Utils/GalleryUtils.test.ts @@ -2,6 +2,7 @@ import { HttpStatusCodes } from "../Common/Constants"; import { useDialog } from "../Explorer/Controls/Dialog"; import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; import Explorer from "../Explorer/Explorer"; +import { useNotebook } from "../Explorer/Notebook/useNotebook"; import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; import * as GalleryUtils from "./GalleryUtils"; @@ -34,7 +35,7 @@ describe("GalleryUtils", () => { expect(useDialog.getState().visible).toBe(true); expect(useDialog.getState().dialogProps).toBeDefined(); - expect(useDialog.getState().dialogProps.title).toBe("Download to My Notebooks"); + expect(useDialog.getState().dialogProps.title).toBe(`Download to ${useNotebook.getState().notebookFolderName}`); }); it("favoriteItem favorites item", async () => { diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index db3f081de..6ed56f05c 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -224,12 +224,12 @@ export function downloadItem( const name = data.name; useDialog.getState().showOkCancelModalDialog( - `Download to ${useNotebook.getState().NotebookFolderName}`, + `Download to ${useNotebook.getState().notebookFolderName}`, `Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`, "Download", async () => { const clearInProgressMessage = logConsoleProgress( - `Downloading ${name} to ${useNotebook.getState().NotebookFolderName}` + `Downloading ${name} to ${useNotebook.getState().notebookFolderName}` ); const startKey = traceStart(Action.NotebooksGalleryDownload, { notebookId: data.id, From d10f3c69f1ddd0975770d1537fcefd71bcfbe191 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Mon, 13 Sep 2021 16:25:21 -0400 Subject: [PATCH 06/54] MongoClient Feature Flag (#1073) Adding a feature flag for Mongo Client that allows a user to specify a mongo endpoint and an API so that users can test specific APIs locally. Example: https://localhost:1234/hostedExplorer.html?feature.mongoproxyendpoint=https://localhost:12901&feature.mongoProxyAPIs=createDocument|readDocument The above link says to test APIs createDocument and readDocument on https://localhost:12901 Co-authored-by: artrejo Co-authored-by: Asier Isayas --- src/Common/MongoProxyClient.test.ts | 31 ++++++++++++++++++++- src/Common/MongoProxyClient.ts | 23 +++++++++------ src/Platform/Hosted/extractFeatures.test.ts | 19 +++++++++++-- src/Platform/Hosted/extractFeatures.ts | 13 +++++++++ 4 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/Common/MongoProxyClient.test.ts b/src/Common/MongoProxyClient.test.ts index 3a5a02365..4d7bd9022 100644 --- a/src/Common/MongoProxyClient.test.ts +++ b/src/Common/MongoProxyClient.test.ts @@ -3,8 +3,9 @@ import { resetConfigContext, updateConfigContext } from "../ConfigContext"; import { DatabaseAccount } from "../Contracts/DataModels"; import { Collection } from "../Contracts/ViewModels"; import DocumentId from "../Explorer/Tree/DocumentId"; +import { extractFeatures } from "../Platform/Hosted/extractFeatures"; import { updateUserContext } from "../UserContext"; -import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient"; +import { deleteDocument, getEndpoint, getFeatureEndpointOrDefault, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient"; const databaseId = "testDB"; @@ -246,4 +247,32 @@ describe("MongoProxyClient", () => { expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer"); }); }); + describe("getFeatureEndpointOrDefault", () => { + beforeEach(() => { + resetConfigContext(); + updateConfigContext({ + BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", + }); + const params = new URLSearchParams({ + "feature.mongoProxyEndpoint": "https://localhost:12901", + "feature.mongoProxyAPIs": "readDocument|createDocument", + }); + const features = extractFeatures(params); + updateUserContext({ + authType: AuthType.AAD, + features: features + }); + }); + + + it("returns a local endpoint", () => { + const endpoint = getFeatureEndpointOrDefault("readDocument"); + expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer"); + }); + + it("returns a production endpoint", () => { + const endpoint = getFeatureEndpointOrDefault("deleteDocument"); + expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer"); + }); + }); }); diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 2945f1288..1869e7061 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -6,6 +6,7 @@ import * as DataModels from "../Contracts/DataModels"; import { MessageTypes } from "../Contracts/ExplorerContracts"; import { Collection } from "../Contracts/ViewModels"; import DocumentId from "../Explorer/Tree/DocumentId"; +import { hasFlag } from "../Platform/Hosted/extractFeatures"; import { userContext } from "../UserContext"; import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants"; @@ -78,7 +79,7 @@ export function queryDocuments( : "", }; - const endpoint = getEndpoint() || ""; + const endpoint = getFeatureEndpointOrDefault("resourcelist") || ""; const headers = { ...defaultHeaders, @@ -141,7 +142,8 @@ export function readDocument( : "", }; - const endpoint = getEndpoint(); + const endpoint = getFeatureEndpointOrDefault("readDocument"); + return window .fetch(`${endpoint}?${queryString.stringify(params)}`, { method: "GET", @@ -181,7 +183,7 @@ export function createDocument( pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "", }; - const endpoint = getEndpoint(); + const endpoint = getFeatureEndpointOrDefault("createDocument"); return window .fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, { @@ -225,7 +227,7 @@ export function updateDocument( ? documentId.partitionKeyProperty : "", }; - const endpoint = getEndpoint(); + const endpoint = getFeatureEndpointOrDefault("updateDocument"); return window .fetch(`${endpoint}?${queryString.stringify(params)}`, { @@ -266,7 +268,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum ? documentId.partitionKeyProperty : "", }; - const endpoint = getEndpoint(); + const endpoint = getFeatureEndpointOrDefault("deleteDocument");; return window .fetch(`${endpoint}?${queryString.stringify(params)}`, { @@ -309,7 +311,7 @@ export function createMongoCollectionWithProxy( autoPilotThroughput: params.autoPilotMaxThroughput?.toString(), }; - const endpoint = getEndpoint(); + const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy"); return window .fetch( @@ -333,8 +335,13 @@ export function createMongoCollectionWithProxy( }); } -export function getEndpoint(): string { - let url = (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT) + "/api/mongo/explorer"; +export function getFeatureEndpointOrDefault(feature: string): string { + return (hasFlag(userContext.features.mongoProxyAPIs, feature)) ? getEndpoint(userContext.features.mongoProxyEndpoint) : getEndpoint(); +} + +export function getEndpoint(customEndpoint?: string): string { + let url = customEndpoint ? customEndpoint : (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT); + url += "/api/mongo/explorer"; if (userContext.authType === AuthType.EncryptedToken) { url = url.replace("api/mongo", "api/guest/mongo"); diff --git a/src/Platform/Hosted/extractFeatures.test.ts b/src/Platform/Hosted/extractFeatures.test.ts index a44e81a5d..ac2e7afae 100644 --- a/src/Platform/Hosted/extractFeatures.test.ts +++ b/src/Platform/Hosted/extractFeatures.test.ts @@ -1,4 +1,4 @@ -import { extractFeatures } from "./extractFeatures"; +import { extractFeatures, hasFlag } from "./extractFeatures"; describe("extractFeatures", () => { it("correctly detects feature flags in a case insensitive manner", () => { @@ -14,9 +14,24 @@ describe("extractFeatures", () => { }); const features = extractFeatures(params); - expect(features.notebookServerUrl).toBe(url); expect(features.notebookServerToken).toBe(token); expect(features.enableNotebooks).toBe(notebooksEnabled); }); }); + +describe("hasFlag", () => { + it("correctly determines if value has flag", () => { + const desiredFlag = "readDocument"; + + const singleFlagValue = "readDocument"; + const multipleFlagValues = "readDocument|createDocument"; + const differentFlagValue = "createDocument"; + + expect(hasFlag(singleFlagValue, desiredFlag)).toBe(true); + expect(hasFlag(multipleFlagValues, desiredFlag)).toBe(true); + expect(hasFlag(differentFlagValue, desiredFlag)).toBe(false); + expect(hasFlag(multipleFlagValues, undefined)).toBe(false); + expect(hasFlag(undefined, desiredFlag)).toBe(false); + }); +}); diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 29c106a6e..130991301 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -29,6 +29,8 @@ export type Features = { readonly pr?: string; readonly showMinRUSurvey: boolean; readonly ttl90Days: boolean; + readonly mongoProxyEndpoint: string; + readonly mongoProxyAPIs: string; readonly notebooksTemporarilyDown: boolean; }; @@ -63,6 +65,8 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear enableKoResourceTree: "true" === get("enablekoresourcetree"), executeSproc: "true" === get("dataexplorerexecutesproc"), hostedDataExplorer: "true" === get("hosteddataexplorerenabled"), + mongoProxyEndpoint: get("mongoproxyendpoint"), + mongoProxyAPIs: get("mongoproxyapis"), junoEndpoint: get("junoendpoint"), livyEndpoint: get("livyendpoint"), notebookBasePath: get("notebookbasepath"), @@ -80,3 +84,12 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear phoenix: "true" === get("phoenix"), }; } + +export function hasFlag(flags: string, desiredFlag: string): boolean { + if (!flags || !desiredFlag) { + return false; + } + + const features = flags.split("|"); + return features.find((feature) => feature === desiredFlag) ? true : false; +} From 8866976bb42f146f71e9e0918f91f6fb0efdbab3 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Tue, 14 Sep 2021 12:28:33 -0400 Subject: [PATCH 07/54] fixed hasFlag test (#1076) Co-authored-by: Asier Isayas --- src/Platform/Hosted/extractFeatures.test.ts | 5 +++-- src/Platform/Hosted/extractFeatures.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Platform/Hosted/extractFeatures.test.ts b/src/Platform/Hosted/extractFeatures.test.ts index ac2e7afae..98fc8ebd7 100644 --- a/src/Platform/Hosted/extractFeatures.test.ts +++ b/src/Platform/Hosted/extractFeatures.test.ts @@ -31,7 +31,8 @@ describe("hasFlag", () => { expect(hasFlag(singleFlagValue, desiredFlag)).toBe(true); expect(hasFlag(multipleFlagValues, desiredFlag)).toBe(true); expect(hasFlag(differentFlagValue, desiredFlag)).toBe(false); - expect(hasFlag(multipleFlagValues, undefined)).toBe(false); - expect(hasFlag(undefined, desiredFlag)).toBe(false); + expect(hasFlag(multipleFlagValues, undefined as unknown as string)).toBe(false); + expect(hasFlag(undefined as unknown as string, desiredFlag)).toBe(false); + expect(hasFlag(undefined as unknown as string, undefined as unknown as string)).toBe(false); }); }); diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 130991301..1313aa834 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -29,8 +29,8 @@ export type Features = { readonly pr?: string; readonly showMinRUSurvey: boolean; readonly ttl90Days: boolean; - readonly mongoProxyEndpoint: string; - readonly mongoProxyAPIs: string; + readonly mongoProxyEndpoint?: string; + readonly mongoProxyAPIs?: string; readonly notebooksTemporarilyDown: boolean; }; From 2d945c82317297a618f35646aaee9cb0267dae9c Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Tue, 14 Sep 2021 12:33:09 -0400 Subject: [PATCH 08/54] allowing azure client secret to be null in dev mode (#1079) Co-authored-by: Asier Isayas --- webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index a1cb34e5a..e6bcbe1e0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -103,7 +103,7 @@ module.exports = function (_env = {}, argv = {}) { envVars.NODE_ENV = "development"; envVars.AZURE_CLIENT_ID = AZURE_CLIENT_ID; envVars.AZURE_TENANT_ID = AZURE_TENANT_ID; - envVars.AZURE_CLIENT_SECRET = AZURE_CLIENT_SECRET; + envVars.AZURE_CLIENT_SECRET = AZURE_CLIENT_SECRET || null; envVars.SUBSCRIPTION_ID = SUBSCRIPTION_ID; envVars.RESOURCE_GROUP = RESOURCE_GROUP; typescriptRule.use[0].options.compilerOptions = { target: "ES2018" }; From 665270296f6bf8de8f76a27fe5bb1ad856954624 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Wed, 15 Sep 2021 13:05:55 -0700 Subject: [PATCH 09/54] Fix throughput cost estimate in add collection panel (#1070) --- src/Common/MongoProxyClient.test.ts | 12 ++- src/Common/MongoProxyClient.ts | 8 +- .../Controls/Settings/SettingsRenderUtils.tsx | 73 +++++++------- .../CostEstimateText/CostEstimateText.tsx | 7 +- src/Platform/Hosted/extractFeatures.test.ts | 6 +- src/Shared/Constants.ts | 6 +- src/Shared/PriceEstimateCalculator.ts | 4 +- src/Utils/PricingUtils.test.ts | 99 +++++++------------ src/Utils/PricingUtils.ts | 36 +++---- 9 files changed, 117 insertions(+), 134 deletions(-) diff --git a/src/Common/MongoProxyClient.test.ts b/src/Common/MongoProxyClient.test.ts index 4d7bd9022..1c49141a0 100644 --- a/src/Common/MongoProxyClient.test.ts +++ b/src/Common/MongoProxyClient.test.ts @@ -5,7 +5,14 @@ import { Collection } from "../Contracts/ViewModels"; import DocumentId from "../Explorer/Tree/DocumentId"; import { extractFeatures } from "../Platform/Hosted/extractFeatures"; import { updateUserContext } from "../UserContext"; -import { deleteDocument, getEndpoint, getFeatureEndpointOrDefault, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient"; +import { + deleteDocument, + getEndpoint, + getFeatureEndpointOrDefault, + queryDocuments, + readDocument, + updateDocument, +} from "./MongoProxyClient"; const databaseId = "testDB"; @@ -260,11 +267,10 @@ describe("MongoProxyClient", () => { const features = extractFeatures(params); updateUserContext({ authType: AuthType.AAD, - features: features + features: features, }); }); - it("returns a local endpoint", () => { const endpoint = getFeatureEndpointOrDefault("readDocument"); expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer"); diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 1869e7061..668a0ab16 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -268,7 +268,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum ? documentId.partitionKeyProperty : "", }; - const endpoint = getFeatureEndpointOrDefault("deleteDocument");; + const endpoint = getFeatureEndpointOrDefault("deleteDocument"); return window .fetch(`${endpoint}?${queryString.stringify(params)}`, { @@ -336,11 +336,13 @@ export function createMongoCollectionWithProxy( } export function getFeatureEndpointOrDefault(feature: string): string { - return (hasFlag(userContext.features.mongoProxyAPIs, feature)) ? getEndpoint(userContext.features.mongoProxyEndpoint) : getEndpoint(); + return hasFlag(userContext.features.mongoProxyAPIs, feature) + ? getEndpoint(userContext.features.mongoProxyEndpoint) + : getEndpoint(); } export function getEndpoint(customEndpoint?: string): string { - let url = customEndpoint ? customEndpoint : (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT); + let url = customEndpoint ? customEndpoint : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT; url += "/api/mongo/explorer"; if (userContext.authType === AuthType.EncryptedToken) { diff --git a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx index c0cda4f59..fe29a6d90 100644 --- a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx @@ -1,45 +1,45 @@ -import * as React from "react"; -import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; -import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants"; -import { Urls, StyleConstants } from "../../../Common/Constants"; import { - getPriceCurrency, - getCurrencySign, - getAutoscalePricePerRu, - getMultimasterMultiplier, - computeRUUsagePriceHourly, - getPricePerRu, - estimatedCostDisclaimer, -} from "../../../Utils/PricingUtils"; -import { - ITextFieldStyles, + DetailsList, + DetailsListLayoutMode, + DetailsRow, ICheckboxStyles, - IStackProps, - IStackTokens, IChoiceGroupStyles, - Link, - Text, - IMessageBarStyles, - ITextStyles, - IDetailsRowStyles, - IStackStyles, + IColumn, + IDetailsColumnStyles, IDetailsListStyles, + IDetailsRowProps, + IDetailsRowStyles, IDropdownStyles, + IMessageBarStyles, ISeparatorStyles, + IStackProps, + IStackStyles, + IStackTokens, + ITextFieldStyles, + ITextStyles, + Link, MessageBar, MessageBarType, - Stack, + SelectionMode, Spinner, SpinnerSize, - DetailsList, - IColumn, - SelectionMode, - DetailsListLayoutMode, - IDetailsRowProps, - DetailsRow, - IDetailsColumnStyles, + Stack, + Text, } from "@fluentui/react"; -import { isDirtyTypes, isDirty } from "./SettingsUtils"; +import * as React from "react"; +import { StyleConstants, Urls } from "../../../Common/Constants"; +import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants"; +import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; +import { + computeRUUsagePriceHourly, + estimatedCostDisclaimer, + getAutoscalePricePerRu, + getCurrencySign, + getMultimasterMultiplier, + getPriceCurrency, + getPricePerRu, +} from "../../../Utils/PricingUtils"; +import { isDirty, isDirtyTypes } from "./SettingsUtils"; export interface EstimatedSpendingDisplayProps { costType: JSX.Element; @@ -223,14 +223,15 @@ export const getRuPriceBreakdown = ( multimasterEnabled: isMultimaster, isAutoscale: isAutoscale, }); - const basePricePerRu: number = isAutoscale - ? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster)) - : getPricePerRu(serverId); + const multimasterMultiplier = getMultimasterMultiplier(numberOfRegions, isMultimaster); + const pricePerRu: number = isAutoscale + ? getAutoscalePricePerRu(serverId, multimasterMultiplier) + : getPricePerRu(serverId, multimasterMultiplier); return { - hourlyPrice: hourlyPrice, + hourlyPrice, dailyPrice: hourlyPrice * 24, monthlyPrice: hourlyPrice * hoursInAMonth, - pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster), + pricePerRu, currency: getPriceCurrency(serverId), currencySign: getCurrencySign(serverId), }; diff --git a/src/Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText.tsx b/src/Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText.tsx index 51aaae619..fbc469f47 100644 --- a/src/Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText.tsx +++ b/src/Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText.tsx @@ -6,6 +6,7 @@ import { userContext } from "../../../../UserContext"; import { calculateEstimateNumber, computeRUUsagePriceHourly, + estimatedCostDisclaimer, getAutoscalePricePerRu, getCurrencySign, getMultimasterMultiplier, @@ -42,11 +43,9 @@ export const CostEstimateText: FunctionComponent = ({ const currency: string = getPriceCurrency(serverId); const currencySign: string = getCurrencySign(serverId); const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled); - const pricePerRu = isAutoscale - ? getAutoscalePricePerRu(serverId, multiplier) * multiplier - : getPricePerRu(serverId) * multiplier; + const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multiplier) : getPricePerRu(serverId, multiplier); - const iconWithEstimatedCostDisclaimer: JSX.Element = PricingUtils.estimatedCostDisclaimer; + const iconWithEstimatedCostDisclaimer: JSX.Element = {estimatedCostDisclaimer}; if (isAutoscale) { return ( diff --git a/src/Platform/Hosted/extractFeatures.test.ts b/src/Platform/Hosted/extractFeatures.test.ts index 98fc8ebd7..f99b09d89 100644 --- a/src/Platform/Hosted/extractFeatures.test.ts +++ b/src/Platform/Hosted/extractFeatures.test.ts @@ -31,8 +31,8 @@ describe("hasFlag", () => { expect(hasFlag(singleFlagValue, desiredFlag)).toBe(true); expect(hasFlag(multipleFlagValues, desiredFlag)).toBe(true); expect(hasFlag(differentFlagValue, desiredFlag)).toBe(false); - expect(hasFlag(multipleFlagValues, undefined as unknown as string)).toBe(false); - expect(hasFlag(undefined as unknown as string, desiredFlag)).toBe(false); - expect(hasFlag(undefined as unknown as string, undefined as unknown as string)).toBe(false); + expect(hasFlag(multipleFlagValues, (undefined as unknown) as string)).toBe(false); + expect(hasFlag((undefined as unknown) as string, desiredFlag)).toBe(false); + expect(hasFlag((undefined as unknown) as string, (undefined as unknown) as string)).toBe(false); }); }); diff --git a/src/Shared/Constants.ts b/src/Shared/Constants.ts index 97a9349c5..8594ccc25 100644 --- a/src/Shared/Constants.ts +++ b/src/Shared/Constants.ts @@ -125,7 +125,8 @@ export class OfferPricing { S3Price: 0.1344, Standard: { StartingPrice: 24 / hoursInAMonth, // per hour - PricePerRU: 0.00008, + SingleMasterPricePerRU: 0.00008, + MultiMasterPricePerRU: 0.00016, PricePerGB: 0.25 / hoursInAMonth, }, }, @@ -137,7 +138,8 @@ export class OfferPricing { S3Price: 0.6, Standard: { StartingPrice: OfferPricing.MonthlyPricing.mooncake.Standard.StartingPrice / hoursInAMonth, // per hour - PricePerRU: 0.00051, + SingleMasterPricePerRU: 0.00051, + MultiMasterPricePerRU: 0.00102, PricePerGB: OfferPricing.MonthlyPricing.mooncake.Standard.PricePerGB / hoursInAMonth, }, }, diff --git a/src/Shared/PriceEstimateCalculator.ts b/src/Shared/PriceEstimateCalculator.ts index a7f08d230..e6cb27db6 100644 --- a/src/Shared/PriceEstimateCalculator.ts +++ b/src/Shared/PriceEstimateCalculator.ts @@ -2,11 +2,11 @@ import * as Constants from "./Constants"; export function computeRUUsagePrice(serverId: string, requestUnits: number): string { if (serverId === "mooncake") { - const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU; + const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.SingleMasterPricePerRU; return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency; } - const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU; + const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.SingleMasterPricePerRU; return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency; } diff --git a/src/Utils/PricingUtils.test.ts b/src/Utils/PricingUtils.test.ts index de4efa90c..15a504ce2 100644 --- a/src/Utils/PricingUtils.test.ts +++ b/src/Utils/PricingUtils.test.ts @@ -150,7 +150,7 @@ describe("PricingUtils Tests", () => { expect(value).toBe(0.00012); }); - it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled", () => { + it("should return 0.00032 for default cloud, 1RU, 2 region, multimaster enabled", () => { const value = PricingUtils.computeRUUsagePriceHourly({ serverId: "default", requestUnits: 1, @@ -158,9 +158,9 @@ describe("PricingUtils Tests", () => { multimasterEnabled: true, isAutoscale: false, }); - expect(value).toBe(0.00048); + expect(value).toBe(0.00032); }); - it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled, autoscale", () => { + it("should return 0.00032 for default cloud, 1RU, 2 region, multimaster enabled, autoscale", () => { const value = PricingUtils.computeRUUsagePriceHourly({ serverId: "default", requestUnits: 1, @@ -168,7 +168,7 @@ describe("PricingUtils Tests", () => { multimasterEnabled: true, isAutoscale: true, }); - expect(value).toBe(0.00096); + expect(value).toBe(0.00032); }); }); @@ -251,70 +251,47 @@ describe("PricingUtils Tests", () => { }); describe("getPricePerRu()", () => { - it("should return 0.00008 for default clouds", () => { - const value = PricingUtils.getPricePerRu("default"); + it("should return 0.00008 for single master default clouds", () => { + const value = PricingUtils.getPricePerRu("default", 1); expect(value).toBe(0.00008); }); - it("should return 0.00051 for mooncake", () => { - const value = PricingUtils.getPricePerRu("mooncake"); + it("should return 0.00016 for multi master default clouds", () => { + const value = PricingUtils.getPricePerRu("default", 2); + expect(value).toBe(0.00016); + }); + + it("should return 0.00051 for single master mooncake", () => { + const value = PricingUtils.getPricePerRu("mooncake", 1); expect(value).toBe(0.00051); }); + + it("should return 0.00102 for multi master mooncake", () => { + const value = PricingUtils.getPricePerRu("mooncake", 2); + expect(value).toBe(0.00102); + }); }); describe("getRegionMultiplier()", () => { - describe("without multimaster", () => { - it("should return 0 for undefined", () => { - const value = PricingUtils.getRegionMultiplier(undefined, false); - expect(value).toBe(0); - }); - - it("should return 0 for -1", () => { - const value = PricingUtils.getRegionMultiplier(-1, false); - expect(value).toBe(0); - }); - - it("should return 0 for 0", () => { - const value = PricingUtils.getRegionMultiplier(0, false); - expect(value).toBe(0); - }); - - it("should return 1 for 1", () => { - const value = PricingUtils.getRegionMultiplier(1, false); - expect(value).toBe(1); - }); - - it("should return 2 for 2", () => { - const value = PricingUtils.getRegionMultiplier(2, false); - expect(value).toBe(2); - }); + it("should return 0 for undefined", () => { + const value = PricingUtils.getRegionMultiplier(undefined); + expect(value).toBe(0); }); - - describe("with multimaster", () => { - it("should return 0 for undefined", () => { - const value = PricingUtils.getRegionMultiplier(undefined, true); - expect(value).toBe(0); - }); - - it("should return 0 for -1", () => { - const value = PricingUtils.getRegionMultiplier(-1, true); - expect(value).toBe(0); - }); - - it("should return 0 for 0", () => { - const value = PricingUtils.getRegionMultiplier(0, true); - expect(value).toBe(0); - }); - - it("should return 1 for 1", () => { - const value = PricingUtils.getRegionMultiplier(1, true); - expect(value).toBe(1); - }); - - it("should return 3 for 2", () => { - const value = PricingUtils.getRegionMultiplier(2, true); - expect(value).toBe(3); - }); + it("should return 0 for -1", () => { + const value = PricingUtils.getRegionMultiplier(-1); + expect(value).toBe(0); + }); + it("should return 0 for 0", () => { + const value = PricingUtils.getRegionMultiplier(0); + expect(value).toBe(0); + }); + it("should return 1 for 1", () => { + const value = PricingUtils.getRegionMultiplier(1); + expect(value).toBe(1); + }); + it("should return 2 for 2", () => { + const value = PricingUtils.getRegionMultiplier(2); + expect(value).toBe(2); }); }); @@ -376,7 +353,7 @@ describe("PricingUtils Tests", () => { true /* multimaster */ ); expect(value).toBe( - "Cost (USD): $0.19 hourly / $4.61 daily / $140.16 monthly (2 regions, 400RU/s, $0.00016/RU)

*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account

" + "Cost (USD): $0.13 hourly / $3.07 daily / $93.44 monthly (2 regions, 400RU/s, $0.00016/RU)

*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account

" ); }); @@ -424,7 +401,7 @@ describe("PricingUtils Tests", () => { true /* multimaster */, false ); - expect(value).toBe("I acknowledge the estimated $4.61 daily cost for the throughput above."); + expect(value).toBe("I acknowledge the estimated $3.07 daily cost for the throughput above."); }); it("should return 'I acknowledge the estimated $1.54 daily cost for the throughput above.' for 400RU/s on default cloud, 2 region, without multimaster", () => { diff --git a/src/Utils/PricingUtils.ts b/src/Utils/PricingUtils.ts index 09b16373e..d4f8f4643 100644 --- a/src/Utils/PricingUtils.ts +++ b/src/Utils/PricingUtils.ts @@ -34,26 +34,18 @@ export function getRuToolTipText(): string { * Otherwise, return numberOfRegions * @param numberOfRegions */ -export function getRegionMultiplier(numberOfRegions: number, multimasterEnabled: boolean): number { +export function getRegionMultiplier(numberOfRegions: number): number { const normalizedNumberOfRegions: number = normalizeNumber(numberOfRegions); if (normalizedNumberOfRegions <= 0) { return 0; } - if (numberOfRegions === 1) { - return numberOfRegions; - } - - if (multimasterEnabled) { - return numberOfRegions + 1; - } - return numberOfRegions; } export function getMultimasterMultiplier(numberOfRegions: number, multimasterEnabled: boolean): number { - const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled); + const regionMultiplier: number = getRegionMultiplier(numberOfRegions); const multimasterMultiplier: number = !multimasterEnabled ? 1 : regionMultiplier > 1 ? 2 : 1; return multimasterMultiplier; @@ -66,10 +58,12 @@ export function computeRUUsagePriceHourly({ multimasterEnabled, isAutoscale, }: ComputeRUUsagePriceHourlyArgs): number { - const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled); + const regionMultiplier: number = getRegionMultiplier(numberOfRegions); const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled); - const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multimasterMultiplier) : getPricePerRu(serverId); - const ruCharge = requestUnits * pricePerRu * multimasterMultiplier * regionMultiplier; + const pricePerRu = isAutoscale + ? getAutoscalePricePerRu(serverId, multimasterMultiplier) + : getPricePerRu(serverId, multimasterMultiplier); + const ruCharge = requestUnits * pricePerRu * regionMultiplier; return Number(ruCharge.toFixed(5)); } @@ -149,12 +143,16 @@ export function getAutoscalePricePerRu(serverId: string, mmMultiplier: number): } } -export function getPricePerRu(serverId: string): number { +export function getPricePerRu(serverId: string, mmMultiplier: number): number { if (serverId === "mooncake") { - return Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU; + return mmMultiplier > 1 + ? Constants.OfferPricing.HourlyPricing.mooncake.Standard.MultiMasterPricePerRU + : Constants.OfferPricing.HourlyPricing.mooncake.Standard.SingleMasterPricePerRU; } - return Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU; + return mmMultiplier > 1 + ? Constants.OfferPricing.HourlyPricing.default.Standard.MultiMasterPricePerRU + : Constants.OfferPricing.HourlyPricing.default.Standard.SingleMasterPricePerRU; } export function getAutoPilotV3SpendHtml(maxAutoPilotThroughputSet: number, isDatabaseThroughput: boolean): string { @@ -188,9 +186,7 @@ export function getEstimatedAutoscaleSpendHtml( const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth; const currency: string = getPriceCurrency(serverId); const currencySign: string = getCurrencySign(serverId); - const pricePerRu = - getAutoscalePricePerRu(serverId, getMultimasterMultiplier(regions, multimaster)) * - getMultimasterMultiplier(regions, multimaster); + const pricePerRu = getAutoscalePricePerRu(serverId, getMultimasterMultiplier(regions, multimaster)); return ( `Estimated monthly cost (${currency}): ` + @@ -219,7 +215,7 @@ export function getEstimatedSpendHtml( const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth; const currency: string = getPriceCurrency(serverId); const currencySign: string = getCurrencySign(serverId); - const pricePerRu = getPricePerRu(serverId) * getMultimasterMultiplier(regions, multimaster); + const pricePerRu = getPricePerRu(serverId, getMultimasterMultiplier(regions, multimaster)); return ( `Cost (${currency}): ` + From af0dc3094b876fd3466a7fd235f3b2d41c035c6b Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Wed, 15 Sep 2021 16:38:51 -0700 Subject: [PATCH 10/54] Temporarily lower test coverage threshold (#1084) --- jest.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jest.config.js b/jest.config.js index 889b544e0..57ec3f489 100644 --- a/jest.config.js +++ b/jest.config.js @@ -37,8 +37,8 @@ module.exports = { global: { branches: 25, functions: 25, - lines: 30, - statements: 30, + lines: 29.5, + statements: 29.5, }, }, From d7997d716ee068e6f3a47057266f411dd9c40666 Mon Sep 17 00:00:00 2001 From: Karthik chakravarthy <88904658+kcheekuri@users.noreply.github.com> Date: Wed, 15 Sep 2021 19:50:36 -0400 Subject: [PATCH 11/54] Data pane expand issue (#1085) * Data pane expand issue * Data pane expand issue-1 * Data pane expand issue format * Data pane expand issue formating --- src/Explorer/Notebook/useNotebook.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index eae023783..2f4b086b1 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -187,11 +187,14 @@ export const useNotebook: UseStore = create((set, get) => ({ path: "Gallery", type: NotebookContentItemType.File, }; - const gitHubNotebooksContentRoot = { - name: "GitHub repos", - path: "PsuedoDir", - type: NotebookContentItemType.Directory, - }; + const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn() + ? { + name: "GitHub repos", + path: "PsuedoDir", + type: NotebookContentItemType.Directory, + } + : undefined; + set({ myNotebooksContentRoot, galleryContentRoot, From ae9c27795e54cd1fa199ca89d3933ff9679d681c Mon Sep 17 00:00:00 2001 From: vaidankarswapnil <81285216+vaidankarswapnil@users.noreply.github.com> Date: Fri, 17 Sep 2021 02:51:19 +0530 Subject: [PATCH 12/54] Fix execute query keyboard focus moves to hidden element under 'Results' section of executed Query 1 blade (#1082) * fix a11y quertTab results section hidden element focus issue * Removed commented code * Resolved lint issues --- .../Tabs/QueryTab/QueryTabComponent.tsx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index 4f9a63dcf..9577e2f9f 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -1,4 +1,4 @@ -import { DetailsList, DetailsListLayoutMode, IColumn, Pivot, PivotItem, SelectionMode } from "@fluentui/react"; +import { DetailsList, DetailsListLayoutMode, IColumn, Pivot, PivotItem, SelectionMode, Text } from "@fluentui/react"; import React, { Fragment } from "react"; import SplitterLayout from "react-splitter-layout"; import "react-splitter-layout/lib/index.css"; @@ -120,21 +120,13 @@ export default class QueryTabComponent extends React.Component{`${item.toolTip}`}; + return ( + <> + {`${item.toolTip}`} + {`${item.metric}`} + + ); } else { return undefined; } From e10240bd7ac76239206fb8daf31d3a3b29507080 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Yadav <79906609+sunilyadav840@users.noreply.github.com> Date: Fri, 17 Sep 2021 02:52:47 +0530 Subject: [PATCH 13/54] fixed setting keyboard accessibility issue (#1081) --- src/Common/Tooltip/InfoTooltip.tsx | 2 +- .../__snapshots__/ThroughputInput.test.tsx.snap | 6 ++++++ src/Explorer/Panes/SettingsPane/SettingsPane.tsx | 1 - .../SettingsPane/__snapshots__/SettingsPane.test.tsx.snap | 1 - 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Common/Tooltip/InfoTooltip.tsx b/src/Common/Tooltip/InfoTooltip.tsx index 480aa9020..3ce33ca93 100644 --- a/src/Common/Tooltip/InfoTooltip.tsx +++ b/src/Common/Tooltip/InfoTooltip.tsx @@ -9,7 +9,7 @@ export const InfoTooltip: React.FunctionComponent = ({ children }: return ( - + ); diff --git a/src/Explorer/Controls/ThroughputInput/__snapshots__/ThroughputInput.test.tsx.snap b/src/Explorer/Controls/ThroughputInput/__snapshots__/ThroughputInput.test.tsx.snap index d295a7574..4a928aae8 100644 --- a/src/Explorer/Controls/ThroughputInput/__snapshots__/ThroughputInput.test.tsx.snap +++ b/src/Explorer/Controls/ThroughputInput/__snapshots__/ThroughputInput.test.tsx.snap @@ -345,12 +345,14 @@ exports[`ThroughputInput Pane should render Default properly 1`] = ` ariaLabel="Info" className="panelInfoIcon" iconName="Info" + tabIndex={0} >  @@ -1327,12 +1330,14 @@ exports[`ThroughputInput Pane should render Default properly 1`] = ` ariaLabel="Info" className="panelInfoIcon" iconName="Info" + tabIndex={0} >  diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index bec1200f7..db0746276 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -195,7 +195,6 @@ export const SettingsPane: FunctionComponent = () => { step={1} className="textfontclr" role="textbox" - tabIndex={0} id="max-degree" value={"" + maxDegreeOfParallelism} onIncrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)} diff --git a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap index fdf0642f7..ccc2ad3a7 100644 --- a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap +++ b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap @@ -123,7 +123,6 @@ exports[`Settings Pane should render Default properly 1`] = ` onValidate={[Function]} role="textbox" step={1} - tabIndex={0} value="6" /> From 8b30af3d9e94b74f04a0a0ea82e0771dfb9223a7 Mon Sep 17 00:00:00 2001 From: Hardikkumar Nai <80053762+hardiknai-techm@users.noreply.github.com> Date: Fri, 17 Sep 2021 02:53:03 +0530 Subject: [PATCH 14/54] Settings: At 200% resize mode controls present under 'Settings' blade are not visible while navigating over them. (#1075) --- src/Explorer/Panes/PanelComponent.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Explorer/Panes/PanelComponent.less b/src/Explorer/Panes/PanelComponent.less index acf5d3d2c..775f5eaaa 100644 --- a/src/Explorer/Panes/PanelComponent.less +++ b/src/Explorer/Panes/PanelComponent.less @@ -3,7 +3,7 @@ .panelFormWrapper { display: flex; flex-direction: column; - height: 100%; + min-height: 100%; .panelMainContent { flex-grow: 1; From 3032f689b6f4c97cb20eba6738eec4be7a7febad Mon Sep 17 00:00:00 2001 From: vaidankarswapnil <81285216+vaidankarswapnil@users.noreply.github.com> Date: Fri, 17 Sep 2021 02:53:29 +0530 Subject: [PATCH 15/54] Fix delete container and database labels appearing text are not associated with the edit fields (#1072) * Fix a11y issues for delete container and database * Update test snapshot issues --- .../DeleteCollectionConfirmationPane.tsx | 4 ++++ .../DeleteCollectionConfirmationPane.test.tsx.snap | 3 +++ src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx | 5 ++++- .../DeleteDatabaseConfirmationPanel.test.tsx.snap | 6 ++++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx index b09ac3eae..858ba3bc4 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx @@ -108,6 +108,8 @@ export const DeleteCollectionConfirmationPane: FunctionComponent
@@ -123,6 +125,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent { setInputCollectionName(newInput); }} + ariaLabel={confirmContainer} />
{shouldRecordFeedback() && ( @@ -142,6 +145,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent { setDeleteCollectionFeedback(newInput); }} + ariaLabel={reasonInfo} /> )} diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap index 923557265..de359f986 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap @@ -40,6 +40,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect {!formError && } @@ -133,6 +134,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent { setDatabaseInput(newInput); }} + ariaLabel={confirmDatabase} /> {isLastNonEmptyDatabase() && ( @@ -151,6 +153,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent { setDatabaseFeedbackInput(newInput); }} + ariaLabel={reasonInfo} /> )} diff --git a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap index ca0868a35..9a2b6ef16 100644 --- a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap @@ -363,6 +363,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `