From 65882ea8316010ba89a0e38565e7e5f5c521f7f9 Mon Sep 17 00:00:00 2001 From: Meha Kaushik Date: Tue, 7 Sep 2021 23:42:39 -0700 Subject: [PATCH 01/40] Self-Server for GraphAPI Compute (#1017) * Self-Server for GraphAPI Compute * Update GraphAPICompute.json --- src/Localization/en/GraphAPICompute.json | 57 +++ .../GraphAPICompute/GraphAPICompute.rp.ts | 200 +++++++++ .../GraphAPICompute/GraphAPICompute.tsx | 423 ++++++++++++++++++ .../GraphAPICompute/GraphAPICompute.types.ts | 65 +++ src/SelfServe/SelfServe.tsx | 8 + src/SelfServe/SelfServeUtils.tsx | 1 + 6 files changed, 754 insertions(+) create mode 100644 src/Localization/en/GraphAPICompute.json create mode 100644 src/SelfServe/GraphAPICompute/GraphAPICompute.rp.ts create mode 100644 src/SelfServe/GraphAPICompute/GraphAPICompute.tsx create mode 100644 src/SelfServe/GraphAPICompute/GraphAPICompute.types.ts diff --git a/src/Localization/en/GraphAPICompute.json b/src/Localization/en/GraphAPICompute.json new file mode 100644 index 000000000..a55102f37 --- /dev/null +++ b/src/Localization/en/GraphAPICompute.json @@ -0,0 +1,57 @@ +{ + "GraphAPIDescription": "Provision a Graph API Compute for your Azure Cosmos DB account.", + "GraphAPICompute": "Graph GraphAPI Compute", + "Provisioned": "Provisioned", + "Deprovisioned": "Deprovisioned", + "Compute": "Compute", + "GremlinV2": "GremlinV2", + "LearnAboutCompute": "Learn more about GraphAPI Compute.", + "DeprovisioningDetailsText": "Learn more about deprovisioning the GraphAPI Compute.", + "ComputePricing": "Learn more about GraphAPI Compute pricing.", + "SKUs": "SKUs", + "SKUsPlaceHolder": "Select SKUs", + "NumberOfInstances": "Number of instances", + "CosmosD4s": "Cosmos.D4s (General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory)", + "CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)", + "CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)", + "CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)", + "CreateMessage": "Graph GraphAPI Compute resource is being created.", + "CreateInitializeTitle": "Provisioning resource", + "CreateInitializeMessage": "GraphAPI Compute resource will be provisioned.", + "CreateSuccessTitle": "Resource provisioned", + "CreateSuccesseMessage": "GraphAPI Compute resource provisioned.", + "CreateFailureTitle": "Failed to provision resource", + "CreateFailureMessage": "GraphAPI Compute resource provisioning failed.", + "UpdateMessage": "GraphAPI Compute resource is being updated.", + "UpdateInitializeTitle": "Updating resource", + "UpdateInitializeMessage": "GraphAPI Compute resource will be updated.", + "UpdateSuccessTitle": "Resource updated", + "UpdateSuccesseMessage": "GraphAPI Compute resource updated.", + "UpdateFailureTitle": "Failed to update resource", + "UpdateFailureMessage": "GraphAPI Compute resource updation failed.", + "DeleteMessage": "GraphAPI Compute resource is being deleted.", + "DeleteInitializeTitle": "Deleting resource", + "DeleteInitializeMessage": "GraphAPI Compute resource will be deleted.", + "DeleteSuccessTitle": "Resource deleted", + "DeleteSuccesseMessage": "GraphAPI Compute resource deleted.", + "DeleteFailureTitle": "Failed to delete resource", + "DeleteFailureMessage": "GraphAPI Compute resource deletion failed.", + "CannotSave": "Cannot save the changes to the GraphAPI Compute resource at the moment.", + "GraphAccountEndpoint": "Graph Account Endpoint", + "CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory", + "CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory", + "CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory", + "ApproximateCost": "Approximate Cost Per Hour", + "CostText": "Hourly cost of the GraphAPI Compute resource depends on the SKU selection, number of instances per region, and number of regions.", + "ConnectionString": "Connection String", + "ConnectionStringText": "To use the GraphAPI Compute, use the connection string shown in ", + "KeysBlade": "the keys blade.", + "MetricsString": "Metrics", + "MetricsText": "Monitor the CPU and memory usage for the GraphAPI Compute instances in ", + "MetricsBlade": "the metrics blade.", + "MonitorUsage": "Monitor Usage", + "ResizingDecisionText": "Number of instances has to be 1 during provisioning. Instances can only be incremented by 1 at once. ", + "ResizingDecisionLink": "Learn more about GraphAPI Compute sizing.", + "WarningBannerOnUpdate": "Adding or modifying GraphAPI Compute instances may affect your bill.", + "WarningBannerOnDelete": "After deprovisioning the GraphAPI Compute, you will not be able to connect to the Graph API account." +} diff --git a/src/SelfServe/GraphAPICompute/GraphAPICompute.rp.ts b/src/SelfServe/GraphAPICompute/GraphAPICompute.rp.ts new file mode 100644 index 000000000..a80ebff4e --- /dev/null +++ b/src/SelfServe/GraphAPICompute/GraphAPICompute.rp.ts @@ -0,0 +1,200 @@ +import { configContext } from "../../ConfigContext"; +import { userContext } from "../../UserContext"; +import { armRequestWithoutPolling } from "../../Utils/arm/request"; +import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor"; +import { RefreshResult } from "../SelfServeTypes"; +import GraphAPICompute from "./GraphAPICompute"; +import { + FetchPricesResponse, + RegionsResponse, + GraphAPIComputeServiceResource, + UpdateComputeRequestParameters, +} from "./GraphAPICompute.types"; + +const apiVersion = "2021-04-01-preview"; +const gremlinV2 = "GremlinV2"; + +export enum ResourceStatus { + Running = "Running", + Creating = "Creating", + Updating = "Updating", + Deleting = "Deleting", +} + +export interface ComputeResponse { + sku: string; + instances: number; + status: string; + endpoint: string; +} + +export const getPath = (subscriptionId: string, resourceGroup: string, name: string): string => { + return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}/services/${gremlinV2}`; +}; + +export const updateComputeResource = async (sku: string, instances: number): Promise => { + const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); + const body: UpdateComputeRequestParameters = { + properties: { + instanceSize: sku, + instanceCount: instances, + serviceType: gremlinV2, + }, + }; + const telemetryData = { ...body, httpMethod: "PUT", selfServeClassName: GraphAPICompute.name }; + const updateTimeStamp = selfServeTraceStart(telemetryData); + let armRequestResult; + try { + armRequestResult = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path, + method: "PUT", + apiVersion, + body, + }); + selfServeTraceSuccess(telemetryData, updateTimeStamp); + } catch (e) { + const failureTelemetry = { ...body, e, selfServeClassName: GraphAPICompute.name }; + selfServeTraceFailure(failureTelemetry, updateTimeStamp); + throw e; + } + return armRequestResult?.operationStatusUrl; +}; + +export const deleteComputeResource = async (): Promise => { + const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); + const telemetryData = { httpMethod: "DELETE", selfServeClassName: GraphAPICompute.name }; + const deleteTimeStamp = selfServeTraceStart(telemetryData); + let armRequestResult; + try { + armRequestResult = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path, + method: "DELETE", + apiVersion, + }); + selfServeTraceSuccess(telemetryData, deleteTimeStamp); + } catch (e) { + const failureTelemetry = { e, selfServeClassName: GraphAPICompute.name }; + selfServeTraceFailure(failureTelemetry, deleteTimeStamp); + throw e; + } + return armRequestResult?.operationStatusUrl; +}; + +export const getComputeResource = async (): Promise => { + const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); + const telemetryData = { httpMethod: "GET", selfServeClassName: GraphAPICompute.name }; + const getResourceTimeStamp = selfServeTraceStart(telemetryData); + let armRequestResult; + try { + armRequestResult = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path, + method: "GET", + apiVersion, + }); + selfServeTraceSuccess(telemetryData, getResourceTimeStamp); + } catch (e) { + const failureTelemetry = { e, selfServeClassName: GraphAPICompute.name }; + selfServeTraceFailure(failureTelemetry, getResourceTimeStamp); + throw e; + } + return armRequestResult?.result; +}; + +export const getCurrentProvisioningState = async (): Promise => { + try { + const response = await getComputeResource(); + return { + sku: response.properties.instanceSize, + instances: response.properties.instanceCount, + status: response.properties.status, + endpoint: response.properties.GraphAPIComputeEndPoint, + }; + } catch (e) { + return { sku: undefined, instances: undefined, status: undefined, endpoint: undefined }; + } +}; + +export const refreshComputeProvisioning = async (): Promise => { + try { + const response = await getComputeResource(); + if (response.properties.status === ResourceStatus.Running.toString()) { + return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined }; + } else if (response.properties.status === ResourceStatus.Creating.toString()) { + return { isUpdateInProgress: true, updateInProgressMessageTKey: "CreateMessage" }; + } else if (response.properties.status === ResourceStatus.Deleting.toString()) { + return { isUpdateInProgress: true, updateInProgressMessageTKey: "DeleteMessage" }; + } else { + return { isUpdateInProgress: true, updateInProgressMessageTKey: "UpdateMessage" }; + } + } catch { + //TODO differentiate between different failures + return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined }; + } +}; + +const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: string): string => { + return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`; +}; + +export const getReadRegions = async (): Promise> => { + try { + const readRegions = new Array(); + + const response = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path: getGeneralPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name), + method: "GET", + apiVersion: "2021-04-01-preview", + }); + + if (response.result.location !== undefined) { + readRegions.push(response.result.location.replace(" ", "").toLowerCase()); + } else { + for (const location of response.result.locations) { + readRegions.push(location.locationName.replace(" ", "").toLowerCase()); + } + } + return readRegions; + } catch (err) { + return new Array(); + } +}; + +const getFetchPricesPathForRegion = (subscriptionId: string): string => { + return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`; +}; + +export const getPriceMap = async (regions: Array): Promise>> => { + try { + const priceMap = new Map>(); + + for (const region of regions) { + const regionPriceMap = new Map(); + + const response = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path: getFetchPricesPathForRegion(userContext.subscriptionId), + method: "POST", + apiVersion: "2020-01-01-preview", + queryParams: { + filter: + "armRegionName eq '" + + region + + "' and serviceFamily eq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'", + }, + }); + + for (const item of response.result.Items) { + regionPriceMap.set(item.skuName, item.retailPrice); + } + priceMap.set(region, regionPriceMap); + } + + return priceMap; + } catch (err) { + return undefined; + } +}; diff --git a/src/SelfServe/GraphAPICompute/GraphAPICompute.tsx b/src/SelfServe/GraphAPICompute/GraphAPICompute.tsx new file mode 100644 index 000000000..677395838 --- /dev/null +++ b/src/SelfServe/GraphAPICompute/GraphAPICompute.tsx @@ -0,0 +1,423 @@ +import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators"; +import { selfServeTrace } from "../SelfServeTelemetryProcessor"; +import { + ChoiceItem, + Description, + DescriptionType, + Info, + InputType, + NumberUiType, + OnSaveResult, + RefreshResult, + SelfServeBaseClass, + SmartUiInput, +} from "../SelfServeTypes"; +import { BladeType, generateBladeLink } from "../SelfServeUtils"; +import { + deleteComputeResource, + getCurrentProvisioningState, + getPriceMap, + getReadRegions, + refreshComputeProvisioning, + updateComputeResource, +} from "./GraphAPICompute.rp"; + +const costPerHourDefaultValue: Description = { + textTKey: "CostText", + type: DescriptionType.Text, + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", + textTKey: "ComputePricing", + }, +}; + +const connectionStringValue: Description = { + textTKey: "ConnectionStringText", + type: DescriptionType.Text, + link: { + href: generateBladeLink(BladeType.SqlKeys), + textTKey: "KeysBlade", + }, +}; + +const metricsStringValue: Description = { + textTKey: "MetricsText", + type: DescriptionType.Text, + link: { + href: generateBladeLink(BladeType.Metrics), + textTKey: "MetricsBlade", + }, +}; + +const CosmosD4s = "Cosmos.D4s"; +const CosmosD8s = "Cosmos.D8s"; +const CosmosD16s = "Cosmos.D16s"; + +const onSKUChange = (newValue: InputType, currentValues: Map): Map => { + currentValues.set("sku", { value: newValue }); + currentValues.set("costPerHour", { + value: calculateCost(newValue as string, currentValues.get("instances").value as number), + }); + + return currentValues; +}; + +const onNumberOfInstancesChange = ( + newValue: InputType, + currentValues: Map, + baselineValues: Map +): Map => { + currentValues.set("instances", { value: newValue }); + const ComputeOriginallyEnabled = baselineValues.get("enableCompute")?.value as boolean; + const baselineInstances = baselineValues.get("instances")?.value as number; + if (!ComputeOriginallyEnabled || baselineInstances !== newValue) { + currentValues.set("warningBanner", { + value: { + textTKey: "WarningBannerOnUpdate", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", + textTKey: "ComputePricing", + }, + } as Description, + hidden: false, + }); + } else { + currentValues.set("warningBanner", undefined); + } + + currentValues.set("costPerHour", { + value: calculateCost(currentValues.get("sku").value as string, newValue as number), + }); + + return currentValues; +}; + +const onEnableComputeChange = ( + newValue: InputType, + currentValues: Map, + baselineValues: ReadonlyMap +): Map => { + currentValues.set("enableCompute", { value: newValue }); + const ComputeOriginallyEnabled = baselineValues.get("enableCompute")?.value as boolean; + if (ComputeOriginallyEnabled === newValue) { + currentValues.set("sku", baselineValues.get("sku")); + currentValues.set("instances", baselineValues.get("instances")); + currentValues.set("costPerHour", baselineValues.get("costPerHour")); + currentValues.set("warningBanner", baselineValues.get("warningBanner")); + currentValues.set("connectionString", baselineValues.get("connectionString")); + currentValues.set("metricsString", baselineValues.get("metricsString")); + return currentValues; + } + + currentValues.set("warningBanner", undefined); + if (newValue === true) { + currentValues.set("warningBanner", { + value: { + textTKey: "WarningBannerOnUpdate", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", //needs updating + textTKey: "ComputePricing", + }, + } as Description, + hidden: false, + }); + + currentValues.set("costPerHour", { + value: calculateCost(baselineValues.get("sku").value as string, baselineValues.get("instances").value as number), + hidden: false, + }); + } else { + currentValues.set("warningBanner", { + value: { + textTKey: "WarningBannerOnDelete", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", // needs updating + textTKey: "DeprovisioningDetailsText", + }, + } as Description, + hidden: false, + }); + + currentValues.set("costPerHour", { value: costPerHourDefaultValue, hidden: true }); + } + const sku = currentValues.get("sku"); + const hideAttributes = newValue === undefined || !(newValue as boolean); + currentValues.set("sku", { + value: sku.value, + hidden: hideAttributes, + disabled: ComputeOriginallyEnabled, + }); + currentValues.set("instances", { + value: 1, + hidden: hideAttributes, + disabled: true, + }); + + currentValues.set("connectionString", { + value: connectionStringValue, + hidden: !newValue || !ComputeOriginallyEnabled, + }); + + currentValues.set("metricsString", { + value: metricsStringValue, + hidden: !newValue || !ComputeOriginallyEnabled, + }); + + return currentValues; +}; + +const skuDropDownItems: ChoiceItem[] = [ + { labelTKey: "CosmosD4s", key: CosmosD4s }, + { labelTKey: "CosmosD8s", key: CosmosD8s }, + { labelTKey: "CosmosD16s", key: CosmosD16s }, +]; + +const getSkus = async (): Promise => { + return skuDropDownItems; +}; + +const NumberOfInstancesDropdownInfo: Info = { + messageTKey: "ResizingDecisionText", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-size", // todo + textTKey: "ResizingDecisionLink", + }, +}; + +const getInstancesMin = async (): Promise => { + return 1; +}; + +const getInstancesMax = async (): Promise => { + return 5; +}; + +const ApproximateCostDropDownInfo: Info = { + messageTKey: "CostText", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", //todo + textTKey: "ComputePricing", + }, +}; + +let priceMap: Map>; +let regions: Array; + +const calculateCost = (skuName: string, instanceCount: number): Description => { + try { + let costPerHour = 0; + for (const region of regions) { + const incrementalCost = priceMap.get(region).get(skuName.replace("Cosmos.", "")); + if (incrementalCost === undefined) { + throw new Error("Value not found in map."); + } + costPerHour += incrementalCost; + } + + costPerHour *= instanceCount; + costPerHour = Math.round(costPerHour * 100) / 100; + + return { + textTKey: `${costPerHour} USD`, + type: DescriptionType.Text, + }; + } catch (err) { + return costPerHourDefaultValue; + } +}; + +@IsDisplayable() +@RefreshOptions({ retryIntervalInMs: 20000 }) +export default class GraphAPICompute extends SelfServeBaseClass { + public onRefresh = async (): Promise => { + return await refreshComputeProvisioning(); + }; + + public onSave = async ( + currentValues: Map, + baselineValues: Map + ): Promise => { + selfServeTrace({ selfServeClassName: GraphAPICompute.name }); + + const ComputeCurrentlyEnabled = currentValues.get("enableCompute")?.value as boolean; + const ComputeOriginallyEnabled = baselineValues.get("enableCompute")?.value as boolean; + + currentValues.set("warningBanner", undefined); + + if (ComputeOriginallyEnabled) { + if (!ComputeCurrentlyEnabled) { + const operationStatusUrl = await deleteComputeResource(); + return { + operationStatusUrl: operationStatusUrl, + portalNotification: { + initialize: { + titleTKey: "DeleteInitializeTitle", + messageTKey: "DeleteInitializeMessage", + }, + success: { + titleTKey: "DeleteSuccessTitle", + messageTKey: "DeleteSuccesseMessage", + }, + failure: { + titleTKey: "DeleteFailureTitle", + messageTKey: "DeleteFailureMessage", + }, + }, + }; + } else { + const sku = currentValues.get("sku")?.value as string; + const instances = currentValues.get("instances").value as number; + const operationStatusUrl = await updateComputeResource(sku, instances); + return { + operationStatusUrl: operationStatusUrl, + portalNotification: { + initialize: { + titleTKey: "UpdateInitializeTitle", + messageTKey: "UpdateInitializeMessage", + }, + success: { + titleTKey: "UpdateSuccessTitle", + messageTKey: "UpdateSuccesseMessage", + }, + failure: { + titleTKey: "UpdateFailureTitle", + messageTKey: "UpdateFailureMessage", + }, + }, + }; + } + } else { + const sku = currentValues.get("sku")?.value as string; + const instances = currentValues.get("instances").value as number; + const operationStatusUrl = await updateComputeResource(sku, instances); + return { + operationStatusUrl: operationStatusUrl, + portalNotification: { + initialize: { + titleTKey: "CreateInitializeTitle", + messageTKey: "CreateInitializeMessage", + }, + success: { + titleTKey: "CreateSuccessTitle", + messageTKey: "CreateSuccesseMessage", + }, + failure: { + titleTKey: "CreateFailureTitle", + messageTKey: "CreateFailureMessage", + }, + }, + }; + } + }; + + public initialize = async (): Promise> => { + // Based on the RP call enableCompute will be true if it has not yet been enabled and false if it has. + const defaults = new Map(); + defaults.set("enableCompute", { value: false }); + defaults.set("sku", { value: CosmosD4s, hidden: true }); + defaults.set("instances", { value: 1, hidden: true }); + defaults.set("costPerHour", undefined); + defaults.set("connectionString", undefined); + defaults.set("metricsString", { + value: undefined, + hidden: true, + }); + + regions = await getReadRegions(); + priceMap = await getPriceMap(regions); + const response = await getCurrentProvisioningState(); + if (response.status && response.status === "Creating") { + defaults.set("enableCompute", { value: true }); + defaults.set("sku", { value: response.sku, disabled: true }); + defaults.set("instances", { value: response.instances, disabled: true }); + defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) }); + defaults.set("connectionString", { + value: connectionStringValue, + hidden: true, + }); + defaults.set("metricsString", { + value: metricsStringValue, + hidden: true, + }); + } else if (response.status && response.status !== "Deleting") { + defaults.set("enableCompute", { value: true }); + defaults.set("sku", { value: response.sku, disabled: true }); + defaults.set("instances", { value: response.instances }); + defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) }); + defaults.set("connectionString", { + value: connectionStringValue, + hidden: false, + }); + defaults.set("metricsString", { + value: metricsStringValue, + hidden: false, + }); + } + + defaults.set("warningBanner", undefined); + return defaults; + }; + + @Values({ + isDynamicDescription: true, + }) + warningBanner: string; + + @Values({ + description: { + textTKey: "GraphAPIDescription", + type: DescriptionType.Text, + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", //todo + textTKey: "LearnAboutCompute", + }, + }, + }) + description: string; + + @OnChange(onEnableComputeChange) + @Values({ + labelTKey: "Compute", + trueLabelTKey: "Provisioned", + falseLabelTKey: "Deprovisioned", + }) + enableCompute: boolean; + + @OnChange(onSKUChange) + @Values({ + labelTKey: "SKUs", + choices: getSkus, + placeholderTKey: "SKUsPlaceHolder", + }) + sku: ChoiceItem; + + @OnChange(onNumberOfInstancesChange) + @PropertyInfo(NumberOfInstancesDropdownInfo) + @Values({ + labelTKey: "NumberOfInstances", + min: getInstancesMin, + max: getInstancesMax, + step: 1, + uiType: NumberUiType.Spinner, + }) + instances: number; + + @PropertyInfo(ApproximateCostDropDownInfo) + @Values({ + labelTKey: "ApproximateCost", + isDynamicDescription: true, + }) + costPerHour: string; + + @Values({ + labelTKey: "ConnectionString", + isDynamicDescription: true, + }) + connectionString: string; + + @Values({ + labelTKey: "MonitorUsage", + description: metricsStringValue, + }) + metricsString: string; +} diff --git a/src/SelfServe/GraphAPICompute/GraphAPICompute.types.ts b/src/SelfServe/GraphAPICompute/GraphAPICompute.types.ts new file mode 100644 index 000000000..76e61c435 --- /dev/null +++ b/src/SelfServe/GraphAPICompute/GraphAPICompute.types.ts @@ -0,0 +1,65 @@ +export enum Regions { + NorthCentralUS = "NorthCentralUS", + WestUS = "WestUS", + EastUS2 = "EastUS2", +} + +export interface AccountProps { + regions: Regions; + enableLogging: boolean; + accountName: string; + collectionThroughput: number; + dbThroughput: number; +} + +export type GraphAPIComputeServiceResource = { + id: string; + name: string; + type: string; + properties: GraphAPIComputeServiceProps; + locations: GraphAPIComputeServiceLocations; +}; +export type GraphAPIComputeServiceProps = { + serviceType: string; + creationTime: string; + status: string; + instanceSize: string; + instanceCount: number; + GraphAPIComputeEndPoint: string; +}; + +export type GraphAPIComputeServiceLocations = { + location: string; + status: string; + GraphAPIComputeEndpoint: string; +}; + +export type UpdateComputeRequestParameters = { + properties: UpdateComputeRequestProperties; +}; + +export type UpdateComputeRequestProperties = { + instanceSize: string; + instanceCount: number; + serviceType: string; +}; + +export type FetchPricesResponse = { + Items: Array; + NextPageLink: string | undefined; + Count: number; +}; + +export type PriceItem = { + retailPrice: number; + skuName: string; +}; + +export type RegionsResponse = { + locations: Array; + location: string; +}; + +export type RegionItem = { + locationName: string; +}; diff --git a/src/SelfServe/SelfServe.tsx b/src/SelfServe/SelfServe.tsx index 50f6eecf8..99e6e51c9 100644 --- a/src/SelfServe/SelfServe.tsx +++ b/src/SelfServe/SelfServe.tsx @@ -50,6 +50,14 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise Date: Wed, 8 Sep 2021 14:04:31 -0700 Subject: [PATCH 02/40] Sqlx approx cost bug fixes (#975) * function naming changed * bug fix: replacing multiple occurences of space correctly now --- src/SelfServe/SqlX/SqlX.rp.ts | 10 +++++----- src/SelfServe/SqlX/SqlX.tsx | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/SelfServe/SqlX/SqlX.rp.ts b/src/SelfServe/SqlX/SqlX.rp.ts index 61080763e..a7e788728 100644 --- a/src/SelfServe/SqlX/SqlX.rp.ts +++ b/src/SelfServe/SqlX/SqlX.rp.ts @@ -138,9 +138,9 @@ const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: str return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`; }; -export const getReadRegions = async (): Promise> => { +export const getRegions = async (): Promise> => { try { - const readRegions = new Array(); + const regions = new Array(); const response = await armRequestWithoutPolling({ host: configContext.ARM_ENDPOINT, @@ -150,13 +150,13 @@ export const getReadRegions = async (): Promise> => { }); if (response.result.location !== undefined) { - readRegions.push(response.result.location.replace(" ", "").toLowerCase()); + regions.push(response.result.location.split(" ").join("").toLowerCase()); } else { for (const location of response.result.locations) { - readRegions.push(location.locationName.replace(" ", "").toLowerCase()); + regions.push(location.locationName.split(" ").join("").toLowerCase()); } } - return readRegions; + return regions; } catch (err) { return new Array(); } diff --git a/src/SelfServe/SqlX/SqlX.tsx b/src/SelfServe/SqlX/SqlX.tsx index ca1177fa3..0d4c096da 100644 --- a/src/SelfServe/SqlX/SqlX.tsx +++ b/src/SelfServe/SqlX/SqlX.tsx @@ -17,7 +17,7 @@ import { deleteDedicatedGatewayResource, getCurrentProvisioningState, getPriceMap, - getReadRegions, + getRegions, refreshDedicatedGatewayProvisioning, updateDedicatedGatewayResource, } from "./SqlX.rp"; @@ -324,7 +324,7 @@ export default class SqlX extends SelfServeBaseClass { hidden: true, }); - regions = await getReadRegions(); + regions = await getRegions(); priceMap = await getPriceMap(regions); const response = await getCurrentProvisioningState(); From 7e4f030547f4d745c504573183d55da77c6cffa1 Mon Sep 17 00:00:00 2001 From: kcheekuri <88904658+kcheekuri@users.noreply.github.com> Date: Thu, 9 Sep 2021 14:02:00 -0400 Subject: [PATCH 03/40] =?UTF-8?q?Hidding=20container=20connection=20status?= =?UTF-8?q?=20behind=20the=20feature=20flag=20and=20initi=E2=80=A6=20(#101?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Hidding container connection status behind the feature flag and initializing scratch issue * maintaining connecting status UX part at notebooks context * Changing scratch name to temporary and showing only after connected --- src/Common/Constants.ts | 1 - src/Explorer/Explorer.tsx | 8 ---- .../CommandBar/CommandBarComponentAdapter.tsx | 9 +++- .../CommandBar/ConnectionStatusComponent.tsx | 13 ++--- src/Explorer/Notebook/useNotebook.ts | 10 ++-- .../CopyNotebookPaneComponent.tsx | 4 +- src/Explorer/Tree/ResourceTree.tsx | 6 +-- src/Explorer/Tree/ResourceTreeAdapter.tsx | 2 +- src/Phoenix/PhoenixClient.ts | 47 ++++++++++++------- src/Utils/GalleryUtils.test.ts | 3 +- src/Utils/GalleryUtils.ts | 4 +- 11 files changed, 59 insertions(+), 48 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 6e13f07b5..bb8a2787b 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -340,7 +340,6 @@ export enum ConflictOperationType { export enum ConnectionStatusType { Connecting = "Connecting", - Allocating = "Allocating", Connected = "Connected", Failed = "Connection Failed", } diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 2cd3e79da..55f056df7 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -4,7 +4,6 @@ import _ from "underscore"; import { AuthType } from "../AuthType"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import * as Constants from "../Common/Constants"; -import { ConnectionStatusType } from "../Common/Constants"; import { readCollection } from "../Common/dataAccess/readCollection"; import { readDatabases } from "../Common/dataAccess/readDatabases"; import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility"; @@ -347,10 +346,6 @@ export default class Explorer { } this._isInitializingNotebooks = true; if (userContext.features.phoenix) { - const connectionStatus: DataModels.ContainerConnectionInfo = { - status: ConnectionStatusType.Allocating, - }; - useNotebook.getState().setConnectionInfo(connectionStatus); const provisionData = { cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint, resourceId: userContext.databaseAccount.id, @@ -361,9 +356,6 @@ export default class Explorer { }; const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData); if (connectionInfo.data && connectionInfo.data.notebookServerUrl) { - connectionStatus.status = ConnectionStatusType.Connected; - useNotebook.getState().setConnectionInfo(connectionStatus); - useNotebook.getState().setNotebookServerInfo({ notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl, authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken, diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index 452d7e881..a6d17e96a 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -9,6 +9,7 @@ import create, { UseStore } from "zustand"; import { StyleConstants } from "../../../Common/Constants"; import * as ViewModels from "../../../Contracts/ViewModels"; import { useTabs } from "../../../hooks/useTabs"; +import { userContext } from "../../../UserContext"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import Explorer from "../../Explorer"; import { useSelectedNode } from "../../useSelectedNode"; @@ -54,7 +55,13 @@ export const CommandBar: React.FC = ({ container }: Props) => { const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor); uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); - uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus("connectionStatus")); + if ( + userContext.features.notebooksTemporarilyDown === false && + userContext.features.phoenix === true && + useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2 + ) { + uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus("connectionStatus")); + } if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) { uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker")); diff --git a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx index 7e4cfa04d..34066b201 100644 --- a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx +++ b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx @@ -1,4 +1,4 @@ -import { Icon, ProgressIndicator, Spinner, SpinnerSize, Stack, TooltipHost } from "@fluentui/react"; +import { Icon, ProgressIndicator, Stack, TooltipHost } from "@fluentui/react"; import * as React from "react"; import { ConnectionStatusType } from "../../../Common/Constants"; import { useNotebook } from "../../Notebook/useNotebook"; @@ -40,14 +40,9 @@ export const ConnectionStatus: React.FC = (): JSX.Element => { const connectionInfo = useNotebook((state) => state.connectionInfo); if (!connectionInfo) { - return ( - - Connecting - - - ); + return <>; } - if (connectionInfo && connectionInfo.status === ConnectionStatusType.Allocating && isActive === false) { + if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) { setIsActive(true); } else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) { stopTimer(); @@ -68,7 +63,7 @@ export const ConnectionStatus: React.FC = (): JSX.Element => { {connectionInfo.status} - {connectionInfo.status === ConnectionStatusType.Allocating && isActive && ( + {connectionInfo.status === ConnectionStatusType.Connecting && isActive && ( )} diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index 348f7278e..eae023783 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -29,7 +29,7 @@ interface NotebookState { gitHubNotebooksContentRoot: NotebookContentItem; galleryContentRoot: NotebookContentItem; connectionInfo: DataModels.ContainerConnectionInfo; - NotebookFolderName: string; + notebookFolderName: string; setIsNotebookEnabled: (isNotebookEnabled: boolean) => void; setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void; setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void; @@ -38,6 +38,7 @@ interface NotebookState { setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void; setIsShellEnabled: (isShellEnabled: boolean) => void; setNotebookBasePath: (notebookBasePath: string) => void; + setNotebookFolderName: (notebookFolderName: string) => void; refreshNotebooksEnabledStateForAccount: () => Promise; findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem; insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void; @@ -69,7 +70,7 @@ export const useNotebook: UseStore = create((set, get) => ({ gitHubNotebooksContentRoot: undefined, galleryContentRoot: undefined, connectionInfo: undefined, - NotebookFolderName: userContext.features.phoenix ? "My Notebooks Scratch" : "My Notebooks", + notebookFolderName: undefined, setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }), setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }), setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => @@ -80,6 +81,7 @@ export const useNotebook: UseStore = create((set, get) => ({ setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }), setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }), setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }), + setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }), refreshNotebooksEnabledStateForAccount: async (): Promise => { const { databaseAccount, authType } = userContext; if ( @@ -173,8 +175,10 @@ export const useNotebook: UseStore = create((set, get) => ({ isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); }, initializeNotebooksTree: async (notebookManager: NotebookManager): Promise => { + const notebookFolderName = userContext.features.phoenix === true ? "Temporary Notebooks" : "My Notebooks"; + set({ notebookFolderName }); const myNotebooksContentRoot = { - name: get().NotebookFolderName, + name: get().notebookFolderName, path: get().notebookBasePath, type: NotebookContentItemType.Directory, }; diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx index 66ef0d584..1aaab131a 100644 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx +++ b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx @@ -49,8 +49,8 @@ export const CopyNotebookPaneComponent: FunctionComponent const options: IDropdownOption[] = []; options.push({ key: "MyNotebooks-Item", - text: useNotebook.getState().NotebookFolderName, - title: useNotebook.getState().NotebookFolderName, + text: useNotebook.getState().notebookFolderName, + title: useNotebook.getState().notebookFolderName, data: { type: "MyNotebooks", } as Location, diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index 9f5396ed2..8d6bece87 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -11,7 +11,7 @@ import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; import PublishIcon from "../../../images/notebook/publish_content.svg"; import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; -import { Areas, Notebook } from "../../Common/Constants"; +import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; @@ -128,15 +128,13 @@ export const ResourceTree: React.FC = ({ container }: Resourc notebooksTree.children.push(buildGalleryNotebooksTree()); } - if (myNotebooksContentRoot) { + if (myNotebooksContentRoot && useNotebook.getState().connectionInfo.status == ConnectionStatusType.Connected) { notebooksTree.children.push(buildMyNotebooksTree()); } if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { // collapse all other notebook nodes notebooksTree.children.forEach((node) => (node.isExpanded = false)); notebooksTree.children.push(buildGitHubNotebooksTree(true)); - } else if (container.notebookManager && !container.notebookManager.gitHubOAuthService.isLoggedIn()) { - notebooksTree.children.push(buildGitHubNotebooksTree(false)); } } return notebooksTree; diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 7941446d3..9c5da6b19 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -132,7 +132,7 @@ export class ResourceTreeAdapter implements ReactAdapter { type: NotebookContentItemType.File, }; this.myNotebooksContentRoot = { - name: useNotebook.getState().NotebookFolderName, + name: useNotebook.getState().notebookFolderName, path: useNotebook.getState().notebookBasePath, type: NotebookContentItemType.Directory, }; diff --git a/src/Phoenix/PhoenixClient.ts b/src/Phoenix/PhoenixClient.ts index 155672cd3..aa4ed9322 100644 --- a/src/Phoenix/PhoenixClient.ts +++ b/src/Phoenix/PhoenixClient.ts @@ -1,6 +1,6 @@ import { ConnectionStatusType, HttpHeaders, HttpStatusCodes } from "../Common/Constants"; import { configContext } from "../ConfigContext"; -import * as DataModels from "../Contracts/DataModels"; +import { ContainerConnectionInfo } from "../Contracts/DataModels"; import { useNotebook } from "../Explorer/Notebook/useNotebook"; import { userContext } from "../UserContext"; import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; @@ -25,25 +25,40 @@ export class PhoenixClient { public async containerConnectionInfo( provisionData: IProvosionData ): Promise> { - const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/provision`, { - method: "POST", - headers: PhoenixClient.getHeaders(), - body: JSON.stringify(provisionData), - }); - let data: IPhoenixConnectionInfoResult; - if (response.status === HttpStatusCodes.OK) { - data = await response.json(); - } else { - const connectionStatus: DataModels.ContainerConnectionInfo = { + try { + const connectionStatus: ContainerConnectionInfo = { + status: ConnectionStatusType.Connecting, + }; + useNotebook.getState().setConnectionInfo(connectionStatus); + const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/provision`, { + method: "POST", + headers: PhoenixClient.getHeaders(), + body: JSON.stringify(provisionData), + }); + let data: IPhoenixConnectionInfoResult; + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + if (data && data.notebookServerUrl) { + connectionStatus.status = ConnectionStatusType.Connected; + useNotebook.getState().setConnectionInfo(connectionStatus); + } + } else { + connectionStatus.status = ConnectionStatusType.Failed; + useNotebook.getState().setConnectionInfo(connectionStatus); + } + + return { + status: response.status, + data, + }; + } catch (error) { + const connectionStatus: ContainerConnectionInfo = { status: ConnectionStatusType.Failed, }; useNotebook.getState().setConnectionInfo(connectionStatus); + console.error(error); + throw error; } - - return { - status: response.status, - data, - }; } public static getPhoenixEndpoint(): string { diff --git a/src/Utils/GalleryUtils.test.ts b/src/Utils/GalleryUtils.test.ts index 4df3651cd..2ba4ac7e3 100644 --- a/src/Utils/GalleryUtils.test.ts +++ b/src/Utils/GalleryUtils.test.ts @@ -2,6 +2,7 @@ import { HttpStatusCodes } from "../Common/Constants"; import { useDialog } from "../Explorer/Controls/Dialog"; import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; import Explorer from "../Explorer/Explorer"; +import { useNotebook } from "../Explorer/Notebook/useNotebook"; import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; import * as GalleryUtils from "./GalleryUtils"; @@ -34,7 +35,7 @@ describe("GalleryUtils", () => { expect(useDialog.getState().visible).toBe(true); expect(useDialog.getState().dialogProps).toBeDefined(); - expect(useDialog.getState().dialogProps.title).toBe("Download to My Notebooks"); + expect(useDialog.getState().dialogProps.title).toBe(`Download to ${useNotebook.getState().notebookFolderName}`); }); it("favoriteItem favorites item", async () => { diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index db3f081de..6ed56f05c 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -224,12 +224,12 @@ export function downloadItem( const name = data.name; useDialog.getState().showOkCancelModalDialog( - `Download to ${useNotebook.getState().NotebookFolderName}`, + `Download to ${useNotebook.getState().notebookFolderName}`, `Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`, "Download", async () => { const clearInProgressMessage = logConsoleProgress( - `Downloading ${name} to ${useNotebook.getState().NotebookFolderName}` + `Downloading ${name} to ${useNotebook.getState().notebookFolderName}` ); const startKey = traceStart(Action.NotebooksGalleryDownload, { notebookId: data.id, From d10f3c69f1ddd0975770d1537fcefd71bcfbe191 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Mon, 13 Sep 2021 16:25:21 -0400 Subject: [PATCH 04/40] MongoClient Feature Flag (#1073) Adding a feature flag for Mongo Client that allows a user to specify a mongo endpoint and an API so that users can test specific APIs locally. Example: https://localhost:1234/hostedExplorer.html?feature.mongoproxyendpoint=https://localhost:12901&feature.mongoProxyAPIs=createDocument|readDocument The above link says to test APIs createDocument and readDocument on https://localhost:12901 Co-authored-by: artrejo Co-authored-by: Asier Isayas --- src/Common/MongoProxyClient.test.ts | 31 ++++++++++++++++++++- src/Common/MongoProxyClient.ts | 23 +++++++++------ src/Platform/Hosted/extractFeatures.test.ts | 19 +++++++++++-- src/Platform/Hosted/extractFeatures.ts | 13 +++++++++ 4 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/Common/MongoProxyClient.test.ts b/src/Common/MongoProxyClient.test.ts index 3a5a02365..4d7bd9022 100644 --- a/src/Common/MongoProxyClient.test.ts +++ b/src/Common/MongoProxyClient.test.ts @@ -3,8 +3,9 @@ import { resetConfigContext, updateConfigContext } from "../ConfigContext"; import { DatabaseAccount } from "../Contracts/DataModels"; import { Collection } from "../Contracts/ViewModels"; import DocumentId from "../Explorer/Tree/DocumentId"; +import { extractFeatures } from "../Platform/Hosted/extractFeatures"; import { updateUserContext } from "../UserContext"; -import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient"; +import { deleteDocument, getEndpoint, getFeatureEndpointOrDefault, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient"; const databaseId = "testDB"; @@ -246,4 +247,32 @@ describe("MongoProxyClient", () => { expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer"); }); }); + describe("getFeatureEndpointOrDefault", () => { + beforeEach(() => { + resetConfigContext(); + updateConfigContext({ + BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", + }); + const params = new URLSearchParams({ + "feature.mongoProxyEndpoint": "https://localhost:12901", + "feature.mongoProxyAPIs": "readDocument|createDocument", + }); + const features = extractFeatures(params); + updateUserContext({ + authType: AuthType.AAD, + features: features + }); + }); + + + it("returns a local endpoint", () => { + const endpoint = getFeatureEndpointOrDefault("readDocument"); + expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer"); + }); + + it("returns a production endpoint", () => { + const endpoint = getFeatureEndpointOrDefault("deleteDocument"); + expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer"); + }); + }); }); diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 2945f1288..1869e7061 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -6,6 +6,7 @@ import * as DataModels from "../Contracts/DataModels"; import { MessageTypes } from "../Contracts/ExplorerContracts"; import { Collection } from "../Contracts/ViewModels"; import DocumentId from "../Explorer/Tree/DocumentId"; +import { hasFlag } from "../Platform/Hosted/extractFeatures"; import { userContext } from "../UserContext"; import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants"; @@ -78,7 +79,7 @@ export function queryDocuments( : "", }; - const endpoint = getEndpoint() || ""; + const endpoint = getFeatureEndpointOrDefault("resourcelist") || ""; const headers = { ...defaultHeaders, @@ -141,7 +142,8 @@ export function readDocument( : "", }; - const endpoint = getEndpoint(); + const endpoint = getFeatureEndpointOrDefault("readDocument"); + return window .fetch(`${endpoint}?${queryString.stringify(params)}`, { method: "GET", @@ -181,7 +183,7 @@ export function createDocument( pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "", }; - const endpoint = getEndpoint(); + const endpoint = getFeatureEndpointOrDefault("createDocument"); return window .fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, { @@ -225,7 +227,7 @@ export function updateDocument( ? documentId.partitionKeyProperty : "", }; - const endpoint = getEndpoint(); + const endpoint = getFeatureEndpointOrDefault("updateDocument"); return window .fetch(`${endpoint}?${queryString.stringify(params)}`, { @@ -266,7 +268,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum ? documentId.partitionKeyProperty : "", }; - const endpoint = getEndpoint(); + const endpoint = getFeatureEndpointOrDefault("deleteDocument");; return window .fetch(`${endpoint}?${queryString.stringify(params)}`, { @@ -309,7 +311,7 @@ export function createMongoCollectionWithProxy( autoPilotThroughput: params.autoPilotMaxThroughput?.toString(), }; - const endpoint = getEndpoint(); + const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy"); return window .fetch( @@ -333,8 +335,13 @@ export function createMongoCollectionWithProxy( }); } -export function getEndpoint(): string { - let url = (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT) + "/api/mongo/explorer"; +export function getFeatureEndpointOrDefault(feature: string): string { + return (hasFlag(userContext.features.mongoProxyAPIs, feature)) ? getEndpoint(userContext.features.mongoProxyEndpoint) : getEndpoint(); +} + +export function getEndpoint(customEndpoint?: string): string { + let url = customEndpoint ? customEndpoint : (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT); + url += "/api/mongo/explorer"; if (userContext.authType === AuthType.EncryptedToken) { url = url.replace("api/mongo", "api/guest/mongo"); diff --git a/src/Platform/Hosted/extractFeatures.test.ts b/src/Platform/Hosted/extractFeatures.test.ts index a44e81a5d..ac2e7afae 100644 --- a/src/Platform/Hosted/extractFeatures.test.ts +++ b/src/Platform/Hosted/extractFeatures.test.ts @@ -1,4 +1,4 @@ -import { extractFeatures } from "./extractFeatures"; +import { extractFeatures, hasFlag } from "./extractFeatures"; describe("extractFeatures", () => { it("correctly detects feature flags in a case insensitive manner", () => { @@ -14,9 +14,24 @@ describe("extractFeatures", () => { }); const features = extractFeatures(params); - expect(features.notebookServerUrl).toBe(url); expect(features.notebookServerToken).toBe(token); expect(features.enableNotebooks).toBe(notebooksEnabled); }); }); + +describe("hasFlag", () => { + it("correctly determines if value has flag", () => { + const desiredFlag = "readDocument"; + + const singleFlagValue = "readDocument"; + const multipleFlagValues = "readDocument|createDocument"; + const differentFlagValue = "createDocument"; + + expect(hasFlag(singleFlagValue, desiredFlag)).toBe(true); + expect(hasFlag(multipleFlagValues, desiredFlag)).toBe(true); + expect(hasFlag(differentFlagValue, desiredFlag)).toBe(false); + expect(hasFlag(multipleFlagValues, undefined)).toBe(false); + expect(hasFlag(undefined, desiredFlag)).toBe(false); + }); +}); diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 29c106a6e..130991301 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -29,6 +29,8 @@ export type Features = { readonly pr?: string; readonly showMinRUSurvey: boolean; readonly ttl90Days: boolean; + readonly mongoProxyEndpoint: string; + readonly mongoProxyAPIs: string; readonly notebooksTemporarilyDown: boolean; }; @@ -63,6 +65,8 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear enableKoResourceTree: "true" === get("enablekoresourcetree"), executeSproc: "true" === get("dataexplorerexecutesproc"), hostedDataExplorer: "true" === get("hosteddataexplorerenabled"), + mongoProxyEndpoint: get("mongoproxyendpoint"), + mongoProxyAPIs: get("mongoproxyapis"), junoEndpoint: get("junoendpoint"), livyEndpoint: get("livyendpoint"), notebookBasePath: get("notebookbasepath"), @@ -80,3 +84,12 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear phoenix: "true" === get("phoenix"), }; } + +export function hasFlag(flags: string, desiredFlag: string): boolean { + if (!flags || !desiredFlag) { + return false; + } + + const features = flags.split("|"); + return features.find((feature) => feature === desiredFlag) ? true : false; +} From 8866976bb42f146f71e9e0918f91f6fb0efdbab3 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Tue, 14 Sep 2021 12:28:33 -0400 Subject: [PATCH 05/40] fixed hasFlag test (#1076) Co-authored-by: Asier Isayas --- src/Platform/Hosted/extractFeatures.test.ts | 5 +++-- src/Platform/Hosted/extractFeatures.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Platform/Hosted/extractFeatures.test.ts b/src/Platform/Hosted/extractFeatures.test.ts index ac2e7afae..98fc8ebd7 100644 --- a/src/Platform/Hosted/extractFeatures.test.ts +++ b/src/Platform/Hosted/extractFeatures.test.ts @@ -31,7 +31,8 @@ describe("hasFlag", () => { expect(hasFlag(singleFlagValue, desiredFlag)).toBe(true); expect(hasFlag(multipleFlagValues, desiredFlag)).toBe(true); expect(hasFlag(differentFlagValue, desiredFlag)).toBe(false); - expect(hasFlag(multipleFlagValues, undefined)).toBe(false); - expect(hasFlag(undefined, desiredFlag)).toBe(false); + expect(hasFlag(multipleFlagValues, undefined as unknown as string)).toBe(false); + expect(hasFlag(undefined as unknown as string, desiredFlag)).toBe(false); + expect(hasFlag(undefined as unknown as string, undefined as unknown as string)).toBe(false); }); }); diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 130991301..1313aa834 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -29,8 +29,8 @@ export type Features = { readonly pr?: string; readonly showMinRUSurvey: boolean; readonly ttl90Days: boolean; - readonly mongoProxyEndpoint: string; - readonly mongoProxyAPIs: string; + readonly mongoProxyEndpoint?: string; + readonly mongoProxyAPIs?: string; readonly notebooksTemporarilyDown: boolean; }; From 2d945c82317297a618f35646aaee9cb0267dae9c Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Tue, 14 Sep 2021 12:33:09 -0400 Subject: [PATCH 06/40] allowing azure client secret to be null in dev mode (#1079) Co-authored-by: Asier Isayas --- webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index a1cb34e5a..e6bcbe1e0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -103,7 +103,7 @@ module.exports = function (_env = {}, argv = {}) { envVars.NODE_ENV = "development"; envVars.AZURE_CLIENT_ID = AZURE_CLIENT_ID; envVars.AZURE_TENANT_ID = AZURE_TENANT_ID; - envVars.AZURE_CLIENT_SECRET = AZURE_CLIENT_SECRET; + envVars.AZURE_CLIENT_SECRET = AZURE_CLIENT_SECRET || null; envVars.SUBSCRIPTION_ID = SUBSCRIPTION_ID; envVars.RESOURCE_GROUP = RESOURCE_GROUP; typescriptRule.use[0].options.compilerOptions = { target: "ES2018" }; From 665270296f6bf8de8f76a27fe5bb1ad856954624 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Wed, 15 Sep 2021 13:05:55 -0700 Subject: [PATCH 07/40] Fix throughput cost estimate in add collection panel (#1070) --- src/Common/MongoProxyClient.test.ts | 12 ++- src/Common/MongoProxyClient.ts | 8 +- .../Controls/Settings/SettingsRenderUtils.tsx | 73 +++++++------- .../CostEstimateText/CostEstimateText.tsx | 7 +- src/Platform/Hosted/extractFeatures.test.ts | 6 +- src/Shared/Constants.ts | 6 +- src/Shared/PriceEstimateCalculator.ts | 4 +- src/Utils/PricingUtils.test.ts | 99 +++++++------------ src/Utils/PricingUtils.ts | 36 +++---- 9 files changed, 117 insertions(+), 134 deletions(-) diff --git a/src/Common/MongoProxyClient.test.ts b/src/Common/MongoProxyClient.test.ts index 4d7bd9022..1c49141a0 100644 --- a/src/Common/MongoProxyClient.test.ts +++ b/src/Common/MongoProxyClient.test.ts @@ -5,7 +5,14 @@ import { Collection } from "../Contracts/ViewModels"; import DocumentId from "../Explorer/Tree/DocumentId"; import { extractFeatures } from "../Platform/Hosted/extractFeatures"; import { updateUserContext } from "../UserContext"; -import { deleteDocument, getEndpoint, getFeatureEndpointOrDefault, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient"; +import { + deleteDocument, + getEndpoint, + getFeatureEndpointOrDefault, + queryDocuments, + readDocument, + updateDocument, +} from "./MongoProxyClient"; const databaseId = "testDB"; @@ -260,11 +267,10 @@ describe("MongoProxyClient", () => { const features = extractFeatures(params); updateUserContext({ authType: AuthType.AAD, - features: features + features: features, }); }); - it("returns a local endpoint", () => { const endpoint = getFeatureEndpointOrDefault("readDocument"); expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer"); diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 1869e7061..668a0ab16 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -268,7 +268,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum ? documentId.partitionKeyProperty : "", }; - const endpoint = getFeatureEndpointOrDefault("deleteDocument");; + const endpoint = getFeatureEndpointOrDefault("deleteDocument"); return window .fetch(`${endpoint}?${queryString.stringify(params)}`, { @@ -336,11 +336,13 @@ export function createMongoCollectionWithProxy( } export function getFeatureEndpointOrDefault(feature: string): string { - return (hasFlag(userContext.features.mongoProxyAPIs, feature)) ? getEndpoint(userContext.features.mongoProxyEndpoint) : getEndpoint(); + return hasFlag(userContext.features.mongoProxyAPIs, feature) + ? getEndpoint(userContext.features.mongoProxyEndpoint) + : getEndpoint(); } export function getEndpoint(customEndpoint?: string): string { - let url = customEndpoint ? customEndpoint : (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT); + let url = customEndpoint ? customEndpoint : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT; url += "/api/mongo/explorer"; if (userContext.authType === AuthType.EncryptedToken) { diff --git a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx index c0cda4f59..fe29a6d90 100644 --- a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx @@ -1,45 +1,45 @@ -import * as React from "react"; -import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; -import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants"; -import { Urls, StyleConstants } from "../../../Common/Constants"; import { - getPriceCurrency, - getCurrencySign, - getAutoscalePricePerRu, - getMultimasterMultiplier, - computeRUUsagePriceHourly, - getPricePerRu, - estimatedCostDisclaimer, -} from "../../../Utils/PricingUtils"; -import { - ITextFieldStyles, + DetailsList, + DetailsListLayoutMode, + DetailsRow, ICheckboxStyles, - IStackProps, - IStackTokens, IChoiceGroupStyles, - Link, - Text, - IMessageBarStyles, - ITextStyles, - IDetailsRowStyles, - IStackStyles, + IColumn, + IDetailsColumnStyles, IDetailsListStyles, + IDetailsRowProps, + IDetailsRowStyles, IDropdownStyles, + IMessageBarStyles, ISeparatorStyles, + IStackProps, + IStackStyles, + IStackTokens, + ITextFieldStyles, + ITextStyles, + Link, MessageBar, MessageBarType, - Stack, + SelectionMode, Spinner, SpinnerSize, - DetailsList, - IColumn, - SelectionMode, - DetailsListLayoutMode, - IDetailsRowProps, - DetailsRow, - IDetailsColumnStyles, + Stack, + Text, } from "@fluentui/react"; -import { isDirtyTypes, isDirty } from "./SettingsUtils"; +import * as React from "react"; +import { StyleConstants, Urls } from "../../../Common/Constants"; +import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants"; +import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; +import { + computeRUUsagePriceHourly, + estimatedCostDisclaimer, + getAutoscalePricePerRu, + getCurrencySign, + getMultimasterMultiplier, + getPriceCurrency, + getPricePerRu, +} from "../../../Utils/PricingUtils"; +import { isDirty, isDirtyTypes } from "./SettingsUtils"; export interface EstimatedSpendingDisplayProps { costType: JSX.Element; @@ -223,14 +223,15 @@ export const getRuPriceBreakdown = ( multimasterEnabled: isMultimaster, isAutoscale: isAutoscale, }); - const basePricePerRu: number = isAutoscale - ? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster)) - : getPricePerRu(serverId); + const multimasterMultiplier = getMultimasterMultiplier(numberOfRegions, isMultimaster); + const pricePerRu: number = isAutoscale + ? getAutoscalePricePerRu(serverId, multimasterMultiplier) + : getPricePerRu(serverId, multimasterMultiplier); return { - hourlyPrice: hourlyPrice, + hourlyPrice, dailyPrice: hourlyPrice * 24, monthlyPrice: hourlyPrice * hoursInAMonth, - pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster), + pricePerRu, currency: getPriceCurrency(serverId), currencySign: getCurrencySign(serverId), }; diff --git a/src/Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText.tsx b/src/Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText.tsx index 51aaae619..fbc469f47 100644 --- a/src/Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText.tsx +++ b/src/Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText.tsx @@ -6,6 +6,7 @@ import { userContext } from "../../../../UserContext"; import { calculateEstimateNumber, computeRUUsagePriceHourly, + estimatedCostDisclaimer, getAutoscalePricePerRu, getCurrencySign, getMultimasterMultiplier, @@ -42,11 +43,9 @@ export const CostEstimateText: FunctionComponent = ({ const currency: string = getPriceCurrency(serverId); const currencySign: string = getCurrencySign(serverId); const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled); - const pricePerRu = isAutoscale - ? getAutoscalePricePerRu(serverId, multiplier) * multiplier - : getPricePerRu(serverId) * multiplier; + const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multiplier) : getPricePerRu(serverId, multiplier); - const iconWithEstimatedCostDisclaimer: JSX.Element = PricingUtils.estimatedCostDisclaimer; + const iconWithEstimatedCostDisclaimer: JSX.Element = {estimatedCostDisclaimer}; if (isAutoscale) { return ( diff --git a/src/Platform/Hosted/extractFeatures.test.ts b/src/Platform/Hosted/extractFeatures.test.ts index 98fc8ebd7..f99b09d89 100644 --- a/src/Platform/Hosted/extractFeatures.test.ts +++ b/src/Platform/Hosted/extractFeatures.test.ts @@ -31,8 +31,8 @@ describe("hasFlag", () => { expect(hasFlag(singleFlagValue, desiredFlag)).toBe(true); expect(hasFlag(multipleFlagValues, desiredFlag)).toBe(true); expect(hasFlag(differentFlagValue, desiredFlag)).toBe(false); - expect(hasFlag(multipleFlagValues, undefined as unknown as string)).toBe(false); - expect(hasFlag(undefined as unknown as string, desiredFlag)).toBe(false); - expect(hasFlag(undefined as unknown as string, undefined as unknown as string)).toBe(false); + expect(hasFlag(multipleFlagValues, (undefined as unknown) as string)).toBe(false); + expect(hasFlag((undefined as unknown) as string, desiredFlag)).toBe(false); + expect(hasFlag((undefined as unknown) as string, (undefined as unknown) as string)).toBe(false); }); }); diff --git a/src/Shared/Constants.ts b/src/Shared/Constants.ts index 97a9349c5..8594ccc25 100644 --- a/src/Shared/Constants.ts +++ b/src/Shared/Constants.ts @@ -125,7 +125,8 @@ export class OfferPricing { S3Price: 0.1344, Standard: { StartingPrice: 24 / hoursInAMonth, // per hour - PricePerRU: 0.00008, + SingleMasterPricePerRU: 0.00008, + MultiMasterPricePerRU: 0.00016, PricePerGB: 0.25 / hoursInAMonth, }, }, @@ -137,7 +138,8 @@ export class OfferPricing { S3Price: 0.6, Standard: { StartingPrice: OfferPricing.MonthlyPricing.mooncake.Standard.StartingPrice / hoursInAMonth, // per hour - PricePerRU: 0.00051, + SingleMasterPricePerRU: 0.00051, + MultiMasterPricePerRU: 0.00102, PricePerGB: OfferPricing.MonthlyPricing.mooncake.Standard.PricePerGB / hoursInAMonth, }, }, diff --git a/src/Shared/PriceEstimateCalculator.ts b/src/Shared/PriceEstimateCalculator.ts index a7f08d230..e6cb27db6 100644 --- a/src/Shared/PriceEstimateCalculator.ts +++ b/src/Shared/PriceEstimateCalculator.ts @@ -2,11 +2,11 @@ import * as Constants from "./Constants"; export function computeRUUsagePrice(serverId: string, requestUnits: number): string { if (serverId === "mooncake") { - const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU; + const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.SingleMasterPricePerRU; return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency; } - const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU; + const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.SingleMasterPricePerRU; return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency; } diff --git a/src/Utils/PricingUtils.test.ts b/src/Utils/PricingUtils.test.ts index de4efa90c..15a504ce2 100644 --- a/src/Utils/PricingUtils.test.ts +++ b/src/Utils/PricingUtils.test.ts @@ -150,7 +150,7 @@ describe("PricingUtils Tests", () => { expect(value).toBe(0.00012); }); - it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled", () => { + it("should return 0.00032 for default cloud, 1RU, 2 region, multimaster enabled", () => { const value = PricingUtils.computeRUUsagePriceHourly({ serverId: "default", requestUnits: 1, @@ -158,9 +158,9 @@ describe("PricingUtils Tests", () => { multimasterEnabled: true, isAutoscale: false, }); - expect(value).toBe(0.00048); + expect(value).toBe(0.00032); }); - it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled, autoscale", () => { + it("should return 0.00032 for default cloud, 1RU, 2 region, multimaster enabled, autoscale", () => { const value = PricingUtils.computeRUUsagePriceHourly({ serverId: "default", requestUnits: 1, @@ -168,7 +168,7 @@ describe("PricingUtils Tests", () => { multimasterEnabled: true, isAutoscale: true, }); - expect(value).toBe(0.00096); + expect(value).toBe(0.00032); }); }); @@ -251,70 +251,47 @@ describe("PricingUtils Tests", () => { }); describe("getPricePerRu()", () => { - it("should return 0.00008 for default clouds", () => { - const value = PricingUtils.getPricePerRu("default"); + it("should return 0.00008 for single master default clouds", () => { + const value = PricingUtils.getPricePerRu("default", 1); expect(value).toBe(0.00008); }); - it("should return 0.00051 for mooncake", () => { - const value = PricingUtils.getPricePerRu("mooncake"); + it("should return 0.00016 for multi master default clouds", () => { + const value = PricingUtils.getPricePerRu("default", 2); + expect(value).toBe(0.00016); + }); + + it("should return 0.00051 for single master mooncake", () => { + const value = PricingUtils.getPricePerRu("mooncake", 1); expect(value).toBe(0.00051); }); + + it("should return 0.00102 for multi master mooncake", () => { + const value = PricingUtils.getPricePerRu("mooncake", 2); + expect(value).toBe(0.00102); + }); }); describe("getRegionMultiplier()", () => { - describe("without multimaster", () => { - it("should return 0 for undefined", () => { - const value = PricingUtils.getRegionMultiplier(undefined, false); - expect(value).toBe(0); - }); - - it("should return 0 for -1", () => { - const value = PricingUtils.getRegionMultiplier(-1, false); - expect(value).toBe(0); - }); - - it("should return 0 for 0", () => { - const value = PricingUtils.getRegionMultiplier(0, false); - expect(value).toBe(0); - }); - - it("should return 1 for 1", () => { - const value = PricingUtils.getRegionMultiplier(1, false); - expect(value).toBe(1); - }); - - it("should return 2 for 2", () => { - const value = PricingUtils.getRegionMultiplier(2, false); - expect(value).toBe(2); - }); + it("should return 0 for undefined", () => { + const value = PricingUtils.getRegionMultiplier(undefined); + expect(value).toBe(0); }); - - describe("with multimaster", () => { - it("should return 0 for undefined", () => { - const value = PricingUtils.getRegionMultiplier(undefined, true); - expect(value).toBe(0); - }); - - it("should return 0 for -1", () => { - const value = PricingUtils.getRegionMultiplier(-1, true); - expect(value).toBe(0); - }); - - it("should return 0 for 0", () => { - const value = PricingUtils.getRegionMultiplier(0, true); - expect(value).toBe(0); - }); - - it("should return 1 for 1", () => { - const value = PricingUtils.getRegionMultiplier(1, true); - expect(value).toBe(1); - }); - - it("should return 3 for 2", () => { - const value = PricingUtils.getRegionMultiplier(2, true); - expect(value).toBe(3); - }); + it("should return 0 for -1", () => { + const value = PricingUtils.getRegionMultiplier(-1); + expect(value).toBe(0); + }); + it("should return 0 for 0", () => { + const value = PricingUtils.getRegionMultiplier(0); + expect(value).toBe(0); + }); + it("should return 1 for 1", () => { + const value = PricingUtils.getRegionMultiplier(1); + expect(value).toBe(1); + }); + it("should return 2 for 2", () => { + const value = PricingUtils.getRegionMultiplier(2); + expect(value).toBe(2); }); }); @@ -376,7 +353,7 @@ describe("PricingUtils Tests", () => { true /* multimaster */ ); expect(value).toBe( - "Cost (USD): $0.19 hourly / $4.61 daily / $140.16 monthly (2 regions, 400RU/s, $0.00016/RU)

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

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

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

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