diff --git a/.eslintignore b/.eslintignore index 6beb83c58..b37c00904 100644 --- a/.eslintignore +++ b/.eslintignore @@ -288,8 +288,6 @@ src/Utils/DatabaseAccountUtils.ts src/Utils/JunoUtils.ts src/Utils/MessageValidation.ts src/Utils/NotebookConfigurationUtils.ts -src/Utils/OfferUtils.test.ts -src/Utils/OfferUtils.ts src/Utils/PricingUtils.test.ts src/Utils/QueryUtils.test.ts src/Utils/QueryUtils.ts diff --git a/src/Common/DataAccessUtilityBase.ts b/src/Common/DataAccessUtilityBase.ts index 156f15687..e835829d4 100644 --- a/src/Common/DataAccessUtilityBase.ts +++ b/src/Common/DataAccessUtilityBase.ts @@ -1,26 +1,13 @@ -import { - ConflictDefinition, - FeedOptions, - ItemDefinition, - OfferDefinition, - QueryIterator, - Resource -} from "@azure/cosmos"; -import { RequestOptions } from "@azure/cosmos/dist-esm"; +import { ConflictDefinition, FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; import Q from "q"; -import { configContext, Platform } from "../ConfigContext"; import * as DataModels from "../Contracts/DataModels"; -import { MessageTypes } from "../Contracts/ExplorerContracts"; import * as ViewModels from "../Contracts/ViewModels"; import ConflictId from "../Explorer/Tree/ConflictId"; import DocumentId from "../Explorer/Tree/DocumentId"; import StoredProcedure from "../Explorer/Tree/StoredProcedure"; import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; -import { OfferUtils } from "../Utils/OfferUtils"; import * as Constants from "./Constants"; import { client } from "./CosmosClient"; -import * as HeadersUtility from "./HeadersUtility"; -import { sendCachedDataMessage } from "./MessageHandler"; export function getCommonQueryOptions(options: FeedOptions): any { const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage); diff --git a/src/Common/OfferUtility.test.ts b/src/Common/OfferUtility.test.ts new file mode 100644 index 000000000..5b8a39a69 --- /dev/null +++ b/src/Common/OfferUtility.test.ts @@ -0,0 +1,62 @@ +import * as OfferUtility from "./OfferUtility"; +import { SDKOfferDefinition, Offer } from "../Contracts/DataModels"; +import { OfferResponse } from "@azure/cosmos"; + +describe("parseSDKOfferResponse", () => { + it("manual throughput", () => { + const mockOfferDefinition = { + content: { + offerThroughput: 500, + collectionThroughputInfo: { + minimumRUForCollection: 400, + numPhysicalPartitions: 1 + } + }, + id: "test" + } as SDKOfferDefinition; + + const mockResponse = { + resource: mockOfferDefinition + } as OfferResponse; + + const expectedResult: Offer = { + manualThroughput: 500, + autoscaleMaxThroughput: undefined, + minimumThroughput: 400, + id: "test", + offerDefinition: mockOfferDefinition + }; + + expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult); + }); + + it("autoscale throughput", () => { + const mockOfferDefinition = { + content: { + offerThroughput: 400, + collectionThroughputInfo: { + minimumRUForCollection: 400, + numPhysicalPartitions: 1 + }, + offerAutopilotSettings: { + maxThroughput: 5000 + } + }, + id: "test" + } as SDKOfferDefinition; + + const mockResponse = { + resource: mockOfferDefinition + } as OfferResponse; + + const expectedResult: Offer = { + manualThroughput: undefined, + autoscaleMaxThroughput: 5000, + minimumThroughput: 400, + id: "test", + offerDefinition: mockOfferDefinition + }; + + expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult); + }); +}); diff --git a/src/Common/OfferUtility.ts b/src/Common/OfferUtility.ts new file mode 100644 index 000000000..a71dc7c84 --- /dev/null +++ b/src/Common/OfferUtility.ts @@ -0,0 +1,33 @@ +import { Offer, SDKOfferDefinition } from "../Contracts/DataModels"; +import { OfferResponse } from "@azure/cosmos"; + +export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => { + const offerDefinition: SDKOfferDefinition = offerResponse?.resource; + const offerContent = offerDefinition.content; + if (!offerContent) { + return undefined; + } + + const minimumThroughput = offerContent.collectionThroughputInfo?.minimumRUForCollection; + const autopilotSettings = offerContent.offerAutopilotSettings; + + if (autopilotSettings) { + return { + id: offerDefinition.id, + autoscaleMaxThroughput: autopilotSettings.maxThroughput, + manualThroughput: undefined, + minimumThroughput, + offerDefinition, + headers: offerResponse.headers + }; + } + + return { + id: offerDefinition.id, + autoscaleMaxThroughput: undefined, + manualThroughput: offerContent.offerThroughput, + minimumThroughput, + offerDefinition, + headers: offerResponse.headers + }; +}; diff --git a/src/Common/dataAccess/readCollectionOffer.ts b/src/Common/dataAccess/readCollectionOffer.ts index b98da68c6..f7d97a4b0 100644 --- a/src/Common/dataAccess/readCollectionOffer.ts +++ b/src/Common/dataAccess/readCollectionOffer.ts @@ -1,9 +1,6 @@ -import * as DataModels from "../../Contracts/DataModels"; import { AuthType } from "../../AuthType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; -import { HttpHeaders } from "../Constants"; -import { RequestOptions } from "@azure/cosmos/dist-esm"; -import { client } from "../CosmosClient"; +import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels"; import { handleError } from "../ErrorHandlingUtils"; import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; @@ -11,50 +8,22 @@ import { getCassandraTableThroughput } from "../../Utils/arm/generatedClients/20 import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; -import { readOffers } from "./readOffers"; +import { readOfferWithSDK } from "./readOfferWithSDK"; import { userContext } from "../../UserContext"; -export const readCollectionOffer = async ( - params: DataModels.ReadCollectionOfferParams -): Promise => { +export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise => { const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`); - let offerId = params.offerId; - if (!offerId) { - if (window.authType === AuthType.AAD && !userContext.useSDKOperations) { - try { - offerId = await getCollectionOfferIdWithARM(params.databaseId, params.collectionId); - } catch (error) { - clearMessage(); - if (error.code !== "NotFound") { - throw error; - } - return undefined; - } - } else { - offerId = await getCollectionOfferIdWithSDK(params.collectionResourceId); - if (!offerId) { - clearMessage(); - return undefined; - } - } - } - - const options: RequestOptions = { - initialHeaders: { - [HttpHeaders.populateCollectionThroughputInfo]: true - } - }; try { - const response = await client() - .offer(offerId) - .read(options); - return ( - response && { - ...response.resource, - headers: response.headers - } - ); + if ( + window.authType === AuthType.AAD && + !userContext.useSDKOperations && + userContext.defaultExperience !== DefaultAccountExperienceType.Table + ) { + return await readCollectionOfferWithARM(params.databaseId, params.collectionId); + } + + return await readOfferWithSDK(params.offerId, params.collectionResourceId); } catch (error) { handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`); throw error; @@ -63,61 +32,90 @@ export const readCollectionOffer = async ( } }; -const getCollectionOfferIdWithARM = async (databaseId: string, collectionId: string): Promise => { - let rpResponse; +const readCollectionOfferWithARM = async (databaseId: string, collectionId: string): Promise => { const subscriptionId = userContext.subscriptionId; const resourceGroup = userContext.resourceGroup; const accountName = userContext.databaseAccount.name; const defaultExperience = userContext.defaultExperience; - switch (defaultExperience) { - case DefaultAccountExperienceType.DocumentDB: - rpResponse = await getSqlContainerThroughput( - subscriptionId, - resourceGroup, - accountName, - databaseId, - collectionId - ); - break; - case DefaultAccountExperienceType.MongoDB: - rpResponse = await getMongoDBCollectionThroughput( - subscriptionId, - resourceGroup, - accountName, - databaseId, - collectionId - ); - break; - case DefaultAccountExperienceType.Cassandra: - rpResponse = await getCassandraTableThroughput( - subscriptionId, - resourceGroup, - accountName, - databaseId, - collectionId - ); - break; - case DefaultAccountExperienceType.Graph: - rpResponse = await getGremlinGraphThroughput( - subscriptionId, - resourceGroup, - accountName, - databaseId, - collectionId - ); - break; - case DefaultAccountExperienceType.Table: - rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId); - break; - default: - throw new Error(`Unsupported default experience type: ${defaultExperience}`); + + let rpResponse; + try { + switch (defaultExperience) { + case DefaultAccountExperienceType.DocumentDB: + rpResponse = await getSqlContainerThroughput( + subscriptionId, + resourceGroup, + accountName, + databaseId, + collectionId + ); + break; + case DefaultAccountExperienceType.MongoDB: + rpResponse = await getMongoDBCollectionThroughput( + subscriptionId, + resourceGroup, + accountName, + databaseId, + collectionId + ); + break; + case DefaultAccountExperienceType.Cassandra: + rpResponse = await getCassandraTableThroughput( + subscriptionId, + resourceGroup, + accountName, + databaseId, + collectionId + ); + break; + case DefaultAccountExperienceType.Graph: + rpResponse = await getGremlinGraphThroughput( + subscriptionId, + resourceGroup, + accountName, + databaseId, + collectionId + ); + break; + case DefaultAccountExperienceType.Table: + rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId); + break; + default: + throw new Error(`Unsupported default experience type: ${defaultExperience}`); + } + } catch (error) { + if (error.code !== "NotFound") { + throw error; + } + + return undefined; } - return rpResponse?.name; -}; + const resource = rpResponse?.properties?.resource; + if (resource) { + const offerId: string = rpResponse.name; + const minimumThroughput: number = + typeof resource.minimumThroughput === "string" + ? parseInt(resource.minimumThroughput) + : resource.minimumThroughput; + const autoscaleSettings = resource.autoscaleSettings; -const getCollectionOfferIdWithSDK = async (collectionResourceId: string): Promise => { - const offers = await readOffers(); - const offer = offers.find(offer => offer.resource === collectionResourceId); - return offer?.id; + if (autoscaleSettings) { + return { + id: offerId, + autoscaleMaxThroughput: autoscaleSettings.maxThroughput, + manualThroughput: undefined, + minimumThroughput + }; + } + + return { + id: offerId, + autoscaleMaxThroughput: undefined, + manualThroughput: resource.throughput, + minimumThroughput + }; + } + + return undefined; }; diff --git a/src/Common/dataAccess/readDatabaseOffer.ts b/src/Common/dataAccess/readDatabaseOffer.ts index 909b59c1a..1f4a67acd 100644 --- a/src/Common/dataAccess/readDatabaseOffer.ts +++ b/src/Common/dataAccess/readDatabaseOffer.ts @@ -1,51 +1,28 @@ -import * as DataModels from "../../Contracts/DataModels"; import { AuthType } from "../../AuthType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; -import { HttpHeaders } from "../Constants"; -import { RequestOptions } from "@azure/cosmos/dist-esm"; -import { client } from "../CosmosClient"; +import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels"; import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; import { handleError } from "../ErrorHandlingUtils"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; -import { readOffers } from "./readOffers"; +import { readOfferWithSDK } from "./readOfferWithSDK"; import { userContext } from "../../UserContext"; -export const readDatabaseOffer = async ( - params: DataModels.ReadDatabaseOfferParams -): Promise => { +export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise => { const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`); - let offerId = params.offerId; - if (!offerId) { - offerId = await (window.authType === AuthType.AAD && - !userContext.useSDKOperations && - userContext.defaultExperience !== DefaultAccountExperienceType.Table - ? getDatabaseOfferIdWithARM(params.databaseId) - : getDatabaseOfferIdWithSDK(params.databaseResourceId)); - if (!offerId) { - clearMessage(); - return undefined; - } - } - - const options: RequestOptions = { - initialHeaders: { - [HttpHeaders.populateCollectionThroughputInfo]: true - } - }; try { - const response = await client() - .offer(offerId) - .read(options); - return ( - response && { - ...response.resource, - headers: response.headers - } - ); + if ( + window.authType === AuthType.AAD && + !userContext.useSDKOperations && + userContext.defaultExperience !== DefaultAccountExperienceType.Table + ) { + return await readDatabaseOfferWithARM(params.databaseId); + } + + return await readOfferWithSDK(params.offerId, params.databaseResourceId); } catch (error) { handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`); throw error; @@ -54,13 +31,13 @@ export const readDatabaseOffer = async ( } }; -const getDatabaseOfferIdWithARM = async (databaseId: string): Promise => { - let rpResponse; +const readDatabaseOfferWithARM = async (databaseId: string): Promise => { const subscriptionId = userContext.subscriptionId; const resourceGroup = userContext.resourceGroup; const accountName = userContext.databaseAccount.name; const defaultExperience = userContext.defaultExperience; + let rpResponse; try { switch (defaultExperience) { case DefaultAccountExperienceType.DocumentDB: @@ -78,18 +55,39 @@ const getDatabaseOfferIdWithARM = async (databaseId: string): Promise => default: throw new Error(`Unsupported default experience type: ${defaultExperience}`); } - - return rpResponse?.name; } catch (error) { if (error.code !== "NotFound") { throw error; } + return undefined; } -}; -const getDatabaseOfferIdWithSDK = async (databaseResourceId: string): Promise => { - const offers = await readOffers(); - const offer = offers.find(offer => offer.resource === databaseResourceId); - return offer?.id; + const resource = rpResponse?.properties?.resource; + if (resource) { + const offerId: string = rpResponse.name; + const minimumThroughput: number = + typeof resource.minimumThroughput === "string" + ? parseInt(resource.minimumThroughput) + : resource.minimumThroughput; + const autoscaleSettings = resource.autoscaleSettings; + + if (autoscaleSettings) { + return { + id: offerId, + autoscaleMaxThroughput: autoscaleSettings.maxThroughput, + manualThroughput: undefined, + minimumThroughput + }; + } + + return { + id: offerId, + autoscaleMaxThroughput: undefined, + manualThroughput: resource.throughput, + minimumThroughput + }; + } + + return undefined; }; diff --git a/src/Common/dataAccess/readOfferWithSDK.ts b/src/Common/dataAccess/readOfferWithSDK.ts new file mode 100644 index 000000000..ce60a3c0f --- /dev/null +++ b/src/Common/dataAccess/readOfferWithSDK.ts @@ -0,0 +1,29 @@ +import { HttpHeaders } from "../Constants"; +import { Offer } from "../../Contracts/DataModels"; +import { RequestOptions } from "@azure/cosmos/dist-esm"; +import { client } from "../CosmosClient"; +import { parseSDKOfferResponse } from "../OfferUtility"; +import { readOffers } from "./readOffers"; + +export const readOfferWithSDK = async (offerId: string, resourceId: string): Promise => { + if (!offerId) { + const offers = await readOffers(); + const offer = offers.find(offer => offer.resource === resourceId); + + if (!offer) { + return undefined; + } + offerId = offer.id; + } + + const options: RequestOptions = { + initialHeaders: { + [HttpHeaders.populateCollectionThroughputInfo]: true + } + }; + const response = await client() + .offer(offerId) + .read(options); + + return parseSDKOfferResponse(response); +}; diff --git a/src/Common/dataAccess/readOffers.ts b/src/Common/dataAccess/readOffers.ts index 4afc452aa..4f5bc350b 100644 --- a/src/Common/dataAccess/readOffers.ts +++ b/src/Common/dataAccess/readOffers.ts @@ -1,9 +1,9 @@ -import { Offer } from "../../Contracts/DataModels"; +import { SDKOfferDefinition } from "../../Contracts/DataModels"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError, getErrorMessage } from "../ErrorHandlingUtils"; -export const readOffers = async (): Promise => { +export const readOffers = async (): Promise => { const clearMessage = logConsoleProgress(`Querying offers`); try { diff --git a/src/Common/dataAccess/updateOffer.ts b/src/Common/dataAccess/updateOffer.ts index 24cd150c8..5489ec12d 100644 --- a/src/Common/dataAccess/updateOffer.ts +++ b/src/Common/dataAccess/updateOffer.ts @@ -1,13 +1,14 @@ import { AuthType } from "../../AuthType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { HttpHeaders } from "../Constants"; -import { Offer, UpdateOfferParams } from "../../Contracts/DataModels"; +import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels"; import { OfferDefinition } from "@azure/cosmos"; import { RequestOptions } from "@azure/cosmos/dist-esm"; import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; +import { parseSDKOfferResponse } from "../OfferUtility"; import { readCollectionOffer } from "./readCollectionOffer"; import { readDatabaseOffer } from "./readDatabaseOffer"; import { @@ -373,21 +374,21 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd }; const updateOfferWithSDK = async (params: UpdateOfferParams): Promise => { - const currentOffer = params.currentOffer; - const newOffer: Offer = { + const sdkOfferDefinition = params.currentOffer.offerDefinition; + const newOffer: SDKOfferDefinition = { content: { offerThroughput: undefined, offerIsRUPerMinuteThroughputEnabled: false }, _etag: undefined, _ts: undefined, - _rid: currentOffer._rid, - _self: currentOffer._self, - id: currentOffer.id, - offerResourceId: currentOffer.offerResourceId, - offerVersion: currentOffer.offerVersion, - offerType: currentOffer.offerType, - resource: currentOffer.resource + _rid: sdkOfferDefinition._rid, + _self: sdkOfferDefinition._self, + id: sdkOfferDefinition.id, + offerResourceId: sdkOfferDefinition.offerResourceId, + offerVersion: sdkOfferDefinition.offerVersion, + offerType: sdkOfferDefinition.offerType, + resource: sdkOfferDefinition.resource }; if (params.autopilotThroughput) { @@ -415,5 +416,6 @@ const updateOfferWithSDK = async (params: UpdateOfferParams): Promise => .offer(params.currentOffer.id) // TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660) .replace((newOffer as unknown) as OfferDefinition, options); - return sdkResponse?.resource; + + return parseSDKOfferResponse(sdkResponse); }; diff --git a/src/Common/dataAccess/updateOfferThroughputBeyondLimit.test.ts b/src/Common/dataAccess/updateOfferThroughputBeyondLimit.test.ts deleted file mode 100644 index f90e45f02..000000000 --- a/src/Common/dataAccess/updateOfferThroughputBeyondLimit.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { updateOfferThroughputBeyondLimit } from "./updateOfferThroughputBeyondLimit"; - -describe("updateOfferThroughputBeyondLimit", () => { - it("should call fetch", async () => { - window.fetch = jest.fn(() => { - return { - ok: true - }; - }); - window.dataExplorer = { - logConsoleData: jest.fn(), - deleteInProgressConsoleDataWithId: jest.fn() - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; - await updateOfferThroughputBeyondLimit({ - subscriptionId: "foo", - resourceGroup: "foo", - databaseAccountName: "foo", - databaseName: "foo", - throughput: 1000000000, - offerIsRUPerMinuteThroughputEnabled: false - }); - expect(window.fetch).toHaveBeenCalled(); - }); -}); diff --git a/src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts b/src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts deleted file mode 100644 index 93a8f8737..000000000 --- a/src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Platform, configContext } from "../../ConfigContext"; -import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; -import { AutoPilotOfferSettings } from "../../Contracts/DataModels"; -import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; -import { HttpHeaders } from "../Constants"; -import { handleError } from "../ErrorHandlingUtils"; - -interface UpdateOfferThroughputRequest { - subscriptionId: string; - resourceGroup: string; - databaseAccountName: string; - databaseName: string; - collectionName?: string; - throughput: number; - offerIsRUPerMinuteThroughputEnabled: boolean; - offerAutopilotSettings?: AutoPilotOfferSettings; -} - -export async function updateOfferThroughputBeyondLimit(request: UpdateOfferThroughputRequest): Promise { - if (configContext.platform !== Platform.Portal) { - throw new Error("Updating throughput beyond specified limit is not supported on this platform"); - } - - const resourceDescriptionInfo = request.collectionName - ? `database ${request.databaseName} and container ${request.collectionName}` - : `database ${request.databaseName}`; - - const clearMessage = logConsoleProgress( - `Requesting increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}` - ); - - const url = `${configContext.BACKEND_ENDPOINT}/api/offerthroughputrequest/updatebeyondspecifiedlimit`; - const authorizationHeader = getAuthorizationHeader(); - - const response = await fetch(url, { - method: "POST", - body: JSON.stringify(request), - headers: { [authorizationHeader.header]: authorizationHeader.token, [HttpHeaders.contentType]: "application/json" } - }); - - if (response.ok) { - logConsoleInfo( - `Successfully requested an increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}` - ); - clearMessage(); - return undefined; - } - - const error = await response.json(); - handleError( - error, - "updateOfferThroughputBeyondLimit", - `Failed to request an increase in throughput for ${request.throughput}` - ); - clearMessage(); - throw error; -} diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 82779e428..350330176 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -208,12 +208,21 @@ export interface QueryMetrics { vmExecutionTime: any; } -export interface Offer extends Resource { +export interface Offer { + id: string; + autoscaleMaxThroughput: number; + manualThroughput: number; + minimumThroughput: number; + offerDefinition?: SDKOfferDefinition; + headers?: any; +} + +export interface SDKOfferDefinition extends Resource { offerVersion?: string; offerType?: string; content?: { offerThroughput: number; - offerIsRUPerMinuteThroughputEnabled: boolean; + offerIsRUPerMinuteThroughputEnabled?: boolean; collectionThroughputInfo?: OfferThroughputInfo; offerAutopilotSettings?: AutoPilotOfferSettings; }; @@ -221,10 +230,6 @@ export interface Offer extends Resource { offerResourceId?: string; } -export interface OfferWithHeaders extends Offer { - headers: any; -} - export interface CollectionQuotaInfo { storedProcedures: number; triggers: number; diff --git a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx index d4a3b1ec1..c230753e4 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx @@ -89,12 +89,11 @@ describe("SettingsComponent", () => { it("auto pilot helper functions pass on correct value", () => { const newCollection = { ...collection }; newCollection.offer = ko.observable({ - content: { - offerAutopilotSettings: { - maxThroughput: 10000 - } - } - } as DataModels.Offer); + autoscaleMaxThroughput: 10000, + manualThroughput: undefined, + minimumThroughput: 400, + id: "test" + }); const props = { ...baseProps }; props.settingsTab.collection = newCollection; @@ -187,21 +186,6 @@ describe("SettingsComponent", () => { expect(settingsComponentInstance.hasConflictResolution()).toEqual(true); }); - it("isOfferReplacePending", () => { - let settingsComponentInstance = new SettingsComponent(baseProps); - expect(settingsComponentInstance.isOfferReplacePending()).toEqual(undefined); - - const newCollection = { ...collection }; - newCollection.offer = ko.observable({ - headers: { "x-ms-offer-replace-pending": true } - } as DataModels.OfferWithHeaders); - const props = { ...baseProps }; - props.settingsTab.collection = newCollection; - - settingsComponentInstance = new SettingsComponent(props); - expect(settingsComponentInstance.isOfferReplacePending()).toEqual(true); - }); - it("save calls updateCollection, updateMongoDBCollectionThroughRP and updateOffer", async () => { const wrapper = shallow(); wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true, isMongoIndexingPolicySaveable: true }); diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index fb5672eac..fc9d88e05 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -2,28 +2,23 @@ import * as React from "react"; import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; import * as Constants from "../../../Common/Constants"; import * as DataModels from "../../../Contracts/DataModels"; -import * as SharedConstants from "../../../Shared/Constants"; import * as ViewModels from "../../../Contracts/ViewModels"; import DiscardIcon from "../../../../images/discard.svg"; import SaveIcon from "../../../../images/save-cosmos.svg"; import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; -import { RequestOptions } from "@azure/cosmos/dist-esm"; import Explorer from "../../Explorer"; import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; -import { userContext } from "../../../UserContext"; -import { updateOfferThroughputBeyondLimit } from "../../../Common/dataAccess/updateOfferThroughputBeyondLimit"; import SettingsTab from "../../Tabs/SettingsTabV2"; -import { mongoIndexingPolicyAADError, throughputUnit } from "./SettingsRenderUtils"; +import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils"; import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent"; import { MongoIndexingPolicyComponent, MongoIndexingPolicyComponentProps } from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent"; import { - getMaxRUs, hasDatabaseSharedThroughput, GeospatialConfigType, TtlType, @@ -275,19 +270,14 @@ export class SettingsComponent extends React.Component { - const offer = this.collection?.offer && this.collection.offer(); - const offerAutopilotSettings = offer?.content?.offerAutopilotSettings; + const autoscaleMaxThroughput = this.collection?.offer()?.autoscaleMaxThroughput; - if ( - offerAutopilotSettings && - offerAutopilotSettings.maxThroughput && - AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput) - ) { + if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) { this.setState({ isAutoPilotSelected: true, wasAutopilotOriginallySet: true, - autoPilotThroughput: offerAutopilotSettings.maxThroughput, - autoPilotThroughputBaseline: offerAutopilotSettings.maxThroughput + autoPilotThroughput: autoscaleMaxThroughput, + autoPilotThroughputBaseline: autoscaleMaxThroughput }); } }; @@ -305,12 +295,7 @@ export class SettingsComponent extends React.Component { - const offer = this.collection?.offer && this.collection.offer(); - return ( - offer && - Object.keys(offer).find(value => value === "headers") && - !!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending] - ); + return !!this.collection?.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending]; }; public onSaveClick = async (): Promise => { @@ -448,103 +433,34 @@ export class SettingsComponent extends React.Component autoscale - if (this.hasProvisioningTypeChanged()) { - headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true"; - delete newOffer.content.offerAutopilotSettings; - } else { - delete newOffer.content.offerThroughput; - } - } else { - this.setState({ - isAutoPilotSelected: false - }); - - // user has changed from autoscale --> provisioned - if (this.hasProvisioningTypeChanged()) { - headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true"; - } else { - delete newOffer.content.offerAutopilotSettings; - } - } - - if ( - getMaxRUs(this.collection, this.container) <= - SharedConstants.CollectionCreation.DefaultCollectionRUs1Million && - newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million && - this.container - ) { - const requestPayload = { - subscriptionId: userContext.subscriptionId, - databaseAccountName: userContext.databaseAccount.name, - resourceGroup: userContext.resourceGroup, - databaseName: this.collection.databaseId, - collectionName: this.collection.id(), - throughput: newThroughput, - offerIsRUPerMinuteThroughputEnabled: false - }; - - await updateOfferThroughputBeyondLimit(requestPayload); - this.collection.offer().content.offerThroughput = originalThroughputValue; - this.setState({ - isScaleSaveable: false, - isScaleDiscardable: false, - throughput: originalThroughputValue, - throughputBaseline: originalThroughputValue, - initialNotification: { - description: `Throughput update for ${newThroughput} ${throughputUnit}` - } as DataModels.Notification - }); - } else { - const updateOfferParams: DataModels.UpdateOfferParams = { - databaseId: this.collection.databaseId, - collectionId: this.collection.id(), - currentOffer: this.collection.offer(), - autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined, - manualThroughput: this.state.isAutoPilotSelected ? undefined : newThroughput - }; - if (this.hasProvisioningTypeChanged()) { - if (this.state.isAutoPilotSelected) { - updateOfferParams.migrateToAutoPilot = true; - } else { - updateOfferParams.migrateToManual = true; - } - } - const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams); - this.collection.offer(updatedOffer); - this.setState({ isScaleSaveable: false, isScaleDiscardable: false }); + const updateOfferParams: DataModels.UpdateOfferParams = { + databaseId: this.collection.databaseId, + collectionId: this.collection.id(), + currentOffer: this.collection.offer(), + autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined, + manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput + }; + if (this.hasProvisioningTypeChanged()) { if (this.state.isAutoPilotSelected) { - this.setState({ - autoPilotThroughput: updatedOffer.content.offerAutopilotSettings.maxThroughput, - autoPilotThroughputBaseline: updatedOffer.content.offerAutopilotSettings.maxThroughput - }); + updateOfferParams.migrateToAutoPilot = true; } else { - this.setState({ - throughput: updatedOffer.content.offerThroughput, - throughputBaseline: updatedOffer.content.offerThroughput - }); + updateOfferParams.migrateToManual = true; } } + const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams); + this.collection.offer(updatedOffer); + this.setState({ isScaleSaveable: false, isScaleDiscardable: false }); + if (this.state.isAutoPilotSelected) { + this.setState({ + autoPilotThroughput: updatedOffer.autoscaleMaxThroughput, + autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput + }); + } else { + this.setState({ + throughput: updatedOffer.manualThroughput, + throughputBaseline: updatedOffer.manualThroughput + }); + } } this.container.isRefreshingExplorer(false); this.setBaseline(); @@ -809,7 +725,7 @@ export class SettingsComponent extends React.ComponentSample Text)} diff --git a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx index d437d9f01..04305cd96 100644 --- a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx @@ -319,14 +319,13 @@ export const getThroughputApplyShortDelayMessage = ( throughput: number, throughputUnit: string, databaseName: string, - collectionName: string, - targetThroughput: number + collectionName: string ): JSX.Element => ( A request to increase the throughput is currently in progress. This operation will take some time to complete.
Database: {databaseName}, Container: {collectionName}{" "} - {getCurrentThroughput(isAutoscale, throughput, throughputUnit, targetThroughput)} + {getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx index d87a3b914..628ac46ea 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx @@ -44,7 +44,7 @@ describe("ScaleComponent", () => { } as DataModels.Notification }; - it("renders with correct intiial notification", () => { + it("renders with correct initial notification", () => { let wrapper = shallow(); expect(wrapper).toMatchSnapshot(); expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true); @@ -54,16 +54,13 @@ describe("ScaleComponent", () => { const newCollection = { ...collection }; const maxThroughput = 5000; - const targetMaxThroughput = 50000; newCollection.offer = ko.observable({ - content: { - offerAutopilotSettings: { - maxThroughput: maxThroughput, - targetMaxThroughput: targetMaxThroughput - } - }, + manualThroughput: undefined, + autoscaleMaxThroughput: maxThroughput, + minimumThroughput: 400, + id: "offer", headers: { "x-ms-offer-replace-pending": true } - } as DataModels.OfferWithHeaders); + }); const newProps = { ...baseProps, initialNotification: undefined as DataModels.Notification, @@ -73,7 +70,6 @@ describe("ScaleComponent", () => { expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true); expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false); expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput); - expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(targetMaxThroughput); }); it("autoScale disabled", () => { @@ -109,11 +105,6 @@ describe("ScaleComponent", () => { expect(scaleComponent.isAutoScaleEnabled()).toEqual(true); }); - it("getMaxRUThroughputInputLimit", () => { - const scaleComponent = new ScaleComponent(baseProps); - expect(scaleComponent.getMaxRUThroughputInputLimit()).toEqual(40000); - }); - it("getThroughputTitle", () => { let scaleComponent = new ScaleComponent(baseProps); expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)"); @@ -138,14 +129,8 @@ describe("ScaleComponent", () => { it("getThroughputWarningMessage", () => { const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000; - const throughputBeyondMaxRus = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million - 1000; - const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit }; - let scaleComponent = new ScaleComponent(newProps); + const scaleComponent = new ScaleComponent(newProps); expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage"); - - newProps.throughput = throughputBeyondMaxRus; - scaleComponent = new ScaleComponent(newProps); - expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputDelayedApplyWarningMessage"); }); }); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx index af4ee478b..47e0a4b65 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx @@ -12,10 +12,9 @@ import { throughputUnit, getThroughputApplyLongDelayMessage, getThroughputApplyShortDelayMessage, - updateThroughputBeyondLimitWarningMessage, - updateThroughputDelayedApplyWarningMessage + updateThroughputBeyondLimitWarningMessage } from "../SettingsRenderUtils"; -import { getMaxRUs, getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils"; +import { hasDatabaseSharedThroughput } from "../SettingsUtils"; import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils"; import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react"; import { configContext, Platform } from "../../../../ConfigContext"; @@ -62,11 +61,7 @@ export class ScaleComponent extends React.Component { }; private getStorageCapacityTitle = (): JSX.Element => { - // Mongo container with system partition key still treat as "Fixed" - const isFixed = - !this.props.collection.partitionKey || - (this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey); - const capacity: string = isFixed ? "Fixed" : "Unlimited"; + const capacity: string = this.props.isFixedContainer ? "Fixed" : "Unlimited"; return ( @@ -75,12 +70,26 @@ export class ScaleComponent extends React.Component { ); }; - public getMaxRUThroughputInputLimit = (): number => { - if (configContext.platform === Platform.Hosted && this.props.collection.partitionKey) { - return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million; + public getMaxRUs = (): number => { + if (this.props.container?.isTryCosmosDBSubscription()) { + return Constants.TryCosmosExperience.maxRU; } - return getMaxRUs(this.props.collection, this.props.container); + if (this.props.isFixedContainer) { + return SharedConstants.CollectionCreation.MaxRUPerPartition; + } + + return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million; + }; + + public getMinRUs = (): number => { + if (this.props.container?.isTryCosmosDBSubscription()) { + return SharedConstants.CollectionCreation.DefaultCollectionRUs400; + } + + return ( + this.props.collection.offer()?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400 + ); }; public getThroughputTitle = (): string => { @@ -88,11 +97,8 @@ export class ScaleComponent extends React.Component { return AutoPilotUtils.getAutoPilotHeaderText(); } - const minThroughput: string = getMinRUs(this.props.collection, this.props.container).toLocaleString(); - const maxThroughput: string = - this.canThroughputExceedMaximumValue() && !this.props.isFixedContainer - ? "unlimited" - : getMaxRUs(this.props.collection, this.props.container).toLocaleString(); + const minThroughput: string = this.getMinRUs().toLocaleString(); + const maxThroughput: string = !this.props.isFixedContainer ? "unlimited" : this.getMaxRUs().toLocaleString(); return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`; }; @@ -109,26 +115,15 @@ export class ScaleComponent extends React.Component { return this.getLongDelayMessage(); } - const offer = this.props.collection?.offer && this.props.collection.offer(); - if ( - offer && - Object.keys(offer).find(value => { - return value === "headers"; - }) && - !!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending] - ) { - const throughput = offer?.content?.offerAutopilotSettings?.maxThroughput; - - const targetThroughput = - offer.content?.offerAutopilotSettings?.targetMaxThroughput || offer?.content?.offerThroughput; - + const offer = this.props.collection?.offer(); + if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) { + const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput; return getThroughputApplyShortDelayMessage( this.props.isAutoPilotSelected, throughput, throughputUnit, this.props.collection.databaseId, - this.props.collection.id(), - targetThroughput + this.props.collection.id() ); } @@ -138,21 +133,12 @@ export class ScaleComponent extends React.Component { public getThroughputWarningMessage = (): JSX.Element => { const throughputExceedsBackendLimits: boolean = this.canThroughputExceedMaximumValue() && - getMaxRUs(this.props.collection, this.props.container) <= - SharedConstants.CollectionCreation.DefaultCollectionRUs1Million && this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million; if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) { return updateThroughputBeyondLimitWarningMessage; } - const throughputExceedsMaxValue: boolean = - !this.isEmulator && this.props.throughput > getMaxRUs(this.props.collection, this.props.container); - - if (throughputExceedsMaxValue && !!this.props.collection.partitionKey && !this.props.isFixedContainer) { - return updateThroughputDelayedApplyWarningMessage; - } - return undefined; }; @@ -183,8 +169,8 @@ export class ScaleComponent extends React.Component { throughput={this.props.throughput} throughputBaseline={this.props.throughputBaseline} onThroughputChange={this.props.onThroughputChange} - minimum={getMinRUs(this.props.collection, this.props.container)} - maximum={this.getMaxRUThroughputInputLimit()} + minimum={this.getMinRUs()} + maximum={this.getMaxRUs()} isEnabled={!hasDatabaseSharedThroughput(this.props.collection)} canExceedMaximumValue={this.canThroughputExceedMaximumValue()} label={this.getThroughputTitle()} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ScaleComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ScaleComponent.test.tsx.snap index 0b6aaa749..e47b3cb2e 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ScaleComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ScaleComponent.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ScaleComponent renders with correct intiial notification 1`] = ` +exports[`ScaleComponent renders with correct initial notification 1`] = ` { - it("getMaxRUs", () => { - expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4); - expect(getMaxRUs(collection, container)).toEqual(40000); - }); - - it("getMinRUs", () => { - expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4); - expect(getMinRUs(collection, container)).toEqual(6000); - }); - it("hasDatabaseSharedThroughput", () => { expect(hasDatabaseSharedThroughput(collection)).toEqual(undefined); diff --git a/src/Explorer/Controls/Settings/SettingsUtils.tsx b/src/Explorer/Controls/Settings/SettingsUtils.tsx index 9e29b73fe..06b29b4b3 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.tsx @@ -1,10 +1,6 @@ import * as ViewModels from "../../../Contracts/ViewModels"; import * as DataModels from "../../../Contracts/DataModels"; import * as Constants from "../../../Common/Constants"; -import * as SharedConstants from "../../../Shared/Constants"; -import * as PricingUtils from "../../../Utils/PricingUtils"; - -import Explorer from "../../Explorer"; import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types"; const zeroValue = 0; @@ -71,57 +67,6 @@ export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection): return database?.isDatabaseShared() && !collection.offer(); }; -export const getMaxRUs = (collection: ViewModels.Collection, container: Explorer): number => { - const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription() || false; - if (isTryCosmosDBSubscription) { - return Constants.TryCosmosExperience.maxRU; - } - - const numPartitionsFromOffer: number = - collection?.offer && collection.offer()?.content?.collectionThroughputInfo?.numPhysicalPartitions; - - const numPartitionsFromQuotaInfo: number = collection?.quotaInfo()?.numPartitions; - - const numPartitions = numPartitionsFromOffer ?? numPartitionsFromQuotaInfo ?? 1; - - return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions; -}; - -export const getMinRUs = (collection: ViewModels.Collection, container: Explorer): number => { - const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription(); - if (isTryCosmosDBSubscription) { - return SharedConstants.CollectionCreation.DefaultCollectionRUs400; - } - - const offerContent = collection?.offer && collection.offer()?.content; - - if (offerContent?.offerAutopilotSettings) { - return SharedConstants.CollectionCreation.DefaultCollectionRUs400; - } - - const collectionThroughputInfo: DataModels.OfferThroughputInfo = offerContent?.collectionThroughputInfo; - - if (collectionThroughputInfo?.minimumRUForCollection > 0) { - return collectionThroughputInfo.minimumRUForCollection; - } - - const numPartitions = collectionThroughputInfo?.numPhysicalPartitions ?? collection.quotaInfo()?.numPartitions; - - if (!numPartitions || numPartitions === 1) { - return SharedConstants.CollectionCreation.DefaultCollectionRUs400; - } - - const baseRU = SharedConstants.CollectionCreation.DefaultCollectionRUs400; - - const quotaInKb = collection.quotaInfo().collectionSize; - const quotaInGb = PricingUtils.usageInGB(quotaInKb); - - const perPartitionGBQuota: number = Math.max(10, quotaInGb / numPartitions); - const baseRUbyPartitions: number = ((numPartitions * perPartitionGBQuota) / 10) * 100; - - return Math.max(baseRU, baseRUbyPartitions); -}; - export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => { // Backend can contain different casing as it does case-insensitive comparisson if (!modeFromBackend) { diff --git a/src/Explorer/Controls/Settings/TestUtils.tsx b/src/Explorer/Controls/Settings/TestUtils.tsx index 92db0174c..ad02bea24 100644 --- a/src/Explorer/Controls/Settings/TestUtils.tsx +++ b/src/Explorer/Controls/Settings/TestUtils.tsx @@ -20,15 +20,11 @@ export const collection = ({ uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy, quotaInfo: ko.observable({} as DataModels.CollectionQuotaInfo), offer: ko.observable({ - content: { - offerThroughput: 10000, - offerIsRUPerMinuteThroughputEnabled: false, - collectionThroughputInfo: { - minimumRUForCollection: 6000, - numPhysicalPartitions: 4 - } as DataModels.OfferThroughputInfo - } - } as DataModels.Offer), + autoscaleMaxThroughput: undefined, + manualThroughput: 10000, + minimumThroughput: 6000, + id: "offer" + }), conflictResolutionPolicy: ko.observable( {} as DataModels.ConflictResolutionPolicy ), diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap index 90e5c8af8..5d0a46560 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap @@ -227,7 +227,7 @@ exports[`SettingsUtils functions render 1`] = ` , Container: sampleCollection - , Current manual throughput: 1000 RU/s, Target manual throughput: 2000 + , Current manual throughput: 1000 RU/s string = ? `Current autoscale throughput: ${Math.round(throughput / 10)} - ${throughput} RU/s` : `Current manual throughput: ${throughput} RU/s`; -const throughputApplyDelayedMessage = (isAutoscale: boolean, throughput: number, databaseName: string) => - `The request to increase the throughput has successfully been submitted. - This operation will take 1-3 business days to complete. View the latest status in Notifications.
- Database: ${databaseName}, ${currentThroughput(isAutoscale, throughput)}`; - const throughputApplyShortDelayMessage = (isAutoscale: boolean, throughput: number, databaseName: string) => `A request to increase the throughput is currently in progress. This operation will take some time to complete.
@@ -66,8 +59,8 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. public displayedError: ko.Observable; public isTemplateReady: ko.Observable; public minRUAnotationVisible: ko.Computed; - public minRUs: ko.Computed; - public maxRUs: ko.Computed; + public minRUs: ko.Observable; + public maxRUs: ko.Observable; public maxRUsText: ko.PureComputed; public maxRUThroughputInputLimit: ko.Computed; public notificationStatusInfo: ko.Observable; @@ -92,7 +85,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. private _hasProvisioningTypeChanged: ko.Computed; private _wasAutopilotOriginallySet: ko.Observable; - private _offerReplacePending: ko.Computed; + private _offerReplacePending: ko.Observable; private container: Explorer; constructor(options: ViewModels.TabOptions) { @@ -111,15 +104,14 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. this._wasAutopilotOriginallySet = ko.observable(false); this.isAutoPilotSelected = editable.observable(false); this.autoPilotThroughput = editable.observable(); - const offer = this.database && this.database.offer && this.database.offer(); - const offerAutopilotSettings = offer && offer.content && offer.content.offerAutopilotSettings; this.userCanChangeProvisioningTypes = ko.observable(true); - if (offerAutopilotSettings && offerAutopilotSettings.maxThroughput) { - if (AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)) { + const autoscaleMaxThroughput = this.database?.offer()?.autoscaleMaxThroughput; + if (autoscaleMaxThroughput) { + if (AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) { this._wasAutopilotOriginallySet(true); this.isAutoPilotSelected(true); - this.autoPilotThroughput(offerAutopilotSettings.maxThroughput); + this.autoPilotThroughput(autoscaleMaxThroughput); } } @@ -205,45 +197,15 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. return this._hasProvisioningTypeChanged() && !this._wasAutopilotOriginallySet(); }); - this.minRUs = ko.computed(() => { - const offerContent = - this.database && this.database.offer && this.database.offer() && this.database.offer().content; - - // TODO: backend is returning 1,000,000 as min throughput which seems wrong - // Setting to min throughput to not block and let the backend pass or fail - if (offerContent && offerContent.offerAutopilotSettings) { - return 400; - } - - const collectionThroughputInfo: DataModels.OfferThroughputInfo = - offerContent && offerContent.collectionThroughputInfo; - - if (collectionThroughputInfo && !!collectionThroughputInfo.minimumRUForCollection) { - return collectionThroughputInfo.minimumRUForCollection; - } - const throughputDefaults = this.container.collectionCreationDefaults.throughput; - return throughputDefaults.unlimitedmin; - }); + this.minRUs = ko.observable( + this.database.offer()?.minimumThroughput || this.container.collectionCreationDefaults.throughput.unlimitedmin + ); this.minRUAnotationVisible = ko.computed(() => { return PricingUtils.isLargerThanDefaultMinRU(this.minRUs()); }); - this.maxRUs = ko.computed(() => { - const collectionThroughputInfo: DataModels.OfferThroughputInfo = - this.database && - this.database.offer && - this.database.offer() && - this.database.offer().content && - this.database.offer().content.collectionThroughputInfo; - const numPartitions = collectionThroughputInfo && collectionThroughputInfo.numPhysicalPartitions; - if (!!numPartitions) { - return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions; - } - - const throughputDefaults = this.container.collectionCreationDefaults.throughput; - return throughputDefaults.unlimitedmax; - }); + this.maxRUs = ko.observable(this.container.collectionCreationDefaults.throughput.unlimitedmax); this.maxRUThroughputInputLimit = ko.pureComputed(() => { if (configContext.platform === Platform.Hosted) { @@ -269,37 +231,23 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. return this.throughputTitle() + this.requestUnitsUsageCost(); }); this.pendingNotification = ko.observable(); - this._offerReplacePending = ko.pureComputed(() => { - const offer = this.database && this.database.offer && this.database.offer(); - return ( - offer && - offer.hasOwnProperty("headers") && - !!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending] - ); - }); + this._offerReplacePending = ko.observable( + !!this.database.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending] + ); this.notificationStatusInfo = ko.observable(""); this.shouldShowNotificationStatusPrompt = ko.computed(() => this.notificationStatusInfo().length > 0); this.warningMessage = ko.computed(() => { - const offer = this.database && this.database.offer && this.database.offer(); - if (this.overrideWithProvisionedThroughputSettings()) { return AutoPilotUtils.manualToAutoscaleDisclaimer; } - if ( - offer && - offer.hasOwnProperty("headers") && - !!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending] - ) { - const throughput = offer.content.offerAutopilotSettings - ? offer.content.offerAutopilotSettings.maxThroughput - : offer.content.offerThroughput; - + const offer = this.database.offer(); + if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) { + const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput; return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id()); } if ( - this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million && this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million && this.canThroughputExceedMaximumValue() ) { @@ -432,60 +380,26 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. const headerOptions: RequestOptions = { initialHeaders: {} }; try { - if (this.isAutoPilotSelected()) { - const updateOfferParams: DataModels.UpdateOfferParams = { - databaseId: this.database.id(), - currentOffer: this.database.offer(), - autopilotThroughput: this.autoPilotThroughput(), - manualThroughput: undefined, - migrateToAutoPilot: this._hasProvisioningTypeChanged() - }; + const updateOfferParams: DataModels.UpdateOfferParams = { + databaseId: this.database.id(), + currentOffer: this.database.offer(), + autopilotThroughput: this.isAutoPilotSelected() ? this.autoPilotThroughput() : undefined, + manualThroughput: this.isAutoPilotSelected() ? undefined : this.throughput() + }; - const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams); - this.database.offer(updatedOffer); - this.database.offer.valueHasMutated(); - this._wasAutopilotOriginallySet(this.isAutoPilotSelected()); - } else { - if (this.throughput.editableIsDirty() || this.isAutoPilotSelected.editableIsDirty()) { - const originalThroughputValue = this.throughput.getEditableOriginalValue(); - const newThroughput = this.throughput(); - - if ( - this.canThroughputExceedMaximumValue() && - this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million && - this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million - ) { - const requestPayload = { - subscriptionId: userContext.subscriptionId, - databaseAccountName: userContext.databaseAccount.name, - resourceGroup: userContext.resourceGroup, - databaseName: this.database.id(), - throughput: newThroughput, - offerIsRUPerMinuteThroughputEnabled: false - }; - await updateOfferThroughputBeyondLimit(requestPayload); - this.database.offer().content.offerThroughput = originalThroughputValue; - this.throughput(originalThroughputValue); - this.notificationStatusInfo( - throughputApplyDelayedMessage(this.isAutoPilotSelected(), newThroughput, this.database.id()) - ); - this.throughput.valueHasMutated(); // force component re-render - } else { - const updateOfferParams: DataModels.UpdateOfferParams = { - databaseId: this.database.id(), - currentOffer: this.database.offer(), - autopilotThroughput: undefined, - manualThroughput: newThroughput, - migrateToManual: this._hasProvisioningTypeChanged() - }; - - const updatedOffer = await updateOffer(updateOfferParams); - this._wasAutopilotOriginallySet(this.isAutoPilotSelected()); - this.database.offer(updatedOffer); - this.database.offer.valueHasMutated(); - } + if (this._hasProvisioningTypeChanged()) { + if (this.isAutoPilotSelected()) { + updateOfferParams.migrateToAutoPilot = true; + } else { + updateOfferParams.migrateToManual = true; } } + + const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams); + this.database.offer(updatedOffer); + this.database.offer.valueHasMutated(); + this._setBaseline(); + this._wasAutopilotOriginallySet(this.isAutoPilotSelected()); } catch (error) { this.container.isRefreshingExplorer(false); this.isExecutionError(true); @@ -527,15 +441,10 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. private _setBaseline() { const offer = this.database && this.database.offer && this.database.offer(); - const offerThroughput = offer.content && offer.content.offerThroughput; - const offerAutopilotSettings = offer && offer.content && offer.content.offerAutopilotSettings; - - this.throughput.setBaseline(offerThroughput); + this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(offer.autoscaleMaxThroughput)); + this.autoPilotThroughput.setBaseline(offer.autoscaleMaxThroughput); + this.throughput.setBaseline(offer.manualThroughput); this.userCanChangeProvisioningTypes(true); - - const maxThroughputForAutoPilot = offerAutopilotSettings && offerAutopilotSettings.maxThroughput; - this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(maxThroughputForAutoPilot)); - this.autoPilotThroughput.setBaseline(maxThroughputForAutoPilot || AutoPilotUtils.minAutoPilotThroughput); } protected getTabsButtons(): CommandButtonComponentProps[] { diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 307ff2ff6..c2579ef7b 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -1295,8 +1295,7 @@ export default class Collection implements ViewModels.Collection { databaseAccountName: this.container.databaseAccount().name, databaseName: this.databaseId, collectionName: this.id(), - defaultExperience: this.container.defaultExperience(), - offerVersion: this.offer()?.offerVersion + defaultExperience: this.container.defaultExperience() }, startKey ); diff --git a/src/Utils/AutoPilotUtils.ts b/src/Utils/AutoPilotUtils.ts index 1708eeea7..18a390100 100644 --- a/src/Utils/AutoPilotUtils.ts +++ b/src/Utils/AutoPilotUtils.ts @@ -7,15 +7,6 @@ export const minAutoPilotThroughput = 4000; export const autoPilotIncrementStep = 1000; -export function isValidV3AutoPilotOffer(offer: Offer): boolean { - const maxThroughput = - offer && - offer.content && - offer.content.offerAutopilotSettings && - offer.content.offerAutopilotSettings.maxThroughput; - return isValidAutoPilotThroughput(maxThroughput); -} - export function isValidAutoPilotThroughput(maxThroughput: number): boolean { if (!maxThroughput) { return false; diff --git a/src/Utils/OfferUtils.test.ts b/src/Utils/OfferUtils.test.ts deleted file mode 100644 index c11f3b32f..000000000 --- a/src/Utils/OfferUtils.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import * as Constants from "../../src/Common/Constants"; -import * as DataModels from "../../src/Contracts/DataModels"; -import { OfferUtils } from "../../src/Utils/OfferUtils"; - -describe("OfferUtils tests", () => { - const offerV1: DataModels.Offer = { - _rid: "", - _self: "", - _ts: 0, - _etag: "", - id: "v1", - offerVersion: Constants.OfferVersions.V1, - offerType: "Standard", - offerResourceId: "", - content: null, - resource: "" - }; - - const offerV2: DataModels.Offer = { - _rid: "", - _self: "", - _ts: 0, - _etag: "", - id: "v1", - offerVersion: Constants.OfferVersions.V2, - offerType: "Standard", - offerResourceId: "", - content: null, - resource: "" - }; - - describe("isOfferV1()", () => { - it("should return true for V1", () => { - expect(OfferUtils.isOfferV1(offerV1)).toBeTruthy(); - }); - - it("should return false for V2", () => { - expect(OfferUtils.isOfferV1(offerV2)).toBeFalsy(); - }); - }); - - describe("isNotOfferV1()", () => { - it("should return true for V2", () => { - expect(OfferUtils.isNotOfferV1(offerV2)).toBeTruthy(); - }); - - it("should return false for V1", () => { - expect(OfferUtils.isNotOfferV1(offerV1)).toBeFalsy(); - }); - }); -}); diff --git a/src/Utils/OfferUtils.ts b/src/Utils/OfferUtils.ts deleted file mode 100644 index 0d03516a6..000000000 --- a/src/Utils/OfferUtils.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as Constants from "../Common/Constants"; -import * as DataModels from "../Contracts/DataModels"; - -export class OfferUtils { - public static isOfferV1(offer: DataModels.Offer): boolean { - return !offer || offer.offerVersion !== Constants.OfferVersions.V2; - } - - public static isNotOfferV1(offer: DataModels.Offer): boolean { - return !OfferUtils.isOfferV1(offer); - } -} diff --git a/tsconfig.strict.json b/tsconfig.strict.json index c59b7391e..c2cb50fad 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -76,7 +76,6 @@ "./src/Utils/BlobUtils.ts", "./src/Utils/GitHubUtils.ts", "./src/Utils/MessageValidation.ts", - "./src/Utils/OfferUtils.ts", "./src/Utils/StringUtils.ts", "./src/Utils/WindowUtils.ts", "./src/Utils/arm/generatedClients/2020-04-01/types.ts",