From 48eeb8419d5a842ee43fe1ec739633f6ce732d02 Mon Sep 17 00:00:00 2001 From: fnbalaji <75445927+fnbalaji@users.noreply.github.com> Date: Mon, 17 May 2021 22:15:26 -0700 Subject: [PATCH 1/6] Portal changes for DedicatedGateway (#742) * src/SelfServe/Example/SelfServeExample.rp.ts. Portal changes for DedicatedGateway 1. Change Sqlx endpoints to SqlDedicatedGateway endpoint 2. Remove D32s from the SKU list 3. Add telemetry 4. Remove SKU details field per discussion 5. Support dynamic instance scaling. * format files to ensure format check and lint tests pass * Lint fixes * Lint fixes * Added metrics blade link * updated conditions for warning banner * fixed lint error * Incorporate metrics link and CR feedback * Lint fixes * CR feedback and fix links * CR feedback and fix links * Link fix Co-authored-by: Srinath Narayanan --- src/Localization/en/SqlX.json | 9 ++- src/SelfServe/SqlX/SqlX.rp.ts | 83 ++++++++++++++------- src/SelfServe/SqlX/SqlX.tsx | 135 +++++++++++++++++++++------------- 3 files changed, 147 insertions(+), 80 deletions(-) diff --git a/src/Localization/en/SqlX.json b/src/Localization/en/SqlX.json index 3c1d81dac..e2128a40e 100644 --- a/src/Localization/en/SqlX.json +++ b/src/Localization/en/SqlX.json @@ -37,16 +37,19 @@ "CannotSave": "Cannot save the changes to the Dedicated gateway resource at the moment.", "DedicatedGatewayEndpoint": "Dedicated gatewayEndpoint", "NoValue": "", - "SKUDetails": "SKU Details:", "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", - "CosmosD32Details": "General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory", "Cost": "Cost", "CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.", "ConnectionString": "Connection String", "ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ", - "KeysBlade": "the keys blade", + "KeysBlade": "the keys blade.", + "MetricsString": "Metrics", + "MetricsText": "Monitor the \"DedicatedGatewayMaximumCpuUsage\" and \"DedicatedGatewayAverageMemoryUsage\" in ", + "MetricsBlade": "the metrics blade.", + "ResizingDecisionText": "To understand if the dedicated gateway is the right size, ", + "ResizingDecisionLink": "learn more about dedicated gateway sizing.", "WarningBannerOnUpdate": "Adding or modifying dedicated gateway instances may affect your bill.", "WarningBannerOnDelete": "After deprovisioning the dedicated gateway, you must update any applications using the old dedicated gateway connection string." } \ No newline at end of file diff --git a/src/SelfServe/SqlX/SqlX.rp.ts b/src/SelfServe/SqlX/SqlX.rp.ts index b5a097fca..2bc7a70fa 100644 --- a/src/SelfServe/SqlX/SqlX.rp.ts +++ b/src/SelfServe/SqlX/SqlX.rp.ts @@ -1,10 +1,12 @@ -import { RefreshResult } from "../SelfServeTypes"; +import { configContext } from "../../ConfigContext"; import { userContext } from "../../UserContext"; import { armRequestWithoutPolling } from "../../Utils/arm/request"; -import { configContext } from "../../ConfigContext"; +import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor"; +import { RefreshResult } from "../SelfServeTypes"; +import SqlX from "./SqlX"; import { SqlxServiceResource, UpdateDedicatedGatewayRequestParameters } from "./SqlxTypes"; -const apiVersion = "2020-06-01-preview"; +const apiVersion = "2021-04-01-preview"; export enum ResourceStatus { Running = "Running", @@ -21,7 +23,7 @@ export interface DedicatedGatewayResponse { } export const getPath = (subscriptionId: string, resourceGroup: string, name: string): string => { - return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}/services/sqlx`; + return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}/services/SqlDedicatedGateway`; }; export const updateDedicatedGatewayResource = async (sku: string, instances: number): Promise => { @@ -30,39 +32,66 @@ export const updateDedicatedGatewayResource = async (sku: string, instances: num properties: { instanceSize: sku, instanceCount: instances, - serviceType: "Sqlx", + serviceType: "SqlDedicatedGateway", }, }; - const armRequestResult = await armRequestWithoutPolling({ - host: configContext.ARM_ENDPOINT, - path, - method: "PUT", - apiVersion, - body, - }); - return armRequestResult.operationStatusUrl; + const telemetryData = { ...body, httpMethod: "PUT", selfServeClassName: SqlX.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: SqlX.name }; + selfServeTraceFailure(failureTelemetry, updateTimeStamp); + } + return armRequestResult?.operationStatusUrl; }; export const deleteDedicatedGatewayResource = async (): Promise => { const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); - const armRequestResult = await armRequestWithoutPolling({ - host: configContext.ARM_ENDPOINT, - path, - method: "DELETE", - apiVersion, - }); - return armRequestResult.operationStatusUrl; + const telemetryData = { httpMethod: "DELETE", selfServeClassName: SqlX.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: SqlX.name }; + selfServeTraceFailure(failureTelemetry, deleteTimeStamp); + } + return armRequestResult?.operationStatusUrl; }; export const getDedicatedGatewayResource = async (): Promise => { const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); - const armRequestResult = await armRequestWithoutPolling({ - host: configContext.ARM_ENDPOINT, - path, - method: "GET", - apiVersion, - }); - return armRequestResult.result; + const telemetryData = { httpMethod: "GET", selfServeClassName: SqlX.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: SqlX.name }; + selfServeTraceFailure(failureTelemetry, getResourceTimeStamp); + } + return armRequestResult?.result; }; export const getCurrentProvisioningState = async (): Promise => { diff --git a/src/SelfServe/SqlX/SqlX.tsx b/src/SelfServe/SqlX/SqlX.tsx index a532c5244..44356969a 100644 --- a/src/SelfServe/SqlX/SqlX.tsx +++ b/src/SelfServe/SqlX/SqlX.tsx @@ -23,7 +23,7 @@ const costPerHourValue: Description = { textTKey: "CostText", type: DescriptionType.Text, link: { - href: "https://azure.microsoft.com/en-us/pricing/details/cosmos-db/", + href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", textTKey: "DedicatedGatewayPricing", }, }; @@ -37,43 +37,56 @@ const connectionStringValue: Description = { }, }; +const metricsStringValue: Description = { + textTKey: "MetricsText", + type: DescriptionType.Text, + link: { + href: generateBladeLink(BladeType.Metrics), + textTKey: "MetricsBlade", + }, +}; + +const resizingDecisionValue: Description = { + textTKey: "ResizingDecisionText", + type: DescriptionType.Text, + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-size", + textTKey: "ResizingDecisionLink", + }, +}; + const CosmosD4s = "Cosmos.D4s"; const CosmosD8s = "Cosmos.D8s"; const CosmosD16s = "Cosmos.D16s"; -const CosmosD32s = "Cosmos.D32s"; - -const getSKUDetails = (sku: string): string => { - if (sku === CosmosD4s) { - return "CosmosD4Details"; - } else if (sku === CosmosD8s) { - return "CosmosD8Details"; - } else if (sku === CosmosD16s) { - return "CosmosD16Details"; - } else if (sku === CosmosD32s) { - return "CosmosD32Details"; - } - return "Not Supported Yet"; -}; const onSKUChange = (newValue: InputType, currentValues: Map): Map => { currentValues.set("sku", { value: newValue }); - currentValues.set("skuDetails", { - value: { textTKey: getSKUDetails(`${newValue.toString()}`), type: DescriptionType.Text } as Description, - }); currentValues.set("costPerHour", { value: costPerHourValue }); return currentValues; }; const onNumberOfInstancesChange = ( newValue: InputType, - currentValues: Map + currentValues: Map, + baselineValues: Map ): Map => { currentValues.set("instances", { value: newValue }); - currentValues.set("warningBanner", { - value: { textTKey: "WarningBannerOnUpdate" } as Description, - hidden: false, - }); - + const dedicatedGatewayOriginallyEnabled = baselineValues.get("enableDedicatedGateway")?.value as boolean; + const baselineInstances = baselineValues.get("instances")?.value as number; + if (!dedicatedGatewayOriginallyEnabled || baselineInstances !== newValue) { + currentValues.set("warningBanner", { + value: { + textTKey: "WarningBannerOnUpdate", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", + textTKey: "DedicatedGatewayPricing", + }, + } as Description, + hidden: false, + }); + } else { + currentValues.set("warningBanner", undefined); + } return currentValues; }; @@ -87,10 +100,11 @@ const onEnableDedicatedGatewayChange = ( if (dedicatedGatewayOriginallyEnabled === newValue) { currentValues.set("sku", baselineValues.get("sku")); currentValues.set("instances", baselineValues.get("instances")); - currentValues.set("skuDetails", baselineValues.get("skuDetails")); currentValues.set("costPerHour", baselineValues.get("costPerHour")); currentValues.set("warningBanner", baselineValues.get("warningBanner")); currentValues.set("connectionString", baselineValues.get("connectionString")); + currentValues.set("metricsString", baselineValues.get("metricsString")); + currentValues.set("resizingDecisionString", baselineValues.get("resizingDecisionString")); return currentValues; } @@ -100,7 +114,7 @@ const onEnableDedicatedGatewayChange = ( value: { textTKey: "WarningBannerOnUpdate", link: { - href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", + href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", textTKey: "DedicatedGatewayPricing", }, } as Description, @@ -111,7 +125,7 @@ const onEnableDedicatedGatewayChange = ( value: { textTKey: "WarningBannerOnDelete", link: { - href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", + href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", textTKey: "DeprovisioningDetailsText", }, } as Description, @@ -132,18 +146,22 @@ const onEnableDedicatedGatewayChange = ( disabled: dedicatedGatewayOriginallyEnabled, }); - currentValues.set("skuDetails", { - value: { textTKey: getSKUDetails(`${currentValues.get("sku").value}`), type: DescriptionType.Text } as Description, - hidden: hideAttributes, - disabled: dedicatedGatewayOriginallyEnabled, - }); - currentValues.set("costPerHour", { value: costPerHourValue, hidden: hideAttributes }); currentValues.set("connectionString", { value: connectionStringValue, hidden: !newValue || !dedicatedGatewayOriginallyEnabled, }); + currentValues.set("metricsString", { + value: metricsStringValue, + hidden: !newValue || !dedicatedGatewayOriginallyEnabled, + }); + + currentValues.set("resizingDecisionString", { + value: resizingDecisionValue, + hidden: !newValue || !dedicatedGatewayOriginallyEnabled, + }); + return currentValues; }; @@ -151,7 +169,6 @@ const skuDropDownItems: ChoiceItem[] = [ { labelTKey: "CosmosD4s", key: CosmosD4s }, { labelTKey: "CosmosD8s", key: CosmosD8s }, { labelTKey: "CosmosD16s", key: CosmosD16s }, - { labelTKey: "CosmosD32s", key: CosmosD32s }, ]; const getSkus = async (): Promise => { @@ -184,7 +201,6 @@ export default class SqlX extends SelfServeBaseClass { currentValues.set("warningBanner", undefined); - //TODO : Add try catch for each RP call and return relevant notifications if (dedicatedGatewayOriginallyEnabled) { if (!dedicatedGatewayCurrentlyEnabled) { const operationStatusUrl = await deleteDedicatedGatewayResource(); @@ -206,9 +222,11 @@ export default class SqlX extends SelfServeBaseClass { }, }; } else { - // Check for scaling up/down/in/out + const sku = currentValues.get("sku")?.value as string; + const instances = currentValues.get("instances").value as number; + const operationStatusUrl = await updateDedicatedGatewayResource(sku, instances); return { - operationStatusUrl: undefined, + operationStatusUrl: operationStatusUrl, portalNotification: { initialize: { titleTKey: "UpdateInitializeTitle", @@ -255,24 +273,37 @@ export default class SqlX extends SelfServeBaseClass { defaults.set("enableDedicatedGateway", { value: false }); defaults.set("sku", { value: CosmosD4s, hidden: true }); defaults.set("instances", { value: await getInstancesMin(), hidden: true }); - defaults.set("skuDetails", undefined); defaults.set("costPerHour", undefined); defaults.set("connectionString", undefined); + defaults.set("metricsString", { + value: undefined, + hidden: true, + }); + defaults.set("resizingDecisionString", { + value: undefined, + hidden: true, + }); const response = await getCurrentProvisioningState(); if (response.status && response.status !== "Deleting") { defaults.set("enableDedicatedGateway", { value: true }); defaults.set("sku", { value: response.sku, disabled: true }); - defaults.set("instances", { value: response.instances, disabled: true }); + defaults.set("instances", { value: response.instances, disabled: false }); defaults.set("costPerHour", { value: costPerHourValue }); - defaults.set("skuDetails", { - value: { textTKey: getSKUDetails(`${defaults.get("sku").value}`), type: DescriptionType.Text } as Description, - hidden: false, - }); defaults.set("connectionString", { value: connectionStringValue, hidden: false, }); + + defaults.set("metricsString", { + value: metricsStringValue, + hidden: false, + }); + + defaults.set("resizingDecisionString", { + value: resizingDecisionValue, + hidden: false, + }); } defaults.set("warningBanner", undefined); @@ -289,7 +320,7 @@ export default class SqlX extends SelfServeBaseClass { textTKey: "DedicatedGatewayDescription", type: DescriptionType.Text, link: { - href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", + href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", textTKey: "LearnAboutDedicatedGateway", }, }, @@ -312,12 +343,6 @@ export default class SqlX extends SelfServeBaseClass { }) sku: ChoiceItem; - @Values({ - labelTKey: "SKUDetails", - isDynamicDescription: true, - }) - skuDetails: string; - @OnChange(onNumberOfInstancesChange) @Values({ labelTKey: "NumberOfInstances", @@ -328,6 +353,16 @@ export default class SqlX extends SelfServeBaseClass { }) instances: number; + @Values({ + description: metricsStringValue, + }) + metricsString: string; + + @Values({ + description: resizingDecisionValue, + }) + resizingDecisionString: string; + @Values({ labelTKey: "Cost", isDynamicDescription: true, From 2bc298fef12264afa1b34b464d84f5fc521aae63 Mon Sep 17 00:00:00 2001 From: Zachary Foster Date: Tue, 18 May 2021 18:59:09 -0400 Subject: [PATCH 2/6] [Hosted] AAD implementation for item operations (#643) --- src/Common/CosmosClient.ts | 9 ++++++++- src/Common/dataAccess/createDocument.ts | 2 +- src/Common/dataAccess/queryDocuments.ts | 2 +- src/Common/dataAccess/queryDocumentsPage.ts | 4 ++-- src/Contracts/ExplorerContracts.ts | 2 +- src/HostedExplorer.tsx | 21 +++++++++++---------- src/HostedExplorerChildFrame.ts | 1 + src/Platform/Hosted/extractFeatures.ts | 2 ++ src/Terminal/index.ts | 10 +++++----- src/UserContext.ts | 1 + src/hooks/useAADAuth.ts | 9 ++++++++- src/hooks/useKnockoutExplorer.ts | 1 + 12 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index 9ba100b3b..756c53a2e 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -10,6 +10,13 @@ const _global = typeof self === "undefined" ? window : self; export const tokenProvider = async (requestInfo: RequestInfo) => { const { verb, resourceId, resourceType, headers } = requestInfo; + + if (userContext.features.enableAadDataPlane && userContext.aadToken) { + const AUTH_PREFIX = `type=aad&ver=1.0&sig=`; + const authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`; + return authorizationToken; + } + if (configContext.platform === Platform.Emulator) { // TODO This SDK method mutates the headers object. Find a better one or fix the SDK. await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey); @@ -76,7 +83,7 @@ export function client(): Cosmos.CosmosClient { if (_client) return _client; const options: Cosmos.CosmosClientOptions = { endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called - key: userContext.masterKey, + ...(!userContext.features.enableAadDataPlane && { key: userContext.masterKey }), tokenProvider, connectionPolicy: { enableEndpointDiscovery: false, diff --git a/src/Common/dataAccess/createDocument.ts b/src/Common/dataAccess/createDocument.ts index b64f70ff9..94dde951d 100644 --- a/src/Common/dataAccess/createDocument.ts +++ b/src/Common/dataAccess/createDocument.ts @@ -1,8 +1,8 @@ import { CollectionBase } from "../../Contracts/ViewModels"; +import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { getEntityName } from "../DocumentUtility"; import { handleError } from "../ErrorHandlingUtils"; -import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; export const createDocument = async (collection: CollectionBase, newDocument: unknown): Promise => { const entityName = getEntityName(); diff --git a/src/Common/dataAccess/queryDocuments.ts b/src/Common/dataAccess/queryDocuments.ts index 16b2fb39e..c24e22ff6 100644 --- a/src/Common/dataAccess/queryDocuments.ts +++ b/src/Common/dataAccess/queryDocuments.ts @@ -1,6 +1,6 @@ -import { Queries } from "../Constants"; import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; +import { Queries } from "../Constants"; import { client } from "../CosmosClient"; export const queryDocuments = ( diff --git a/src/Common/dataAccess/queryDocumentsPage.ts b/src/Common/dataAccess/queryDocumentsPage.ts index 064e4126f..e8b5447ef 100644 --- a/src/Common/dataAccess/queryDocumentsPage.ts +++ b/src/Common/dataAccess/queryDocumentsPage.ts @@ -1,8 +1,8 @@ import { QueryResults } from "../../Contracts/ViewModels"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; -import { MinimalQueryIterator, nextPage } from "../IteratorUtilities"; -import { handleError } from "../ErrorHandlingUtils"; import { getEntityName } from "../DocumentUtility"; +import { handleError } from "../ErrorHandlingUtils"; +import { MinimalQueryIterator, nextPage } from "../IteratorUtilities"; export const queryDocumentsPage = async ( resourceName: string, diff --git a/src/Contracts/ExplorerContracts.ts b/src/Contracts/ExplorerContracts.ts index 1689a96b5..d1c3dba58 100644 --- a/src/Contracts/ExplorerContracts.ts +++ b/src/Contracts/ExplorerContracts.ts @@ -1,6 +1,6 @@ -import * as Versions from "./Versions"; import * as ActionContracts from "./ActionContracts"; import * as Diagnostics from "./Diagnostics"; +import * as Versions from "./Versions"; /** * Messaging types used with Data Explorer <-> Portal communication diff --git a/src/HostedExplorer.tsx b/src/HostedExplorer.tsx index 9f73ecdc4..4751ea5fa 100644 --- a/src/HostedExplorer.tsx +++ b/src/HostedExplorer.tsx @@ -5,20 +5,20 @@ import { render } from "react-dom"; import ChevronRight from "../images/chevron-right.svg"; import "../less/hostedexplorer.less"; import { AuthType } from "./AuthType"; -import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer"; import { DatabaseAccount } from "./Contracts/DataModels"; -import { DirectoryPickerPanel } from "./Platform/Hosted/Components/DirectoryPickerPanel"; -import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher"; import "./Explorer/Menus/NavBar/MeControlComponent.less"; -import { useTokenMetadata } from "./hooks/usePortalAccessToken"; -import { MeControl } from "./Platform/Hosted/Components/MeControl"; -import "./Platform/Hosted/ConnectScreen.less"; -import "./Shared/appInsights"; -import { SignInButton } from "./Platform/Hosted/Components/SignInButton"; import { useAADAuth } from "./hooks/useAADAuth"; -import { FeedbackCommandButton } from "./Platform/Hosted/Components/FeedbackCommandButton"; +import { useTokenMetadata } from "./hooks/usePortalAccessToken"; import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame"; +import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher"; +import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer"; +import { DirectoryPickerPanel } from "./Platform/Hosted/Components/DirectoryPickerPanel"; +import { FeedbackCommandButton } from "./Platform/Hosted/Components/FeedbackCommandButton"; +import { MeControl } from "./Platform/Hosted/Components/MeControl"; +import { SignInButton } from "./Platform/Hosted/Components/SignInButton"; +import "./Platform/Hosted/ConnectScreen.less"; import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils"; +import "./Shared/appInsights"; initializeIcons(); @@ -31,7 +31,7 @@ const App: React.FunctionComponent = () => { // For showing/hiding panel const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false); - const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant } = useAADAuth(); + const { isLoggedIn, armToken, graphToken, aadToken, account, tenantId, logout, login, switchTenant } = useAADAuth(); const [databaseAccount, setDatabaseAccount] = React.useState(); const [authType, setAuthType] = React.useState(encryptedToken ? AuthType.EncryptedToken : undefined); const [connectionString, setConnectionString] = React.useState(); @@ -50,6 +50,7 @@ const App: React.FunctionComponent = () => { authType: AuthType.AAD, databaseAccount, authorizationToken: armToken, + aadToken, }; } else if (authType === AuthType.EncryptedToken) { frameWindow.hostedConfig = { diff --git a/src/HostedExplorerChildFrame.ts b/src/HostedExplorerChildFrame.ts index 2cff6c862..1366a0062 100644 --- a/src/HostedExplorerChildFrame.ts +++ b/src/HostedExplorerChildFrame.ts @@ -7,6 +7,7 @@ export interface HostedExplorerChildFrame extends Window { } export interface AAD { + aadToken: string; authType: AuthType.AAD; databaseAccount: DatabaseAccount; authorizationToken: string; diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 18ff57cc2..9b0805515 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -13,6 +13,7 @@ export type Features = { readonly enableSpark: boolean; readonly enableTtl: boolean; readonly executeSproc: boolean; + readonly enableAadDataPlane: boolean; readonly hostedDataExplorer: boolean; readonly junoEndpoint?: string; readonly livyEndpoint?: string; @@ -43,6 +44,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear return { canExceedMaximumValue: "true" === get("canexceedmaximumvalue"), cosmosdb: "true" === get("cosmosdb"), + enableAadDataPlane: "true" === get("enableaaddataplane"), enableChangeFeedPolicy: "true" === get("enablechangefeedpolicy"), enableFixedCollectionWithSharedThroughput: "true" === get("enablefixedcollectionwithsharedthroughput"), enableKOPanel: "true" === get("enablekopanel"), diff --git a/src/Terminal/index.ts b/src/Terminal/index.ts index eb272a1a5..02524eaad 100644 --- a/src/Terminal/index.ts +++ b/src/Terminal/index.ts @@ -1,11 +1,11 @@ -import "@jupyterlab/terminal/style/index.css"; -import "./index.css"; import { ServerConnection } from "@jupyterlab/services"; -import { JupyterLabAppFactory } from "./JupyterLabAppFactory"; +import "@jupyterlab/terminal/style/index.css"; +import { HttpHeaders, TerminalQueryParams } from "../Common/Constants"; import { Action } from "../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import { updateUserContext } from "../UserContext"; -import { HttpHeaders, TerminalQueryParams } from "../Common/Constants"; +import "./index.css"; +import { JupyterLabAppFactory } from "./JupyterLabAppFactory"; const getUrlVars = (): { [key: string]: string } => { const vars: { [key: string]: string } = {}; @@ -50,7 +50,7 @@ const createServerSettings = (urlVars: { [key: string]: string }): ServerConnect const main = async (): Promise => { const urlVars = getUrlVars(); - // Initialize userContext. Currently only subscrptionId is required by TelemetryProcessor + // Initialize userContext. Currently only subscriptionId is required by TelemetryProcessor updateUserContext({ subscriptionId: urlVars[TerminalQueryParams.SubscriptionId], }); diff --git a/src/UserContext.ts b/src/UserContext.ts index 0cbec9226..4655411ad 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -11,6 +11,7 @@ interface UserContext { readonly resourceGroup?: string; readonly databaseAccount?: DatabaseAccount; readonly endpoint?: string; + readonly aadToken?: string; readonly accessToken?: string; readonly authorizationToken?: string; readonly resourceToken?: string; diff --git a/src/hooks/useAADAuth.ts b/src/hooks/useAADAuth.ts index 5be0f7fc4..1c6e946b2 100644 --- a/src/hooks/useAADAuth.ts +++ b/src/hooks/useAADAuth.ts @@ -25,6 +25,7 @@ interface ReturnType { isLoggedIn: boolean; graphToken: string; armToken: string; + aadToken: string; login: () => void; logout: () => void; tenantId: string; @@ -40,6 +41,7 @@ export function useAADAuth(): ReturnType { const [tenantId, setTenantId] = React.useState(cachedTenantId); const [graphToken, setGraphToken] = React.useState(); const [armToken, setArmToken] = React.useState(); + const [aadToken, setAadToken] = React.useState(); msalInstance.setActiveAccount(account); const login = React.useCallback(async () => { @@ -79,9 +81,13 @@ export function useAADAuth(): ReturnType { authority: `https://login.microsoftonline.com/${tenantId}`, scopes: ["https://management.azure.com//.default"], }), - ]).then(([graphTokenResponse, armTokenResponse]) => { + msalInstance.acquireTokenSilent({ + scopes: ["https://cosmos.azure.com/.default"], + }), + ]).then(([graphTokenResponse, armTokenResponse, aadTokenResponse]) => { setGraphToken(graphTokenResponse.accessToken); setArmToken(armTokenResponse.accessToken); + setAadToken(aadTokenResponse.accessToken); }); } }, [account, tenantId]); @@ -92,6 +98,7 @@ export function useAADAuth(): ReturnType { isLoggedIn, graphToken, armToken, + aadToken, login, logout, switchTenant, diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 25807a8c9..d73c0572a 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -83,6 +83,7 @@ async function configureHostedWithAAD(config: AAD, explorerParams: ExplorerParam updateUserContext({ authType: AuthType.AAD, authorizationToken: `Bearer ${config.authorizationToken}`, + aadToken: config.aadToken, }); const account = config.databaseAccount; const accountResourceId = account.id; From 030a4dec3c22c69c485906121559b8fe099ed9af Mon Sep 17 00:00:00 2001 From: Hardikkumar Nai <80053762+hardiknai-techm@users.noreply.github.com> Date: Wed, 19 May 2021 08:00:11 +0530 Subject: [PATCH 3/6] Migrate Cassandra Add Container to React (#723) --- .eslintignore | 4 - src/Explorer/ComponentRegisterer.ts | 4 - .../SettingsComponent.test.tsx.snap | 180 ------ src/Explorer/Explorer.tsx | 27 +- src/Explorer/OpenActions.test.ts | 25 - src/Explorer/OpenActions.ts | 2 +- .../Panes/CassandraAddCollectionPane.html | 273 --------- .../Panes/CassandraAddCollectionPane.ts | 539 ------------------ .../CassandraAddCollectionPane.test.tsx | 32 ++ .../CassandraAddCollectionPane.tsx | 427 ++++++++++++++ .../CassandraAddCollectionPane.test.tsx.snap | 164 ++++++ .../GitHubReposPanel.test.tsx.snap | 45 -- src/Explorer/Panes/PaneComponents.ts | 15 - .../StringInputPane.test.tsx.snap | 45 -- ...eteDatabaseConfirmationPanel.test.tsx.snap | 45 -- src/Main.tsx | 1 - test/cassandra/container.spec.ts | 10 +- tsconfig.strict.json | 1 - 18 files changed, 645 insertions(+), 1194 deletions(-) delete mode 100644 src/Explorer/Panes/CassandraAddCollectionPane.html delete mode 100644 src/Explorer/Panes/CassandraAddCollectionPane.ts create mode 100644 src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.test.tsx create mode 100644 src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx create mode 100644 src/Explorer/Panes/CassandraAddCollectionPane/__snapshots__/CassandraAddCollectionPane.test.tsx.snap delete mode 100644 src/Explorer/Panes/PaneComponents.ts diff --git a/.eslintignore b/.eslintignore index 967345584..f723666ed 100644 --- a/.eslintignore +++ b/.eslintignore @@ -111,13 +111,9 @@ src/Explorer/OpenActionsStubs.ts src/Explorer/Panes/AddDatabasePane.ts src/Explorer/Panes/AddDatabasePane.test.ts src/Explorer/Panes/BrowseQueriesPane.ts -src/Explorer/Panes/CassandraAddCollectionPane.ts src/Explorer/Panes/ContextualPaneBase.ts -src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts -src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts # src/Explorer/Panes/GraphStylingPane.ts # src/Explorer/Panes/NewVertexPane.ts -src/Explorer/Panes/PaneComponents.ts src/Explorer/Panes/RenewAdHocAccessPane.ts src/Explorer/Panes/SetupNotebooksPane.ts src/Explorer/Panes/SwitchDirectoryPane.ts diff --git a/src/Explorer/ComponentRegisterer.ts b/src/Explorer/ComponentRegisterer.ts index 9a49fd643..778e89f02 100644 --- a/src/Explorer/ComponentRegisterer.ts +++ b/src/Explorer/ComponentRegisterer.ts @@ -4,13 +4,9 @@ import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponen import { EditorComponent } from "./Controls/Editor/EditorComponent"; import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent"; import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3"; -import * as PaneComponents from "./Panes/PaneComponents"; ko.components.register("editor", new EditorComponent()); ko.components.register("json-editor", new JsonEditorComponent()); ko.components.register("diff-editor", new DiffEditorComponent()); ko.components.register("dynamic-list", DynamicListComponent); ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3); - -// Panes -ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent()); diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index adfb92b42..6a8979379 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -38,51 +38,6 @@ exports[`SettingsComponent renders 1`] = ` "arcadiaToken": [Function], "canExceedMaximumValue": [Function], "canSaveQueries": [Function], - "cassandraAddCollectionPane": CassandraAddCollectionPane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "createTableQuery": [Function], - "dedicateTableThroughput": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "cassandraaddcollectionpane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isSharedAutoPilotSelected": [Function], - "isTemplateReady": [Function], - "keyspaceCreateNew": [Function], - "keyspaceHasSharedOffer": [Function], - "keyspaceId": [Function], - "keyspaceIds": [Function], - "keyspaceOffers": Map {}, - "keyspaceThroughput": [Function], - "maxThroughputRU": [Function], - "minThroughputRU": [Function], - "requestUnitsUsageCostDedicated": [Function], - "requestUnitsUsageCostShared": [Function], - "ruToolTipText": [Function], - "selectedAutoPilotThroughput": [Function], - "sharedAutoPilotThroughput": [Function], - "sharedThroughputRangeText": [Function], - "sharedThroughputSpendAck": [Function], - "sharedThroughputSpendAckText": [Function], - "sharedThroughputSpendAckVisible": [Function], - "tableId": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "userTableQuery": [Function], - "visible": [Function], - }, "closeDialog": undefined, "closeSidePanel": undefined, "collapsedResourceTreeWidth": 36, @@ -978,51 +933,6 @@ exports[`SettingsComponent renders 1`] = ` "arcadiaToken": [Function], "canExceedMaximumValue": [Function], "canSaveQueries": [Function], - "cassandraAddCollectionPane": CassandraAddCollectionPane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "createTableQuery": [Function], - "dedicateTableThroughput": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "cassandraaddcollectionpane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isSharedAutoPilotSelected": [Function], - "isTemplateReady": [Function], - "keyspaceCreateNew": [Function], - "keyspaceHasSharedOffer": [Function], - "keyspaceId": [Function], - "keyspaceIds": [Function], - "keyspaceOffers": Map {}, - "keyspaceThroughput": [Function], - "maxThroughputRU": [Function], - "minThroughputRU": [Function], - "requestUnitsUsageCostDedicated": [Function], - "requestUnitsUsageCostShared": [Function], - "ruToolTipText": [Function], - "selectedAutoPilotThroughput": [Function], - "sharedAutoPilotThroughput": [Function], - "sharedThroughputRangeText": [Function], - "sharedThroughputSpendAck": [Function], - "sharedThroughputSpendAckText": [Function], - "sharedThroughputSpendAckVisible": [Function], - "tableId": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "userTableQuery": [Function], - "visible": [Function], - }, "closeDialog": undefined, "closeSidePanel": undefined, "collapsedResourceTreeWidth": 36, @@ -1931,51 +1841,6 @@ exports[`SettingsComponent renders 1`] = ` "arcadiaToken": [Function], "canExceedMaximumValue": [Function], "canSaveQueries": [Function], - "cassandraAddCollectionPane": CassandraAddCollectionPane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "createTableQuery": [Function], - "dedicateTableThroughput": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "cassandraaddcollectionpane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isSharedAutoPilotSelected": [Function], - "isTemplateReady": [Function], - "keyspaceCreateNew": [Function], - "keyspaceHasSharedOffer": [Function], - "keyspaceId": [Function], - "keyspaceIds": [Function], - "keyspaceOffers": Map {}, - "keyspaceThroughput": [Function], - "maxThroughputRU": [Function], - "minThroughputRU": [Function], - "requestUnitsUsageCostDedicated": [Function], - "requestUnitsUsageCostShared": [Function], - "ruToolTipText": [Function], - "selectedAutoPilotThroughput": [Function], - "sharedAutoPilotThroughput": [Function], - "sharedThroughputRangeText": [Function], - "sharedThroughputSpendAck": [Function], - "sharedThroughputSpendAckText": [Function], - "sharedThroughputSpendAckVisible": [Function], - "tableId": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "userTableQuery": [Function], - "visible": [Function], - }, "closeDialog": undefined, "closeSidePanel": undefined, "collapsedResourceTreeWidth": 36, @@ -2871,51 +2736,6 @@ exports[`SettingsComponent renders 1`] = ` "arcadiaToken": [Function], "canExceedMaximumValue": [Function], "canSaveQueries": [Function], - "cassandraAddCollectionPane": CassandraAddCollectionPane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "createTableQuery": [Function], - "dedicateTableThroughput": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "cassandraaddcollectionpane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isSharedAutoPilotSelected": [Function], - "isTemplateReady": [Function], - "keyspaceCreateNew": [Function], - "keyspaceHasSharedOffer": [Function], - "keyspaceId": [Function], - "keyspaceIds": [Function], - "keyspaceOffers": Map {}, - "keyspaceThroughput": [Function], - "maxThroughputRU": [Function], - "minThroughputRU": [Function], - "requestUnitsUsageCostDedicated": [Function], - "requestUnitsUsageCostShared": [Function], - "ruToolTipText": [Function], - "selectedAutoPilotThroughput": [Function], - "sharedAutoPilotThroughput": [Function], - "sharedThroughputRangeText": [Function], - "sharedThroughputSpendAck": [Function], - "sharedThroughputSpendAckText": [Function], - "sharedThroughputSpendAckVisible": [Function], - "tableId": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "userTableQuery": [Function], - "visible": [Function], - }, "closeDialog": undefined, "closeSidePanel": undefined, "collapsedResourceTreeWidth": 36, diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 85a377370..a8d9fa7d7 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -54,7 +54,7 @@ import { NotebookUtil } from "./Notebook/NotebookUtil"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; import { AddDatabasePanel } from "./Panes/AddDatabasePanel/AddDatabasePanel"; import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane/BrowseQueriesPane"; -import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; +import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; import { ContextualPaneBase } from "./Panes/ContextualPaneBase"; import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane"; import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel"; @@ -148,7 +148,6 @@ export default class Explorer { public tabsManager: TabsManager; // Contextual panes - public cassandraAddCollectionPane: CassandraAddCollectionPane; private gitHubClient: GitHubClient; public gitHubOAuthService: GitHubOAuthService; public junoClient: JunoClient; @@ -398,13 +397,6 @@ export default class Explorer { } }); - this.cassandraAddCollectionPane = new CassandraAddCollectionPane({ - id: "cassandraaddcollectionpane", - visible: ko.observable(false), - - container: this, - }); - this.tabsManager = params?.tabsManager ?? new TabsManager(); this.tabsManager.openedTabs.subscribe((tabs) => { if (tabs.length === 0) { @@ -1138,7 +1130,10 @@ export default class Explorer { private getDeltaDatabases( updatedDatabaseList: DataModels.Database[] - ): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[] } { + ): { + toAdd: ViewModels.Database[]; + toDelete: ViewModels.Database[]; + } { const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => { const databaseExists = _.some( this.databases(), @@ -1791,7 +1786,7 @@ export default class Explorer { public onNewCollectionClicked(databaseId?: string): void { if (userContext.apiType === "Cassandra") { - this.cassandraAddCollectionPane.open(); + this.openCassandraAddCollectionPane(); } else { this.openAddCollectionPanel(databaseId); } @@ -1983,6 +1978,16 @@ export default class Explorer { ); } + public openCassandraAddCollectionPane(): void { + this.openSidePanel( + "Add Table", + this.closeSidePanel()} + cassandraApiClient={new CassandraAPIDataClient()} + /> + ); + } public openGitHubReposPanel(header: string, junoClient?: JunoClient): void { this.openSidePanel( header, diff --git a/src/Explorer/OpenActions.test.ts b/src/Explorer/OpenActions.test.ts index 0ea2057b7..d06e8b239 100644 --- a/src/Explorer/OpenActions.test.ts +++ b/src/Explorer/OpenActions.test.ts @@ -3,7 +3,6 @@ import { ActionContracts } from "../Contracts/ExplorerContracts"; import * as ViewModels from "../Contracts/ViewModels"; import Explorer from "./Explorer"; import { handleOpenAction } from "./OpenActions"; -import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; describe("OpenActions", () => { describe("handleOpenAction", () => { @@ -15,8 +14,6 @@ describe("OpenActions", () => { beforeEach(() => { explorer = {} as Explorer; explorer.onNewCollectionClicked = jest.fn(); - explorer.cassandraAddCollectionPane = {} as CassandraAddCollectionPane; - explorer.cassandraAddCollectionPane.open = jest.fn(); database = { id: ko.observable("db"), @@ -64,28 +61,6 @@ describe("OpenActions", () => { expect(actionHandled).toBe(true); }); - describe("CassandraAddCollection pane kind", () => { - it("string value should call cassandraAddCollectionPane.open", () => { - const action = { - actionType: "OpenPane", - paneKind: "CassandraAddCollection", - }; - - const actionHandled = handleOpenAction(action, [], explorer); - expect(explorer.cassandraAddCollectionPane.open).toHaveBeenCalled(); - }); - - it("enum value should call cassandraAddCollectionPane.open", () => { - const action = { - actionType: "OpenPane", - paneKind: ActionContracts.PaneKind.CassandraAddCollection, - }; - - const actionHandled = handleOpenAction(action, [], explorer); - expect(explorer.cassandraAddCollectionPane.open).toHaveBeenCalled(); - }); - }); - describe("AddCollection pane kind", () => { it("string value should call explorer.onNewCollectionClicked", () => { const action = { diff --git a/src/Explorer/OpenActions.ts b/src/Explorer/OpenActions.ts index afc3251c2..033cb8135 100644 --- a/src/Explorer/OpenActions.ts +++ b/src/Explorer/OpenActions.ts @@ -145,7 +145,7 @@ function openPane(action: ActionContracts.OpenPane, explorer: Explorer) { action.paneKind === ActionContracts.PaneKind.CassandraAddCollection || (action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection] ) { - explorer.cassandraAddCollectionPane.open(); + explorer.openCassandraAddCollectionPane(); } else if ( action.paneKind === ActionContracts.PaneKind.GlobalSettings || (action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings] diff --git a/src/Explorer/Panes/CassandraAddCollectionPane.html b/src/Explorer/Panes/CassandraAddCollectionPane.html deleted file mode 100644 index 2450196ee..000000000 --- a/src/Explorer/Panes/CassandraAddCollectionPane.html +++ /dev/null @@ -1,273 +0,0 @@ -
-
-
- -
- - - -
- loading indicator -
- -
-
-
diff --git a/src/Explorer/Panes/CassandraAddCollectionPane.ts b/src/Explorer/Panes/CassandraAddCollectionPane.ts deleted file mode 100644 index 18879e6d3..000000000 --- a/src/Explorer/Panes/CassandraAddCollectionPane.ts +++ /dev/null @@ -1,539 +0,0 @@ -import * as ko from "knockout"; -import * as _ from "underscore"; -import * as Constants from "../../Common/Constants"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; -import { configContext, Platform } from "../../ConfigContext"; -import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import * as AddCollectionUtility from "../../Shared/AddCollectionUtility"; -import * as SharedConstants from "../../Shared/Constants"; -import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import { userContext } from "../../UserContext"; -import * as AutoPilotUtils from "../../Utils/AutoPilotUtils"; -import * as PricingUtils from "../../Utils/PricingUtils"; -import { CassandraAPIDataClient } from "../Tables/TableDataClient"; -import { ContextualPaneBase } from "./ContextualPaneBase"; - -export default class CassandraAddCollectionPane extends ContextualPaneBase { - public createTableQuery: ko.Observable; - public keyspaceId: ko.Observable; - public maxThroughputRU: ko.Observable; - public minThroughputRU: ko.Observable; - public tableId: ko.Observable; - public throughput: ko.Observable; - public throughputRangeText: ko.Computed; - public sharedThroughputRangeText: ko.Computed; - public userTableQuery: ko.Observable; - public requestUnitsUsageCostDedicated: ko.Computed; - public requestUnitsUsageCostShared: ko.Computed; - public costsVisible: ko.PureComputed; - public keyspaceHasSharedOffer: ko.Observable; - public keyspaceIds: ko.ObservableArray; - public keyspaceThroughput: ko.Observable; - public keyspaceCreateNew: ko.Observable; - public dedicateTableThroughput: ko.Observable; - public canRequestSupport: ko.PureComputed; - public throughputSpendAckText: ko.Observable; - public throughputSpendAck: ko.Observable; - public sharedThroughputSpendAck: ko.Observable; - public sharedThroughputSpendAckText: ko.Observable; - public isAutoPilotSelected: ko.Observable; - public isSharedAutoPilotSelected: ko.Observable; - public selectedAutoPilotThroughput: ko.Observable; - public sharedAutoPilotThroughput: ko.Observable; - public autoPilotUsageCost: ko.Computed; - public sharedThroughputSpendAckVisible: ko.Computed; - public throughputSpendAckVisible: ko.Computed; - public canExceedMaximumValue: ko.PureComputed; - public isFreeTierAccount: ko.Computed; - public ruToolTipText: ko.Computed; - public canConfigureThroughput: ko.PureComputed; - - private keyspaceOffers: Map; - - constructor(options: ViewModels.PaneOptions) { - super(options); - this.title("Add Table"); - this.createTableQuery = ko.observable("CREATE TABLE "); - this.keyspaceCreateNew = ko.observable(true); - this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText()); - this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled()); - this.keyspaceOffers = new Map(); - this.keyspaceIds = ko.observableArray(); - this.keyspaceHasSharedOffer = ko.observable(false); - this.keyspaceThroughput = ko.observable(); - this.keyspaceId = ko.observable(""); - this.keyspaceId.subscribe((keyspaceId: string) => { - if (this.keyspaceIds.indexOf(keyspaceId) >= 0) { - this.keyspaceHasSharedOffer(this.keyspaceOffers.has(keyspaceId)); - } - }); - this.keyspaceId.extend({ rateLimit: 100 }); - this.dedicateTableThroughput = ko.observable(false); - const throughputDefaults = this.container.collectionCreationDefaults.throughput; - this.maxThroughputRU = ko.observable(throughputDefaults.unlimitedmax); - this.minThroughputRU = ko.observable(throughputDefaults.unlimitedmin); - - this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue()); - - this.isFreeTierAccount = ko.computed(() => { - return userContext?.databaseAccount?.properties?.enableFreeTier; - }); - - this.tableId = ko.observable(""); - this.isAutoPilotSelected = ko.observable(false); - this.isSharedAutoPilotSelected = ko.observable(false); - this.selectedAutoPilotThroughput = ko.observable(); - this.sharedAutoPilotThroughput = ko.observable(); - this.throughput = ko.observable(); - this.throughputRangeText = ko.pureComputed(() => { - const enableAutoPilot = this.isAutoPilotSelected(); - if (!enableAutoPilot) { - return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`; - } - return AutoPilotUtils.getAutoPilotHeaderText(); - }); - this.sharedThroughputRangeText = ko.pureComputed(() => { - if (this.isSharedAutoPilotSelected()) { - return AutoPilotUtils.getAutoPilotHeaderText(); - } - return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`; - }); - this.userTableQuery = ko.observable("(userid int, name text, email text, PRIMARY KEY (userid))"); - this.keyspaceId.subscribe((keyspaceId) => { - this.createTableQuery(`CREATE TABLE ${keyspaceId}.`); - }); - - this.throughputSpendAckText = ko.observable(); - this.throughputSpendAck = ko.observable(false); - this.sharedThroughputSpendAck = ko.observable(false); - this.sharedThroughputSpendAckText = ko.observable(); - - this.resetData(); - - this.requestUnitsUsageCostDedicated = ko.computed(() => { - const { databaseAccount: account } = userContext; - if (!account) { - return ""; - } - - const regions = - (account && - account.properties && - account.properties.readLocations && - account.properties.readLocations.length) || - 1; - const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false; - const offerThroughput: number = this.throughput(); - let estimatedSpend: string; - let estimatedDedicatedSpendAcknowledge: string; - if (!this.isAutoPilotSelected()) { - estimatedSpend = PricingUtils.getEstimatedSpendHtml( - offerThroughput, - userContext.portalEnv, - regions, - multimaster - ); - estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( - offerThroughput, - userContext.portalEnv, - regions, - multimaster, - this.isAutoPilotSelected() - ); - } else { - estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( - this.selectedAutoPilotThroughput(), - userContext.portalEnv, - regions, - multimaster - ); - estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( - this.selectedAutoPilotThroughput(), - userContext.portalEnv, - regions, - multimaster, - this.isAutoPilotSelected() - ); - } - this.throughputSpendAckText(estimatedDedicatedSpendAcknowledge); - return estimatedSpend; - }); - - this.requestUnitsUsageCostShared = ko.computed(() => { - const { databaseAccount: account } = userContext; - if (!account) { - return ""; - } - - const regions = account?.properties?.readLocations?.length || 1; - const multimaster = account?.properties?.enableMultipleWriteLocations || false; - let estimatedSpend: string; - let estimatedSharedSpendAcknowledge: string; - if (!this.isSharedAutoPilotSelected()) { - estimatedSpend = PricingUtils.getEstimatedSpendHtml( - this.keyspaceThroughput(), - userContext.portalEnv, - regions, - multimaster - ); - estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( - this.keyspaceThroughput(), - userContext.portalEnv, - regions, - multimaster, - this.isSharedAutoPilotSelected() - ); - } else { - estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( - this.sharedAutoPilotThroughput(), - userContext.portalEnv, - regions, - multimaster - ); - estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( - this.sharedAutoPilotThroughput(), - userContext.portalEnv, - regions, - multimaster, - this.isSharedAutoPilotSelected() - ); - } - this.sharedThroughputSpendAckText(estimatedSharedSpendAcknowledge); - return estimatedSpend; - }); - - this.costsVisible = ko.pureComputed(() => { - return configContext.platform !== Platform.Emulator; - }); - - this.canRequestSupport = ko.pureComputed(() => { - if (configContext.platform !== Platform.Emulator && !userContext.isTryCosmosDBSubscription) { - const offerThroughput: number = this.throughput(); - return offerThroughput <= 100000; - } - - return false; - }); - - this.sharedThroughputSpendAckVisible = ko.computed(() => { - const autoscaleThroughput = this.sharedAutoPilotThroughput() * 1; - if (this.isSharedAutoPilotSelected()) { - return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K; - } - - return this.keyspaceThroughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs100K; - }); - - this.throughputSpendAckVisible = ko.pureComputed(() => { - const autoscaleThroughput = this.selectedAutoPilotThroughput() * 1; - if (this.isAutoPilotSelected()) { - return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K; - } - - return this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs100K; - }); - - if (!!this.container) { - const updateKeyspaceIds: (keyspaces: ViewModels.Database[]) => void = ( - newKeyspaceIds: ViewModels.Database[] - ): void => { - const cachedKeyspaceIdsList = _.map(newKeyspaceIds, (keyspace: ViewModels.Database) => { - if (keyspace && keyspace.offer && !!keyspace.offer()) { - this.keyspaceOffers.set(keyspace.id(), keyspace.offer()); - } - return keyspace.id(); - }); - this.keyspaceIds(cachedKeyspaceIdsList); - }; - this.container.databases.subscribe((newDatabases: ViewModels.Database[]) => updateKeyspaceIds(newDatabases)); - updateKeyspaceIds(this.container.databases()); - } - - this.autoPilotUsageCost = ko.pureComputed(() => { - const autoPilot = this._getAutoPilot(); - if (!autoPilot) { - return ""; - } - const isDatabaseThroughput: boolean = this.keyspaceCreateNew(); - return PricingUtils.getAutoPilotV3SpendHtml(autoPilot.maxThroughput, isDatabaseThroughput); - }); - } - - public decreaseThroughput() { - let offerThroughput: number = this.throughput(); - - if (offerThroughput > this.minThroughputRU()) { - offerThroughput -= 100; - this.throughput(offerThroughput); - } - } - - public increaseThroughput() { - let offerThroughput: number = this.throughput(); - - if (offerThroughput < this.maxThroughputRU()) { - offerThroughput += 100; - this.throughput(offerThroughput); - } - } - - public open() { - super.open(); - if (!this.container.isServerlessEnabled()) { - this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); - } - const addCollectionPaneOpenMessage = { - collection: ko.toJS({ - id: this.tableId(), - storage: Constants.BackendDefaults.multiPartitionStorageInGb, - offerThroughput: this.throughput(), - partitionKey: "", - databaseId: this.keyspaceId(), - }), - subscriptionType: userContext.subscriptionType, - subscriptionQuotaId: userContext.quotaId, - defaultsCheck: { - storage: "u", - throughput: this.throughput(), - flight: userContext.addCollectionFlight, - }, - dataExplorerArea: Constants.Areas.ContextualPane, - }; - const focusElement = document.getElementById("keyspace-id"); - focusElement && focusElement.focus(); - TelemetryProcessor.trace(Action.CreateCollection, ActionModifiers.Open, addCollectionPaneOpenMessage); - } - - public submit() { - if (!this._isValid()) { - return; - } - this.isExecuting(true); - const autoPilotCommand = `cosmosdb_autoscale_max_throughput`; - let createTableAndKeyspacePromise: Q.Promise; - const toCreateKeyspace: boolean = this.keyspaceCreateNew(); - const useAutoPilotForKeyspace: boolean = this.isSharedAutoPilotSelected() && !!this.sharedAutoPilotThroughput(); - const createKeyspaceQueryPrefix: string = `CREATE KEYSPACE ${this.keyspaceId().trim()} WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 }`; - const createKeyspaceQuery: string = this.keyspaceHasSharedOffer() - ? useAutoPilotForKeyspace - ? `${createKeyspaceQueryPrefix} AND ${autoPilotCommand}=${this.sharedAutoPilotThroughput()};` - : `${createKeyspaceQueryPrefix} AND cosmosdb_provisioned_throughput=${this.keyspaceThroughput()};` - : `${createKeyspaceQueryPrefix};`; - const createTableQueryPrefix: string = `${this.createTableQuery()}${this.tableId().trim()} ${this.userTableQuery()}`; - let createTableQuery: string; - - if (this.canConfigureThroughput() && (this.dedicateTableThroughput() || !this.keyspaceHasSharedOffer())) { - if (this.isAutoPilotSelected() && this.selectedAutoPilotThroughput()) { - createTableQuery = `${createTableQueryPrefix} WITH ${autoPilotCommand}=${this.selectedAutoPilotThroughput()};`; - } else { - createTableQuery = `${createTableQueryPrefix} WITH cosmosdb_provisioned_throughput=${this.throughput()};`; - } - } else { - createTableQuery = `${createTableQueryPrefix};`; - } - - const addCollectionPaneStartMessage = { - collection: ko.toJS({ - id: this.tableId(), - storage: Constants.BackendDefaults.multiPartitionStorageInGb, - offerThroughput: this.throughput(), - partitionKey: "", - databaseId: this.keyspaceId(), - hasDedicatedThroughput: this.dedicateTableThroughput(), - }), - keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), - subscriptionType: userContext.subscriptionType, - subscriptionQuotaId: userContext.quotaId, - defaultsCheck: { - storage: "u", - throughput: this.throughput(), - flight: userContext.addCollectionFlight, - }, - dataExplorerArea: Constants.Areas.ContextualPane, - toCreateKeyspace: toCreateKeyspace, - createKeyspaceQuery: createKeyspaceQuery, - createTableQuery: createTableQuery, - }; - const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, addCollectionPaneStartMessage); - const { databaseAccount } = userContext; - if (toCreateKeyspace) { - createTableAndKeyspacePromise = (this.container.tableDataClient).createTableAndKeyspace( - databaseAccount?.properties.cassandraEndpoint, - databaseAccount?.id, - this.container, - createTableQuery, - createKeyspaceQuery - ); - } else { - createTableAndKeyspacePromise = (this.container.tableDataClient).createTableAndKeyspace( - databaseAccount?.properties.cassandraEndpoint, - databaseAccount?.id, - this.container, - createTableQuery - ); - } - createTableAndKeyspacePromise.then( - () => { - this.container.refreshAllDatabases(); - this.isExecuting(false); - this.close(); - const addCollectionPaneSuccessMessage = { - collection: ko.toJS({ - id: this.tableId(), - storage: Constants.BackendDefaults.multiPartitionStorageInGb, - offerThroughput: this.throughput(), - partitionKey: "", - databaseId: this.keyspaceId(), - hasDedicatedThroughput: this.dedicateTableThroughput(), - }), - keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), - subscriptionType: userContext.subscriptionType, - subscriptionQuotaId: userContext.quotaId, - defaultsCheck: { - storage: "u", - throughput: this.throughput(), - flight: userContext.addCollectionFlight, - }, - dataExplorerArea: Constants.Areas.ContextualPane, - toCreateKeyspace: toCreateKeyspace, - createKeyspaceQuery: createKeyspaceQuery, - createTableQuery: createTableQuery, - }; - TelemetryProcessor.traceSuccess(Action.CreateCollection, addCollectionPaneSuccessMessage, startKey); - }, - (error) => { - const errorMessage = getErrorMessage(error); - this.formErrors(errorMessage); - this.isExecuting(false); - const addCollectionPaneFailedMessage = { - collection: { - id: this.tableId(), - storage: Constants.BackendDefaults.multiPartitionStorageInGb, - offerThroughput: this.throughput(), - partitionKey: "", - databaseId: this.keyspaceId(), - hasDedicatedThroughput: this.dedicateTableThroughput(), - }, - keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), - subscriptionType: userContext.subscriptionType, - subscriptionQuotaId: userContext.quotaId, - defaultsCheck: { - storage: "u", - throughput: this.throughput(), - flight: userContext.addCollectionFlight, - }, - dataExplorerArea: Constants.Areas.ContextualPane, - toCreateKeyspace: toCreateKeyspace, - createKeyspaceQuery: createKeyspaceQuery, - createTableQuery: createTableQuery, - error: errorMessage, - errorStack: getErrorStack(error), - }; - TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey); - } - ); - } - - public resetData() { - super.resetData(); - const throughputDefaults = this.container.collectionCreationDefaults.throughput; - this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); - this.isSharedAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); - this.selectedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); - this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); - this.throughput(AddCollectionUtility.getMaxThroughput(this.container.collectionCreationDefaults, this.container)); - this.keyspaceThroughput(throughputDefaults.shared); - this.maxThroughputRU(throughputDefaults.unlimitedmax); - this.minThroughputRU(throughputDefaults.unlimitedmin); - this.createTableQuery("CREATE TABLE "); - this.userTableQuery("(userid int, name text, email text, PRIMARY KEY (userid))"); - this.tableId(""); - this.keyspaceId(""); - this.throughputSpendAck(false); - this.keyspaceHasSharedOffer(false); - this.keyspaceCreateNew(true); - } - - private _isValid(): boolean { - const throughput = this.throughput(); - const keyspaceThroughput = this.keyspaceThroughput(); - - const sharedAutoscaleThroughput = this.sharedAutoPilotThroughput() * 1; - if ( - this.isSharedAutoPilotSelected() && - sharedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && - !this.sharedThroughputSpendAck() - ) { - this.formErrors(`Please acknowledge the estimated monthly spend.`); - return false; - } - - const dedicatedAutoscaleThroughput = this.selectedAutoPilotThroughput() * 1; - if ( - this.isAutoPilotSelected() && - dedicatedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && - !this.throughputSpendAck() - ) { - this.formErrors(`Please acknowledge the estimated monthly spend.`); - return false; - } - - if ( - (this.keyspaceCreateNew() && this.keyspaceHasSharedOffer() && this.isSharedAutoPilotSelected()) || - this.isAutoPilotSelected() - ) { - const autoPilot = this._getAutoPilot(); - if ( - !autoPilot || - !autoPilot.maxThroughput || - !AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput) - ) { - this.formErrors( - `Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput` - ); - return false; - } - return true; - } - - if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) { - this.formErrors(`Please acknowledge the estimated daily spend.`); - return false; - } - - if ( - this.keyspaceHasSharedOffer() && - this.keyspaceCreateNew() && - keyspaceThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && - !this.sharedThroughputSpendAck() - ) { - this.formErrors("Please acknowledge the estimated daily spend"); - return false; - } - - return true; - } - - private _getAutoPilot(): DataModels.AutoPilotCreationSettings { - if ( - this.keyspaceCreateNew() && - this.keyspaceHasSharedOffer() && - this.isSharedAutoPilotSelected() && - this.sharedAutoPilotThroughput() - ) { - return { - maxThroughput: this.sharedAutoPilotThroughput() * 1, - }; - } - - if (this.selectedAutoPilotThroughput()) { - return { - maxThroughput: this.selectedAutoPilotThroughput() * 1, - }; - } - - return undefined; - } -} diff --git a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.test.tsx b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.test.tsx new file mode 100644 index 000000000..20433bac5 --- /dev/null +++ b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.test.tsx @@ -0,0 +1,32 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { shallow } from "enzyme"; +import React from "react"; +import Explorer from "../../Explorer"; +import { CassandraAPIDataClient } from "../../Tables/TableDataClient"; +import { CassandraAddCollectionPane } from "./CassandraAddCollectionPane"; +const props = { + explorer: new Explorer(), + closePanel: (): void => undefined, + cassandraApiClient: new CassandraAPIDataClient(), +}; + +describe("CassandraAddCollectionPane Pane", () => { + beforeEach(() => render()); + + it("should render Default properly", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + it("click on is Create new keyspace", () => { + fireEvent.click(screen.getByLabelText("Create new keyspace")); + expect(screen.getByLabelText("Provision keyspace throughput")).toBeDefined(); + }); + it("click on Use existing", () => { + fireEvent.click(screen.getByLabelText("Use existing keyspace")); + }); + + it("Enter Keyspace name ", () => { + fireEvent.change(screen.getByLabelText("Keyspace id"), { target: { value: "unittest1" } }); + expect(screen.getByLabelText("CREATE TABLE unittest1.")).toBeDefined(); + }); +}); diff --git a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx new file mode 100644 index 000000000..d7b587d95 --- /dev/null +++ b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx @@ -0,0 +1,427 @@ +import { Label, Stack, TextField } from "@fluentui/react"; +import React, { FunctionComponent, useEffect, useState } from "react"; +import * as _ from "underscore"; +import * as Constants from "../../../Common/Constants"; +import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; +import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip"; +import * as DataModels from "../../../Contracts/DataModels"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import * as AddCollectionUtility from "../../../Shared/AddCollectionUtility"; +import * as SharedConstants from "../../../Shared/Constants"; +import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; +import { userContext } from "../../../UserContext"; +import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; +import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput"; +import Explorer from "../../Explorer"; +import { CassandraAPIDataClient } from "../../Tables/TableDataClient"; +import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; + +export interface CassandraAddCollectionPaneProps { + explorer: Explorer; + closePanel: () => void; + cassandraApiClient: CassandraAPIDataClient; +} + +export const CassandraAddCollectionPane: FunctionComponent = ({ + explorer: container, + closePanel, + cassandraApiClient, +}: CassandraAddCollectionPaneProps) => { + const throughputDefaults = container.collectionCreationDefaults.throughput; + const [createTableQuery, setCreateTableQuery] = useState("CREATE TABLE "); + const [keyspaceId, setKeyspaceId] = useState(""); + const [tableId, setTableId] = useState(""); + const [throughput, setThroughput] = useState( + AddCollectionUtility.getMaxThroughput(container.collectionCreationDefaults, container) + ); + + const [isAutoPilotSelected, setIsAutoPilotSelected] = useState(container.isAutoscaleDefaultEnabled()); + + const [isSharedAutoPilotSelected, setIsSharedAutoPilotSelected] = useState( + container.isAutoscaleDefaultEnabled() + ); + + const [userTableQuery, setUserTableQuery] = useState( + "(userid int, name text, email text, PRIMARY KEY (userid))" + ); + + const [keyspaceHasSharedOffer, setKeyspaceHasSharedOffer] = useState(false); + const [keyspaceIds, setKeyspaceIds] = useState([]); + const [keyspaceThroughput, setKeyspaceThroughput] = useState(throughputDefaults.shared); + const [keyspaceCreateNew, setKeyspaceCreateNew] = useState(true); + const [dedicateTableThroughput, setDedicateTableThroughput] = useState(false); + const [throughputSpendAck, setThroughputSpendAck] = useState(false); + const [sharedThroughputSpendAck, setSharedThroughputSpendAck] = useState(false); + + const { minAutoPilotThroughput: selectedAutoPilotThroughput } = AutoPilotUtils; + const { minAutoPilotThroughput: sharedAutoPilotThroughput } = AutoPilotUtils; + + const _getAutoPilot = (): DataModels.AutoPilotCreationSettings => { + if (keyspaceCreateNew && keyspaceHasSharedOffer && isSharedAutoPilotSelected && sharedAutoPilotThroughput) { + return { + maxThroughput: sharedAutoPilotThroughput * 1, + }; + } + + if (selectedAutoPilotThroughput) { + return { + maxThroughput: selectedAutoPilotThroughput * 1, + }; + } + + return undefined; + }; + + const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier; + + const canConfigureThroughput = !container.isServerlessEnabled(); + + const keyspaceOffers = new Map(); + const [isExecuting, setIsExecuting] = useState(); + const [formErrors, setFormErrors] = useState(""); + + useEffect(() => { + if (keyspaceIds.indexOf(keyspaceId) >= 0) { + setKeyspaceHasSharedOffer(keyspaceOffers.has(keyspaceId)); + } + setCreateTableQuery(`CREATE TABLE ${keyspaceId}.`); + }, [keyspaceId]); + + const addCollectionPaneOpenMessage = { + collection: { + id: tableId, + storage: Constants.BackendDefaults.multiPartitionStorageInGb, + offerThroughput: throughput, + partitionKey: "", + databaseId: keyspaceId, + }, + subscriptionType: userContext.subscriptionType, + subscriptionQuotaId: userContext.quotaId, + defaultsCheck: { + storage: "u", + throughput, + flight: userContext.addCollectionFlight, + }, + dataExplorerArea: Constants.Areas.ContextualPane, + }; + + useEffect(() => { + if (!container.isServerlessEnabled()) { + setIsAutoPilotSelected(container.isAutoscaleDefaultEnabled()); + } + + TelemetryProcessor.trace(Action.CreateCollection, ActionModifiers.Open, addCollectionPaneOpenMessage); + }, []); + + useEffect(() => { + if (container) { + const newKeyspaceIds: ViewModels.Database[] = container.databases(); + const cachedKeyspaceIdsList = _.map(newKeyspaceIds, (keyspace: ViewModels.Database) => { + if (keyspace && keyspace.offer && !!keyspace.offer()) { + keyspaceOffers.set(keyspace.id(), keyspace.offer()); + } + return keyspace.id(); + }); + setKeyspaceIds(cachedKeyspaceIdsList); + } + }, []); + + const _isValid = () => { + const sharedAutoscaleThroughput = sharedAutoPilotThroughput * 1; + if ( + isSharedAutoPilotSelected && + sharedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && + !sharedThroughputSpendAck + ) { + setFormErrors(`Please acknowledge the estimated monthly spend.`); + return false; + } + + const dedicatedAutoscaleThroughput = selectedAutoPilotThroughput * 1; + if ( + isAutoPilotSelected && + dedicatedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && + !throughputSpendAck + ) { + setFormErrors(`Please acknowledge the estimated monthly spend.`); + return false; + } + + if ((keyspaceCreateNew && keyspaceHasSharedOffer && isSharedAutoPilotSelected) || isAutoPilotSelected) { + const autoPilot = _getAutoPilot(); + if ( + !autoPilot || + !autoPilot.maxThroughput || + !AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput) + ) { + setFormErrors( + `Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput` + ); + return false; + } + return true; + } + + if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !throughputSpendAck) { + setFormErrors(`Please acknowledge the estimated daily spend.`); + return false; + } + + if ( + keyspaceHasSharedOffer && + keyspaceCreateNew && + keyspaceThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && + !sharedThroughputSpendAck + ) { + setFormErrors("Please acknowledge the estimated daily spend"); + return false; + } + + return true; + }; + + const onSubmit = async () => { + if (!_isValid()) { + return; + } + setIsExecuting(true); + const autoPilotCommand = `cosmosdb_autoscale_max_throughput`; + + const toCreateKeyspace: boolean = keyspaceCreateNew; + const useAutoPilotForKeyspace: boolean = isSharedAutoPilotSelected && !!sharedAutoPilotThroughput; + const createKeyspaceQueryPrefix = `CREATE KEYSPACE ${keyspaceId.trim()} WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 }`; + const createKeyspaceQuery: string = keyspaceHasSharedOffer + ? useAutoPilotForKeyspace + ? `${createKeyspaceQueryPrefix} AND ${autoPilotCommand}=${keyspaceThroughput};` + : `${createKeyspaceQueryPrefix} AND cosmosdb_provisioned_throughput=${keyspaceThroughput};` + : `${createKeyspaceQueryPrefix};`; + let tableQuery: string; + const createTableQueryPrefix = `${createTableQuery}${tableId.trim()} ${userTableQuery}`; + + if (canConfigureThroughput && (dedicateTableThroughput || !keyspaceHasSharedOffer)) { + if (isAutoPilotSelected && selectedAutoPilotThroughput) { + tableQuery = `${createTableQueryPrefix} WITH ${autoPilotCommand}=${throughput};`; + } else { + tableQuery = `${createTableQueryPrefix} WITH cosmosdb_provisioned_throughput=${throughput};`; + } + } else { + tableQuery = `${createTableQueryPrefix};`; + } + + const addCollectionPaneStartMessage = { + ...addCollectionPaneOpenMessage, + collection: { + ...addCollectionPaneOpenMessage.collection, + hasDedicatedThroughput: dedicateTableThroughput, + }, + keyspaceHasSharedOffer, + toCreateKeyspace, + createKeyspaceQuery, + createTableQuery: tableQuery, + }; + + const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, addCollectionPaneStartMessage); + try { + if (toCreateKeyspace) { + await cassandraApiClient.createTableAndKeyspace( + userContext?.databaseAccount?.properties?.cassandraEndpoint, + userContext?.databaseAccount?.id, + container, + tableQuery, + createKeyspaceQuery + ); + } else { + await cassandraApiClient.createTableAndKeyspace( + userContext?.databaseAccount?.properties?.cassandraEndpoint, + userContext?.databaseAccount?.id, + container, + tableQuery + ); + } + container.refreshAllDatabases(); + setIsExecuting(false); + closePanel(); + + TelemetryProcessor.traceSuccess(Action.CreateCollection, addCollectionPaneStartMessage, startKey); + } catch (error) { + const errorMessage = getErrorMessage(error); + setFormErrors(errorMessage); + setIsExecuting(false); + const addCollectionPaneFailedMessage = { + ...addCollectionPaneStartMessage, + error: errorMessage, + errorStack: getErrorStack(error), + }; + TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey); + } + }; + const handleOnChangeKeyspaceType = (ev: React.FormEvent, mode: string): void => { + setKeyspaceCreateNew(mode === "Create new"); + }; + + const props: RightPaneFormProps = { + expandConsole: () => container.expandConsole(), + formError: formErrors, + isExecuting, + submitButtonText: "Apply", + onSubmit, + }; + return ( + +
+
+

+ +

+ + + handleOnChangeKeyspaceType(e, "Create new")} + /> + Create new + + handleOnChangeKeyspaceType(e, "Use existing")} + /> + Use existing + + + setKeyspaceId(newValue)} + ariaLabel="Keyspace id" + autoFocus + /> + + {keyspaceIds?.map((id: string, index: number) => ( + + ))} + + {canConfigureThroughput && keyspaceCreateNew && ( +
+ setKeyspaceHasSharedOffer(e.target.checked)} + /> + + Provision keyspace throughput + + + Provisioned throughput at the keyspace level will be shared across unlimited number of tables within the + keyspace + +
+ )} + {canConfigureThroughput && keyspaceCreateNew && keyspaceHasSharedOffer && ( +
+ setKeyspaceThroughput(throughput)} + setIsAutoscale={(isAutoscale: boolean) => setIsSharedAutoPilotSelected(isAutoscale)} + onCostAcknowledgeChange={(isAcknowledge: boolean) => { + setSharedThroughputSpendAck(isAcknowledge); + }} + /> +
+ )} +
+
+

+ +

+
+ {createTableQuery} +
+ setTableId(newValue)} + style={{ marginBottom: "5px" }} + /> + setUserTableQuery(newValue)} + /> +
+ + {canConfigureThroughput && keyspaceHasSharedOffer && !keyspaceCreateNew && ( +
+ setDedicateTableThroughput(e.target.checked)} + /> + Provision dedicated throughput for this table + + You can optionally provision dedicated throughput for a table within a keyspace that has throughput + provisioned. This dedicated throughput amount will not be shared with other tables in the keyspace and + does not count towards the throughput you provisioned for the keyspace. This throughput amount will be + billed in addition to the throughput amount you provisioned at the keyspace level. + +
+ )} + {canConfigureThroughput && (!keyspaceHasSharedOffer || dedicateTableThroughput) && ( +
+ setThroughput(throughput)} + setIsAutoscale={(isAutoscale: boolean) => setIsAutoPilotSelected(isAutoscale)} + onCostAcknowledgeChange={(isAcknowledge: boolean) => { + setThroughputSpendAck(isAcknowledge); + }} + /> +
+ )} +
+
+ ); +}; diff --git a/src/Explorer/Panes/CassandraAddCollectionPane/__snapshots__/CassandraAddCollectionPane.test.tsx.snap b/src/Explorer/Panes/CassandraAddCollectionPane/__snapshots__/CassandraAddCollectionPane.test.tsx.snap new file mode 100644 index 000000000..19e1b1e10 --- /dev/null +++ b/src/Explorer/Panes/CassandraAddCollectionPane/__snapshots__/CassandraAddCollectionPane.test.tsx.snap @@ -0,0 +1,164 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CassandraAddCollectionPane Pane should render Default properly 1`] = ` + +
+
+

+ + Keyspace name + + Select an existing keyspace or enter a new keyspace id. + + +

+ + + + Create new + + + + Use existing + + + + +
+ + + Provision keyspace throughput + + + Provisioned throughput at the keyspace level will be shared across unlimited number of tables within the keyspace + +
+
+
+

+ + Enter CQL command to create the table. + + Learn More + + +

+
+ CREATE TABLE +
+ + +
+
+ +
+
+
+`; diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index 417bbd332..fddba4c4d 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -27,51 +27,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "arcadiaToken": [Function], "canExceedMaximumValue": [Function], "canSaveQueries": [Function], - "cassandraAddCollectionPane": CassandraAddCollectionPane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "createTableQuery": [Function], - "dedicateTableThroughput": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "cassandraaddcollectionpane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isSharedAutoPilotSelected": [Function], - "isTemplateReady": [Function], - "keyspaceCreateNew": [Function], - "keyspaceHasSharedOffer": [Function], - "keyspaceId": [Function], - "keyspaceIds": [Function], - "keyspaceOffers": Map {}, - "keyspaceThroughput": [Function], - "maxThroughputRU": [Function], - "minThroughputRU": [Function], - "requestUnitsUsageCostDedicated": [Function], - "requestUnitsUsageCostShared": [Function], - "ruToolTipText": [Function], - "selectedAutoPilotThroughput": [Function], - "sharedAutoPilotThroughput": [Function], - "sharedThroughputRangeText": [Function], - "sharedThroughputSpendAck": [Function], - "sharedThroughputSpendAckText": [Function], - "sharedThroughputSpendAckVisible": [Function], - "tableId": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "userTableQuery": [Function], - "visible": [Function], - }, "closeDialog": undefined, "closeSidePanel": undefined, "collapsedResourceTreeWidth": 36, diff --git a/src/Explorer/Panes/PaneComponents.ts b/src/Explorer/Panes/PaneComponents.ts deleted file mode 100644 index 81163e94a..000000000 --- a/src/Explorer/Panes/PaneComponents.ts +++ /dev/null @@ -1,15 +0,0 @@ -import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html"; -export class PaneComponent { - constructor(data: any) { - return data.data; - } -} - -export class CassandraAddCollectionPaneComponent { - constructor() { - return { - viewModel: PaneComponent, - template: CassandraAddCollectionPaneTemplate, - }; - } -} diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index 77a3af75a..c25904425 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -17,51 +17,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "arcadiaToken": [Function], "canExceedMaximumValue": [Function], "canSaveQueries": [Function], - "cassandraAddCollectionPane": CassandraAddCollectionPane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "createTableQuery": [Function], - "dedicateTableThroughput": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "cassandraaddcollectionpane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isSharedAutoPilotSelected": [Function], - "isTemplateReady": [Function], - "keyspaceCreateNew": [Function], - "keyspaceHasSharedOffer": [Function], - "keyspaceId": [Function], - "keyspaceIds": [Function], - "keyspaceOffers": Map {}, - "keyspaceThroughput": [Function], - "maxThroughputRU": [Function], - "minThroughputRU": [Function], - "requestUnitsUsageCostDedicated": [Function], - "requestUnitsUsageCostShared": [Function], - "ruToolTipText": [Function], - "selectedAutoPilotThroughput": [Function], - "sharedAutoPilotThroughput": [Function], - "sharedThroughputRangeText": [Function], - "sharedThroughputSpendAck": [Function], - "sharedThroughputSpendAckText": [Function], - "sharedThroughputSpendAckVisible": [Function], - "tableId": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "userTableQuery": [Function], - "visible": [Function], - }, "closeDialog": undefined, "closeSidePanel": undefined, "collapsedResourceTreeWidth": 36, diff --git a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap index 44a530919..59f9a7ab6 100644 --- a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap @@ -15,51 +15,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database "arcadiaToken": [Function], "canExceedMaximumValue": [Function], "canSaveQueries": [Function], - "cassandraAddCollectionPane": CassandraAddCollectionPane { - "autoPilotUsageCost": [Function], - "canConfigureThroughput": [Function], - "canExceedMaximumValue": [Function], - "canRequestSupport": [Function], - "container": [Circular], - "costsVisible": [Function], - "createTableQuery": [Function], - "dedicateTableThroughput": [Function], - "firstFieldHasFocus": [Function], - "formErrors": [Function], - "formErrorsDetails": [Function], - "id": "cassandraaddcollectionpane", - "isAutoPilotSelected": [Function], - "isExecuting": [Function], - "isFreeTierAccount": [Function], - "isSharedAutoPilotSelected": [Function], - "isTemplateReady": [Function], - "keyspaceCreateNew": [Function], - "keyspaceHasSharedOffer": [Function], - "keyspaceId": [Function], - "keyspaceIds": [Function], - "keyspaceOffers": Map {}, - "keyspaceThroughput": [Function], - "maxThroughputRU": [Function], - "minThroughputRU": [Function], - "requestUnitsUsageCostDedicated": [Function], - "requestUnitsUsageCostShared": [Function], - "ruToolTipText": [Function], - "selectedAutoPilotThroughput": [Function], - "sharedAutoPilotThroughput": [Function], - "sharedThroughputRangeText": [Function], - "sharedThroughputSpendAck": [Function], - "sharedThroughputSpendAckText": [Function], - "sharedThroughputSpendAckVisible": [Function], - "tableId": [Function], - "throughput": [Function], - "throughputRangeText": [Function], - "throughputSpendAck": [Function], - "throughputSpendAckText": [Function], - "throughputSpendAckVisible": [Function], - "title": [Function], - "userTableQuery": [Function], - "visible": [Function], - }, "closeDialog": undefined, "closeSidePanel": undefined, "collapsedResourceTreeWidth": 36, diff --git a/src/Main.tsx b/src/Main.tsx index b99d951b0..4b04fae7e 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -226,7 +226,6 @@ const App: React.FunctionComponent = () => { closePanel={closeSidePanel} isConsoleExpanded={isNotificationConsoleExpanded} /> -
{showDialog && }
); diff --git a/test/cassandra/container.spec.ts b/test/cassandra/container.spec.ts index 45ee23ce7..cb207cae9 100644 --- a/test/cassandra/container.spec.ts +++ b/test/cassandra/container.spec.ts @@ -15,11 +15,11 @@ test("Cassandra keyspace and table CRUD", async () => { }); await explorer.click('[data-test="New Table"]'); - await explorer.click('[data-test="addCollection-keyspaceId"]'); - await explorer.fill('[data-test="addCollection-keyspaceId"]', keyspaceId); - await explorer.click('[data-test="addCollection-tableId"]'); - await explorer.fill('[data-test="addCollection-tableId"]', tableId); - await explorer.click('[aria-label="Add Table"] [data-test="addCollection-createCollection"]'); + await explorer.click('[aria-label="Keyspace id"]'); + await explorer.fill('[aria-label="Keyspace id"]', keyspaceId); + await explorer.click('[aria-label="addCollection-tableId"]'); + await explorer.fill('[aria-label="addCollection-tableId"]', tableId); + await explorer.click("#sidePanelOkButton"); await safeClick(explorer, `.nodeItem >> text=${keyspaceId}`); await safeClick(explorer, `[data-test="${tableId}"] [aria-label="More"]`); await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Table")'); diff --git a/tsconfig.strict.json b/tsconfig.strict.json index 7a8971ce1..3cd73bf9f 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -60,7 +60,6 @@ "./src/Explorer/Notebook/NotebookUtil.ts", "./src/Explorer/OpenFullScreen.test.tsx", "./src/Explorer/OpenFullScreen.tsx", - "./src/Explorer/Panes/PaneComponents.ts", "./src/Explorer/Panes/PanelFooterComponent.tsx", "./src/Explorer/Panes/PanelInfoErrorComponent.tsx", "./src/Explorer/Panes/PanelLoadingScreen.tsx", From 6e9144b068feccbcd7c417c8f1b42257072011a9 Mon Sep 17 00:00:00 2001 From: Hardikkumar Nai <80053762+hardiknai-techm@users.noreply.github.com> Date: Wed, 19 May 2021 08:27:31 +0530 Subject: [PATCH 4/6] Remove generic right pane component (#790) Co-authored-by: Steve Faulkner --- .../CodeOfConduct.test.tsx} | 10 +- .../CodeOfConduct.tsx} | 8 +- .../CodeOfConduct.test.tsx.snap} | 2 +- .../CodeOfConductComponent.tsx | 123 - .../GalleryViewerComponent.tsx | 4 +- src/Explorer/Explorer.tsx | 8 +- .../NewVertexComponent/NewVertexComponent.tsx | 1 + .../CopyNotebookPane/CopyNotebookPane.tsx | 17 +- .../DeleteCollectionConfirmationPane.test.tsx | 8 +- .../DeleteCollectionConfirmationPane.tsx | 19 +- ...teCollectionConfirmationPane.test.tsx.snap | 4364 ++---- .../DeleteDatabaseConfirmationPanel.test.tsx | 4 +- .../Panes/DeleteDatabaseConfirmationPanel.tsx | 62 +- .../ExecuteSprocParamsPane.tsx | 35 +- .../ExecuteSprocParamsPane.test.tsx.snap | 12386 +++++++--------- .../GenericRightPaneComponent.tsx | 126 - .../Panes/LoadQueryPane/LoadQueryPane.tsx | 36 +- .../__snapshots__/LoadQueryPane.test.tsx.snap | 8 +- .../NewVertexPanel/NewVertexPanel.test.tsx | 6 +- .../Panes/NewVertexPanel/NewVertexPanel.tsx | 32 +- .../NewVertexPanel.test.tsx.snap | 12 +- .../Panes/PanelInfoErrorComponent.tsx | 1 + .../PublishNotebookPane.tsx | 20 +- .../Panes/SaveQueryPane/SaveQueryPane.tsx | 43 +- .../__snapshots__/SaveQueryPane.test.tsx.snap | 8 +- .../Panes/StringInputPane/StringInputPane.tsx | 19 +- .../StringInputPane.test.tsx.snap | 4798 +++--- ...est.tsx => TableQuerySelectPanel.test.tsx} | 2 +- .../{index.tsx => TableQuerySelectPanel.tsx} | 31 +- .../TableQuerySelectPanel.test.tsx.snap | 3007 ++++ .../__snapshots__/index.test.tsx.snap | 4205 ------ .../Panes/UploadFilePane/UploadFilePane.tsx | 4 +- .../Panes/UploadItemsPane/UploadItemsPane.tsx | 4 +- ...eteDatabaseConfirmationPanel.test.tsx.snap | 3210 ++-- test/cassandra/container.spec.ts | 2 +- test/graph/container.spec.ts | 2 +- test/mongo/container.spec.ts | 4 +- test/mongo/container32.spec.ts | 2 +- test/sql/container.spec.ts | 2 +- test/tables/container.spec.ts | 2 +- 40 files changed, 13754 insertions(+), 18883 deletions(-) rename src/Explorer/Controls/NotebookGallery/{CodeOfConductComponent/index.test.tsx => CodeOfConduct/CodeOfConduct.test.tsx} (71%) rename src/Explorer/Controls/NotebookGallery/{CodeOfConductComponent/index.tsx => CodeOfConduct/CodeOfConduct.tsx} (92%) rename src/Explorer/Controls/NotebookGallery/{CodeOfConductComponent/__snapshots__/index.test.tsx.snap => CodeOfConduct/__snapshots__/CodeOfConduct.test.tsx.snap} (96%) delete mode 100644 src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx delete mode 100644 src/Explorer/Panes/GenericRightPaneComponent/GenericRightPaneComponent.tsx rename src/Explorer/Panes/Tables/TableQuerySelectPanel/{index.test.tsx => TableQuerySelectPanel.test.tsx} (95%) rename src/Explorer/Panes/Tables/TableQuerySelectPanel/{index.tsx => TableQuerySelectPanel.tsx} (91%) create mode 100644 src/Explorer/Panes/Tables/TableQuerySelectPanel/__snapshots__/TableQuerySelectPanel.test.tsx.snap delete mode 100644 src/Explorer/Panes/Tables/TableQuerySelectPanel/__snapshots__/index.test.tsx.snap diff --git a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.test.tsx b/src/Explorer/Controls/NotebookGallery/CodeOfConduct/CodeOfConduct.test.tsx similarity index 71% rename from src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.test.tsx rename to src/Explorer/Controls/NotebookGallery/CodeOfConduct/CodeOfConduct.test.tsx index 79a6880a3..e99c0c8c2 100644 --- a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.test.tsx +++ b/src/Explorer/Controls/NotebookGallery/CodeOfConduct/CodeOfConduct.test.tsx @@ -1,12 +1,12 @@ jest.mock("../../../../Juno/JunoClient"); import { shallow } from "enzyme"; import React from "react"; -import { CodeOfConductComponent, CodeOfConductComponentProps } from "."; import { HttpStatusCodes } from "../../../../Common/Constants"; import { JunoClient } from "../../../../Juno/JunoClient"; +import { CodeOfConduct, CodeOfConductProps } from "./CodeOfConduct"; -describe("CodeOfConductComponent", () => { - let codeOfConductProps: CodeOfConductComponentProps; +describe("CodeOfConduct", () => { + let codeOfConductProps: CodeOfConductProps; beforeEach(() => { const junoClient = new JunoClient(); @@ -21,12 +21,12 @@ describe("CodeOfConductComponent", () => { }); it("renders", () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); it("onAcceptedCodeOfConductCalled", async () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.find(".genericPaneSubmitBtn").first().simulate("click"); await Promise.resolve(); expect(codeOfConductProps.onAcceptCodeOfConduct).toBeCalled(); diff --git a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.tsx b/src/Explorer/Controls/NotebookGallery/CodeOfConduct/CodeOfConduct.tsx similarity index 92% rename from src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.tsx rename to src/Explorer/Controls/NotebookGallery/CodeOfConduct/CodeOfConduct.tsx index 6d1d78fb2..2bfee23dc 100644 --- a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.tsx +++ b/src/Explorer/Controls/NotebookGallery/CodeOfConduct/CodeOfConduct.tsx @@ -6,15 +6,15 @@ import { JunoClient } from "../../../../Juno/JunoClient"; import { Action } from "../../../../Shared/Telemetry/TelemetryConstants"; import { trace, traceFailure, traceStart, traceSuccess } from "../../../../Shared/Telemetry/TelemetryProcessor"; -export interface CodeOfConductComponentProps { +export interface CodeOfConductProps { junoClient: JunoClient; onAcceptCodeOfConduct: (result: boolean) => void; } -export const CodeOfConductComponent: FunctionComponent = ({ +export const CodeOfConduct: FunctionComponent = ({ junoClient, onAcceptCodeOfConduct, -}: CodeOfConductComponentProps) => { +}: CodeOfConductProps) => { const descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct"; const descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB."; const descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the "; @@ -47,7 +47,7 @@ export const CodeOfConductComponent: FunctionComponent void; -} - -interface CodeOfConductComponentState { - readCodeOfConduct: boolean; -} - -export class CodeOfConductComponent extends React.Component { - private viewCodeOfConductTraced: boolean; - private descriptionPara1: string; - private descriptionPara2: string; - private descriptionPara3: string; - private link1: { label: string; url: string }; - - constructor(props: CodeOfConductComponentProps) { - super(props); - - this.state = { - readCodeOfConduct: false, - }; - - this.descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct"; - this.descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB."; - this.descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the "; - this.link1 = { label: "code of conduct.", url: CodeOfConductEndpoints.codeOfConduct }; - } - - private async acceptCodeOfConduct(): Promise { - const startKey = traceStart(Action.NotebooksGalleryAcceptCodeOfConduct); - - try { - const response = await this.props.junoClient.acceptCodeOfConduct(); - if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { - throw new Error(`Received HTTP ${response.status} when accepting code of conduct`); - } - - traceSuccess(Action.NotebooksGalleryAcceptCodeOfConduct, {}, startKey); - - this.props.onAcceptCodeOfConduct(response.data); - } catch (error) { - traceFailure( - Action.NotebooksGalleryAcceptCodeOfConduct, - { - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }, - startKey - ); - - handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct"); - } - } - - private onChangeCheckbox = (): void => { - this.setState({ readCodeOfConduct: !this.state.readCodeOfConduct }); - }; - - public render(): JSX.Element { - if (!this.viewCodeOfConductTraced) { - this.viewCodeOfConductTraced = true; - trace(Action.NotebooksGalleryViewCodeOfConduct); - } - - return ( - - - {this.descriptionPara1} - - - - {this.descriptionPara2} - - - - - {this.descriptionPara3} - - {this.link1.label} - - - - - - - - - - await this.acceptCodeOfConduct()} - tabIndex={0} - className="genericPaneSubmitBtn" - text="Continue" - disabled={!this.state.readCodeOfConduct} - /> - - - ); - } -} diff --git a/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx b/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx index f1733a141..e26ef58e0 100644 --- a/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx +++ b/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx @@ -30,7 +30,7 @@ import * as GalleryUtils from "../../../Utils/GalleryUtils"; import Explorer from "../../Explorer"; import { Dialog, DialogProps } from "../Dialog"; import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent"; -import { CodeOfConductComponent } from "./CodeOfConductComponent"; +import { CodeOfConduct } from "./CodeOfConduct/CodeOfConduct"; import "./GalleryViewerComponent.less"; import { InfoComponent } from "./InfoComponent/InfoComponent"; @@ -372,7 +372,7 @@ export class GalleryViewerComponent extends React.Component
- { this.setState({ isCodeOfConductAccepted: result }); diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index a8d9fa7d7..ba8f563c4 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -67,7 +67,7 @@ import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksP import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; import { AddTableEntityPanel } from "./Panes/Tables/AddTableEntityPanel"; import { EditTableEntityPanel } from "./Panes/Tables/EditTableEntityPanel"; -import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel"; +import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel"; import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; import TableListViewModal from "./Tables/DataTable/TableEntityListViewModel"; @@ -1379,7 +1379,7 @@ export default class Explorer { this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); } else { this.openSidePanel( - "", + "Rename Notebook", { @@ -1410,7 +1410,7 @@ export default class Explorer { } this.openSidePanel( - "", + "Create new directory", { @@ -1902,7 +1902,7 @@ export default class Explorer { "Delete " + getDatabaseName(), this.expandConsole()} closePanel={this.closeSidePanel} selectedDatabase={this.findSelectedDatabase()} /> diff --git a/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx b/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx index d3011155a..fce3d43e7 100644 --- a/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx +++ b/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx @@ -132,6 +132,7 @@ export const NewVertexComponent: FunctionComponent = ( onChange={(event: React.ChangeEvent) => { onLabelChange(event); }} + autoFocus />
diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx index 41b52a189..8026401b6 100644 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx +++ b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx @@ -9,10 +9,7 @@ import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUti import Explorer from "../../Explorer"; import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem"; import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter"; -import { - GenericRightPaneComponent, - GenericRightPaneProps, -} from "../GenericRightPaneComponent/GenericRightPaneComponent"; +import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent"; interface Location { @@ -42,7 +39,6 @@ export const CopyNotebookPane: FunctionComponent = ({ }: CopyNotebookPanelProps) => { const [isExecuting, setIsExecuting] = useState(); const [formError, setFormError] = useState(""); - const [formErrorDetail, setFormErrorDetail] = useState(""); const [pinnedRepos, setPinnedRepos] = useState(); const [selectedLocation, setSelectedLocation] = useState(); @@ -92,7 +88,6 @@ export const CopyNotebookPane: FunctionComponent = ({ } catch (error) { const errorMessage = getErrorMessage(error); setFormError(`Failed to copy ${name} to ${destination}`); - setFormErrorDetail(`${errorMessage}`); handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError); } finally { clearMessage && clearMessage(); @@ -130,14 +125,10 @@ export const CopyNotebookPane: FunctionComponent = ({ setSelectedLocation(option?.data); }; - const genericPaneProps: GenericRightPaneProps = { + const props: RightPaneFormProps = { formError, - formErrorDetail, - id: "copynotebookpane", isExecuting: isExecuting, - title: "Copy notebook", submitButtonText: "OK", - onClose: closePanel, onSubmit: () => submit(), expandConsole: () => container.expandConsole(), }; @@ -149,8 +140,8 @@ export const CopyNotebookPane: FunctionComponent = ({ }; return ( - + - + ); }; diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx index 619650451..c75f998e3 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx @@ -130,8 +130,8 @@ describe("Delete Collection Confirmation Pane", () => { .hostNodes() .simulate("change", { target: { value: selectedCollectionId } }); - expect(wrapper.exists(".genericPaneSubmitBtn")).toBe(true); - wrapper.find(".genericPaneSubmitBtn").hostNodes().simulate("click"); + expect(wrapper.exists("#sidePanelOkButton")).toBe(true); + wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit"); expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId); wrapper.unmount(); @@ -151,8 +151,8 @@ describe("Delete Collection Confirmation Pane", () => { .hostNodes() .simulate("change", { target: { value: feedbackText } }); - expect(wrapper.exists(".genericPaneSubmitBtn")).toBe(true); - wrapper.find(".genericPaneSubmitBtn").hostNodes().simulate("click"); + expect(wrapper.exists("#sidePanelOkButton")).toBe(true); + wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit"); expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId); const deleteFeedback = new DeleteFeedback( diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx index 1fdc2488b..5fe8c76f5 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx @@ -12,10 +12,7 @@ import { userContext } from "../../../UserContext"; import { getCollectionName } from "../../../Utils/APITypeUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import Explorer from "../../Explorer"; -import { - GenericRightPaneComponent, - GenericRightPaneProps, -} from "../GenericRightPaneComponent/GenericRightPaneComponent"; +import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; export interface DeleteCollectionConfirmationPaneProps { explorer: Explorer; closePanel: () => void; @@ -35,7 +32,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent => { + const onSubmit = async (): Promise => { const collection = explorer.findSelectedCollection(); if (!collection || inputCollectionName !== collection.id()) { const errorMessage = "Input " + collectionName + " name does not match the selected " + collectionName; @@ -100,19 +97,15 @@ export const DeleteCollectionConfirmationPane: FunctionComponent explorer.expandConsole(), }; return ( - +
@@ -150,6 +143,6 @@ export const DeleteCollectionConfirmationPane: FunctionComponent
- + ); }; diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap index 2cf3a5962..0157faed6 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap @@ -15,70 +15,62 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect } } > - -
-
- Delete container + * - + + Confirm by typing the + container + id + + + - - *": Object { - "left": 0, - "position": "relative", - "top": 0, - }, - }, - "textAlign": "center", - "textDecoration": "none", - "userSelect": "none", - }, - Object { - "backgroundColor": "transparent", - "border": "none", - "color": "#0078d4", - "height": "32px", - "padding": "0 4px", - "width": "32px", - }, - ], - "rootChecked": Object { - "backgroundColor": "#edebe9", - "color": "#005a9e", - }, - "rootCheckedHovered": Object { - "backgroundColor": "#e1dfdd", - "color": "#005a9e", - }, - "rootDisabled": Array [ - Object { - "outline": "transparent", - "position": "relative", - "selectors": Object { - ".ms-Fabric--isFocusVisible &:focus:after": Object { - "border": "1px solid transparent", - "bottom": 2, - "content": "\\"\\"", - "left": 2, - "outline": "1px solid #605e5c", - "position": "absolute", - "right": 2, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "bottom": -2, - "left": -2, - "outlineColor": "ButtonText", - "right": -2, - "top": -2, - }, - }, - "top": 2, - "zIndex": 1, - }, - "::-moz-focus-inner": Object { - "border": "0", - }, - }, - }, - Object { - "backgroundColor": "#f3f2f1", - "borderColor": "#f3f2f1", - "color": "#a19f9d", - "cursor": "default", - "selectors": Object { - ":focus": Object { - "outline": 0, - }, - ":hover": Object { - "outline": 0, - }, - }, - }, - Object { - "color": "#c8c6c4", - }, - ], - "rootExpanded": Object { - "backgroundColor": "#edebe9", - "color": "#005a9e", - }, - "rootHasMenu": Object { - "width": "auto", - }, - "rootHovered": Object { - "backgroundColor": "#f3f2f1", - "color": "#106ebe", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "borderColor": "Highlight", - "color": "Highlight", - }, - }, - }, - "rootPressed": Object { - "backgroundColor": "#edebe9", - "color": "#005a9e", - }, - "screenReaderText": Object { - "border": 0, - "height": 1, - "margin": -1, - "overflow": "hidden", - "padding": 0, - "position": "absolute", - "width": 1, - }, - "splitButtonContainer": Array [ - Object { - "outline": "transparent", - "position": "relative", - "selectors": Object { - ".ms-Fabric--isFocusVisible &:focus:after": Object { - "border": "1px solid #ffffff", - "bottom": 3, - "content": "\\"\\"", - "left": 3, - "outline": "1px solid #605e5c", - "position": "absolute", - "right": 3, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "border": "none", - "bottom": -2, - "left": -2, - "right": -2, - "top": -2, - }, - }, - "top": 3, - "zIndex": 1, - }, - "::-moz-focus-inner": Object { - "border": "0", - }, - }, - }, - Object { - "display": "inline-flex", - "selectors": Object { - ".ms-Button--default": Object { - "borderBottomRightRadius": "0", - "borderRight": "none", - "borderTopRightRadius": "0", - }, - ".ms-Button--primary": Object { - "border": "none", - "borderBottomRightRadius": "0", - "borderTopRightRadius": "0", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "MsHighContrastAdjust": "none", - "backgroundColor": "Window", - "border": "1px solid WindowText", - "borderRightWidth": "0", - "color": "WindowText", - "forcedColorAdjust": "none", - }, - }, - }, - ".ms-Button--primary + .ms-Button": Object { - "border": "none", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "border": "1px solid WindowText", - "borderLeftWidth": "0", - }, - }, - }, - }, - }, - ], - "splitButtonContainerChecked": Object { - "selectors": Object { - ".ms-Button--primary": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "MsHighContrastAdjust": "none", - "backgroundColor": "WindowText", - "color": "Window", - "forcedColorAdjust": "none", - }, - }, - }, - }, - }, - "splitButtonContainerCheckedHovered": Object { - "selectors": Object { - ".ms-Button--primary": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "MsHighContrastAdjust": "none", - "backgroundColor": "WindowText", - "color": "Window", - "forcedColorAdjust": "none", - }, - }, - }, - }, - }, - "splitButtonContainerDisabled": Object { - "border": "none", - "outline": "none", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "MsHighContrastAdjust": "none", - "backgroundColor": "Window", - "borderColor": "GrayText", - "color": "GrayText", - "forcedColorAdjust": "none", - }, - }, - }, - "splitButtonContainerFocused": Object { - "outline": "none!important", - }, - "splitButtonContainerHovered": Object { - "selectors": Object { - ".ms-Button--primary": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "Highlight", - "color": "Window", - }, - }, - }, - ".ms-Button.is-disabled": Object { - "color": "#a19f9d", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "Window", - "borderColor": "GrayText", - "color": "GrayText", - }, - }, - }, - }, - }, - "splitButtonDivider": Object { - "bottom": 8, - "position": "absolute", - "right": 31, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "WindowText", - }, - }, - "top": 8, - "width": 1, - }, - "splitButtonDividerDisabled": Object { - "bottom": 8, - "position": "absolute", - "right": 31, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "GrayText", - }, - }, - "top": 8, - "width": 1, - }, - "splitButtonFlexContainer": Object { - "alignItems": "center", - "display": "flex", - "flexWrap": "nowrap", - "height": "100%", - "justifyContent": "center", - }, - "splitButtonMenuButton": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - ".ms-Button-menuIcon": Object { - "color": "WindowText", - }, - }, - "border": "1px solid #8a8886", - "borderBottomRightRadius": "2px", - "borderLeft": "none", - "borderRadius": 0, - "borderTopRightRadius": "2px", - "boxSizing": "border-box", - "cursor": "pointer", - "display": "inline-block", - "height": "auto", - "marginBottom": 0, - "marginLeft": -1, - "marginRight": 0, - "marginTop": 0, - "outline": "transparent", - "padding": 6, - "textAlign": "center", - "textDecoration": "none", - "userSelect": "none", - "verticalAlign": "top", - "width": 32, - }, - "splitButtonMenuButtonDisabled": Object { - "border": "none", - "pointerEvents": "none", - "selectors": Object { - ".ms-Button--primary": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "Window", - "borderColor": "GrayText", - "color": "GrayText", - }, - }, - }, - ".ms-Button-menuIcon": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "color": "GrayText", - }, - }, - }, - ":hover": Object { - "cursor": "default", - }, - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "Window", - "border": "1px solid GrayText", - "color": "GrayText", - }, - }, - }, - "splitButtonMenuFocused": Object { - "outline": "transparent", - "position": "relative", - "selectors": Object { - ".ms-Fabric--isFocusVisible &:focus:after": Object { - "border": "1px solid #ffffff", - "bottom": 3, - "content": "\\"\\"", - "left": 3, - "outline": "1px solid #605e5c", - "position": "absolute", - "right": 3, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "border": "none", - "bottom": -2, - "left": -2, - "right": -2, - "top": -2, - }, - }, - "top": 3, - "zIndex": 1, - }, - "::-moz-focus-inner": Object { - "border": "0", - }, - }, - }, - "textContainer": Object { - "display": "block", - "flexGrow": 1, - }, - } - } - tabIndex={0} - theme={ - Object { - "disableGlobalClassNames": false, - "effects": Object { - "elevation16": "0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132), 0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108)", - "elevation4": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)", - "elevation64": "0 25.6px 57.6px 0 rgba(0, 0, 0, 0.22), 0 4.8px 14.4px 0 rgba(0, 0, 0, 0.18)", - "elevation8": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)", - "roundedCorner2": "2px", - "roundedCorner4": "4px", - "roundedCorner6": "6px", - }, - "fonts": Object { - "large": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "18px", - "fontWeight": 400, - }, - "medium": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "14px", - "fontWeight": 400, - }, - "mediumPlus": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "16px", - "fontWeight": 400, - }, - "mega": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "68px", - "fontWeight": 600, - }, - "small": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "12px", - "fontWeight": 400, - }, - "smallPlus": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "12px", - "fontWeight": 400, - }, - "superLarge": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "42px", - "fontWeight": 600, - }, - "tiny": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "10px", - "fontWeight": 400, - }, - "xLarge": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "20px", - "fontWeight": 600, - }, - "xLargePlus": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "24px", - "fontWeight": 600, - }, - "xSmall": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "10px", - "fontWeight": 400, - }, - "xxLarge": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "28px", - "fontWeight": 600, - }, - "xxLargePlus": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "32px", - "fontWeight": 600, - }, - }, - "isInverted": false, - "palette": Object { - "accent": "#0078d4", - "black": "#000000", - "blackTranslucent40": "rgba(0,0,0,.4)", - "blue": "#0078d4", - "blueDark": "#002050", - "blueLight": "#00bcf2", - "blueMid": "#00188f", - "green": "#107c10", - "greenDark": "#004b1c", - "greenLight": "#bad80a", - "magenta": "#b4009e", - "magentaDark": "#5c005c", - "magentaLight": "#e3008c", - "neutralDark": "#201f1e", - "neutralLight": "#edebe9", - "neutralLighter": "#f3f2f1", - "neutralLighterAlt": "#faf9f8", - "neutralPrimary": "#323130", - "neutralPrimaryAlt": "#3b3a39", - "neutralQuaternary": "#d2d0ce", - "neutralQuaternaryAlt": "#e1dfdd", - "neutralSecondary": "#605e5c", - "neutralSecondaryAlt": "#8a8886", - "neutralTertiary": "#a19f9d", - "neutralTertiaryAlt": "#c8c6c4", - "orange": "#d83b01", - "orangeLight": "#ea4300", - "orangeLighter": "#ff8c00", - "purple": "#5c2d91", - "purpleDark": "#32145a", - "purpleLight": "#b4a0ff", - "red": "#e81123", - "redDark": "#a4262c", - "teal": "#008272", - "tealDark": "#004b50", - "tealLight": "#00b294", - "themeDark": "#005a9e", - "themeDarkAlt": "#106ebe", - "themeDarker": "#004578", - "themeLight": "#c7e0f4", - "themeLighter": "#deecf9", - "themeLighterAlt": "#eff6fc", - "themePrimary": "#0078d4", - "themeSecondary": "#2b88d8", - "themeTertiary": "#71afe5", - "white": "#ffffff", - "whiteTranslucent40": "rgba(255,255,255,.4)", - "yellow": "#ffb900", - "yellowDark": "#d29200", - "yellowLight": "#fff100", - }, - "rtl": undefined, - "semanticColors": Object { - "accentButtonBackground": "#0078d4", - "accentButtonText": "#ffffff", - "actionLink": "#323130", - "actionLinkHovered": "#201f1e", - "blockingBackground": "#FDE7E9", - "blockingIcon": "#FDE7E9", - "bodyBackground": "#ffffff", - "bodyBackgroundChecked": "#edebe9", - "bodyBackgroundHovered": "#f3f2f1", - "bodyDivider": "#edebe9", - "bodyFrameBackground": "#ffffff", - "bodyFrameDivider": "#edebe9", - "bodyStandoutBackground": "#faf9f8", - "bodySubtext": "#605e5c", - "bodyText": "#323130", - "bodyTextChecked": "#000000", - "buttonBackground": "#ffffff", - "buttonBackgroundChecked": "#c8c6c4", - "buttonBackgroundCheckedHovered": "#edebe9", - "buttonBackgroundDisabled": "#f3f2f1", - "buttonBackgroundHovered": "#f3f2f1", - "buttonBackgroundPressed": "#edebe9", - "buttonBorder": "#8a8886", - "buttonBorderDisabled": "#f3f2f1", - "buttonText": "#323130", - "buttonTextChecked": "#201f1e", - "buttonTextCheckedHovered": "#000000", - "buttonTextDisabled": "#a19f9d", - "buttonTextHovered": "#201f1e", - "buttonTextPressed": "#201f1e", - "cardShadow": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)", - "cardShadowHovered": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)", - "cardStandoutBackground": "#ffffff", - "defaultStateBackground": "#faf9f8", - "disabledBackground": "#f3f2f1", - "disabledBodySubtext": "#c8c6c4", - "disabledBodyText": "#a19f9d", - "disabledBorder": "#c8c6c4", - "disabledSubtext": "#d2d0ce", - "disabledText": "#a19f9d", - "errorBackground": "#FDE7E9", - "errorIcon": "#A80000", - "errorText": "#a4262c", - "focusBorder": "#605e5c", - "infoBackground": "#f3f2f1", - "infoIcon": "#605e5c", - "inputBackground": "#ffffff", - "inputBackgroundChecked": "#0078d4", - "inputBackgroundCheckedHovered": "#005a9e", - "inputBorder": "#605e5c", - "inputBorderHovered": "#323130", - "inputFocusBorderAlt": "#0078d4", - "inputForegroundChecked": "#ffffff", - "inputIcon": "#0078d4", - "inputIconDisabled": "#a19f9d", - "inputIconHovered": "#005a9e", - "inputPlaceholderBackgroundChecked": "#deecf9", - "inputPlaceholderText": "#605e5c", - "inputText": "#323130", - "inputTextHovered": "#201f1e", - "link": "#0078d4", - "linkHovered": "#004578", - "listBackground": "#ffffff", - "listHeaderBackgroundHovered": "#f3f2f1", - "listHeaderBackgroundPressed": "#edebe9", - "listItemBackgroundChecked": "#edebe9", - "listItemBackgroundCheckedHovered": "#e1dfdd", - "listItemBackgroundHovered": "#f3f2f1", - "listText": "#323130", - "listTextColor": "#323130", - "menuBackground": "#ffffff", - "menuDivider": "#c8c6c4", - "menuHeader": "#0078d4", - "menuIcon": "#0078d4", - "menuItemBackgroundChecked": "#edebe9", - "menuItemBackgroundHovered": "#f3f2f1", - "menuItemBackgroundPressed": "#edebe9", - "menuItemText": "#323130", - "menuItemTextHovered": "#201f1e", - "messageLink": "#005A9E", - "messageLinkHovered": "#004578", - "messageText": "#323130", - "primaryButtonBackground": "#0078d4", - "primaryButtonBackgroundDisabled": "#f3f2f1", - "primaryButtonBackgroundHovered": "#106ebe", - "primaryButtonBackgroundPressed": "#005a9e", - "primaryButtonBorder": "transparent", - "primaryButtonText": "#ffffff", - "primaryButtonTextDisabled": "#d2d0ce", - "primaryButtonTextHovered": "#ffffff", - "primaryButtonTextPressed": "#ffffff", - "severeWarningBackground": "#FED9CC", - "severeWarningIcon": "#D83B01", - "smallInputBorder": "#605e5c", - "successBackground": "#DFF6DD", - "successIcon": "#107C10", - "successText": "#107C10", - "variantBorder": "#edebe9", - "variantBorderHovered": "#a19f9d", - "warningBackground": "#FFF4CE", - "warningHighlight": "#ffb900", - "warningIcon": "#797775", - "warningText": "#323130", - }, - "spacing": Object { - "l1": "20px", - "l2": "32px", - "m": "16px", - "s1": "8px", - "s2": "4px", - }, - } - } - title="Close pane" - variantClassName="ms-Button--icon" +
- - - - - + +
+
+
+ +
-
-
+ -
- - * - - - - Confirm by typing the - container - id - - - - -
-
-
- -
-
-
-
-
-
-
- - - Help us improve Azure Cosmos DB! - - - - - What is the reason why you are deleting this - container - ? - - - - -
-
-
-