From 038262824947cf6e54edbee3e30f1e0facbbf5f8 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Wed, 7 Oct 2020 15:25:21 -0700 Subject: [PATCH] Move update offers call to RP --- src/Common/DataAccessUtilityBase.ts | 16 - src/Common/DocumentClientUtilityBase.ts | 35 -- src/Common/dataAccess/readDatabaseOffer.ts | 72 ++- src/Common/dataAccess/updateCollection.ts | 11 +- src/Common/dataAccess/updateOffer.ts | 420 ++++++++++++++++++ src/Contracts/DataModels.ts | 10 + .../Settings/SettingsComponent.test.tsx | 4 +- .../Controls/Settings/SettingsComponent.tsx | 18 +- src/Explorer/Tabs/DatabaseSettingsTab.ts | 219 +++------ src/Explorer/Tabs/SettingsTab.ts | 18 +- src/Explorer/Tree/Database.ts | 7 +- 11 files changed, 580 insertions(+), 250 deletions(-) create mode 100644 src/Common/dataAccess/updateOffer.ts diff --git a/src/Common/DataAccessUtilityBase.ts b/src/Common/DataAccessUtilityBase.ts index 31eb162bb..b816aea63 100644 --- a/src/Common/DataAccessUtilityBase.ts +++ b/src/Common/DataAccessUtilityBase.ts @@ -72,22 +72,6 @@ export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.Partiti return [partitionKeyValue]; } -export function updateOffer( - offer: DataModels.Offer, - newOffer: DataModels.Offer, - options?: RequestOptions -): Q.Promise { - return Q( - client() - .offer(offer.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) - .then(response => { - return Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => response.resource); - }) - ); -} - export function updateDocument( collection: ViewModels.CollectionBase, documentId: DocumentId, diff --git a/src/Common/DocumentClientUtilityBase.ts b/src/Common/DocumentClientUtilityBase.ts index f60d2b09d..fc021b070 100644 --- a/src/Common/DocumentClientUtilityBase.ts +++ b/src/Common/DocumentClientUtilityBase.ts @@ -157,41 +157,6 @@ export function updateDocument( return deferred.promise; } -export function updateOffer( - offer: DataModels.Offer, - newOffer: DataModels.Offer, - options: RequestOptions -): Q.Promise { - var deferred = Q.defer(); - const clearMessage = logConsoleProgress(`Updating offer for resource ${offer.resource}`); - DataAccessUtilityBase.updateOffer(offer, newOffer, options) - .then( - (replacedOffer: DataModels.Offer) => { - logConsoleInfo(`Successfully updated offer for resource ${offer.resource}`); - deferred.resolve(replacedOffer); - }, - (error: any) => { - logConsoleError(`Error updating offer for resource ${offer.resource}: ${JSON.stringify(error)}`); - Logger.logError( - JSON.stringify({ - oldOffer: offer, - newOffer: newOffer, - error: error - }), - "UpdateOffer", - error.code - ); - sendNotificationForError(error); - deferred.reject(error); - } - ) - .finally(() => { - clearMessage(); - }); - - return deferred.promise; -} - export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise { var deferred = Q.defer(); const entityName = getEntityName(); diff --git a/src/Common/dataAccess/readDatabaseOffer.ts b/src/Common/dataAccess/readDatabaseOffer.ts index bd31d17e5..0aec98f51 100644 --- a/src/Common/dataAccess/readDatabaseOffer.ts +++ b/src/Common/dataAccess/readDatabaseOffer.ts @@ -17,29 +17,19 @@ import { userContext } from "../../UserContext"; export const readDatabaseOffer = async ( params: DataModels.ReadDatabaseOfferParams ): Promise => { + if (userContext.defaultExperience === DefaultAccountExperienceType.Table) { + throw new Error("Reading database offer is not allowed for tables accounts"); + } + const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`); let offerId = params.offerId; if (!offerId) { - if ( - window.authType === AuthType.AAD && - !userContext.useSDKOperations && - userContext.defaultExperience !== DefaultAccountExperienceType.Table - ) { - try { - offerId = await getDatabaseOfferIdWithARM(params.databaseId); - } catch (error) { - clearMessage(); - if (error.code !== "NotFound") { - throw new error(); - } - return undefined; - } - } else { - offerId = await getDatabaseOfferIdWithSDK(params.databaseResourceId); - if (!offerId) { - clearMessage(); - return undefined; - } + offerId = await (window.authType === AuthType.AAD && !userContext.useSDKOperations + ? getDatabaseOfferIdWithARM(params.databaseId) + : getDatabaseOfferIdWithSDK(params.databaseResourceId)); + if (!offerId) { + clearMessage(); + return undefined; } } @@ -75,24 +65,32 @@ const getDatabaseOfferIdWithARM = async (databaseId: string): Promise => const resourceGroup = userContext.resourceGroup; const accountName = userContext.databaseAccount.name; const defaultExperience = userContext.defaultExperience; - switch (defaultExperience) { - case DefaultAccountExperienceType.DocumentDB: - rpResponse = await getSqlDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId); - break; - case DefaultAccountExperienceType.MongoDB: - rpResponse = await getMongoDBDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId); - break; - case DefaultAccountExperienceType.Cassandra: - rpResponse = await getCassandraKeyspaceThroughput(subscriptionId, resourceGroup, accountName, databaseId); - break; - case DefaultAccountExperienceType.Graph: - rpResponse = await getGremlinDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId); - break; - default: - throw new Error(`Unsupported default experience type: ${defaultExperience}`); - } - return rpResponse?.name; + try { + switch (defaultExperience) { + case DefaultAccountExperienceType.DocumentDB: + rpResponse = await getSqlDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId); + break; + case DefaultAccountExperienceType.MongoDB: + rpResponse = await getMongoDBDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId); + break; + case DefaultAccountExperienceType.Cassandra: + rpResponse = await getCassandraKeyspaceThroughput(subscriptionId, resourceGroup, accountName, databaseId); + break; + case DefaultAccountExperienceType.Graph: + rpResponse = await getGremlinDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId); + break; + 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 => { diff --git a/src/Common/dataAccess/updateCollection.ts b/src/Common/dataAccess/updateCollection.ts index 794c10003..0e276b7df 100644 --- a/src/Common/dataAccess/updateCollection.ts +++ b/src/Common/dataAccess/updateCollection.ts @@ -41,6 +41,7 @@ export async function updateCollection( try { if ( window.authType === AuthType.AAD && + !userContext.useSDKOperations && userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB && userContext.defaultExperience !== DefaultAccountExperienceType.Table ) { @@ -52,16 +53,18 @@ export async function updateCollection( .replace(newCollection as ContainerDefinition, options); collection = sdkResponse.resource as Collection; } + + logConsoleInfo(`Successfully updated container ${collectionId}`); + await refreshCachedResources(); + return collection; } catch (error) { logConsoleError(`Failed to update container ${collectionId}: ${JSON.stringify(error)}`); logError(JSON.stringify(error), "UpdateCollection", error.code); sendNotificationForError(error); throw error; + } finally { + clearMessage(); } - logConsoleInfo(`Successfully updated container ${collectionId}`); - clearMessage(); - await refreshCachedResources(); - return collection; } async function updateCollectionWithARM( diff --git a/src/Common/dataAccess/updateOffer.ts b/src/Common/dataAccess/updateOffer.ts new file mode 100644 index 000000000..733c183c4 --- /dev/null +++ b/src/Common/dataAccess/updateOffer.ts @@ -0,0 +1,420 @@ +import { AuthType } from "../../AuthType"; +import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; +import { HttpHeaders } from "../Constants"; +import { Offer, 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 { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; +import { logError } from "../Logger"; +import { readCollectionOffer } from "./readCollectionOffer"; +import { readDatabaseOffer } from "./readDatabaseOffer"; +import { refreshCachedOffers, refreshCachedResources } from "../DataAccessUtilityBase"; +import { sendNotificationForError } from "./sendNotificationForError"; +import { + updateSqlDatabaseThroughput, + migrateSqlDatabaseToAutoscale, + migrateSqlDatabaseToManualThroughput, + migrateSqlContainerToAutoscale, + migrateSqlContainerToManualThroughput, + updateSqlContainerThroughput +} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +import { + updateCassandraKeyspaceThroughput, + migrateCassandraKeyspaceToAutoscale, + migrateCassandraKeyspaceToManualThroughput, + migrateCassandraTableToAutoscale, + migrateCassandraTableToManualThroughput, + updateCassandraTableThroughput +} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; +import { + updateMongoDBDatabaseThroughput, + migrateMongoDBDatabaseToAutoscale, + migrateMongoDBDatabaseToManualThroughput, + migrateMongoDBCollectionToAutoscale, + migrateMongoDBCollectionToManualThroughput, + updateMongoDBCollectionThroughput +} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; +import { + updateGremlinDatabaseThroughput, + migrateGremlinDatabaseToAutoscale, + migrateGremlinDatabaseToManualThroughput, + migrateGremlinGraphToAutoscale, + migrateGremlinGraphToManualThroughput, + updateGremlinGraphThroughput +} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; +import { userContext } from "../../UserContext"; +import { + migrateTableToAutoscale, + migrateTableToManualThroughput, + updateTableThroughput +} from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; + +export const updateOffer = async (params: UpdateOfferParams): Promise => { + let updatedOffer: Offer; + const offerResourceText: string = params.collectionId + ? `collection ${params.collectionId}` + : `database ${params.databaseId}`; + const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`); + + try { + if (window.authType === AuthType.AAD && !userContext.useSDKOperations) { + updatedOffer = await (params.collectionId + ? updateCollectionOfferWithARM(params) + : updateDatabaseOfferWithARM(params)); + } else { + updatedOffer = await updateOfferWithSDK(params); + } + await refreshCachedOffers(); + await refreshCachedResources(); + logConsoleInfo(`Successfully updated offer for ${offerResourceText}`); + return updatedOffer; + } catch (error) { + logConsoleError(`Error updating offer for ${offerResourceText}: ${JSON.stringify(error)}`); + logError(JSON.stringify(error), "UpdateCollection", error.code); + sendNotificationForError(error); + throw error; + } finally { + clearMessage(); + } +}; + +const updateCollectionOfferWithARM = async (params: UpdateOfferParams): Promise => { + try { + switch (userContext.defaultExperience) { + case DefaultAccountExperienceType.DocumentDB: + await updateSqlContainerOffer(params); + break; + case DefaultAccountExperienceType.MongoDB: + await updateMongoCollectionOffer(params); + break; + case DefaultAccountExperienceType.Cassandra: + await updateCassandraTableOffer(params); + break; + case DefaultAccountExperienceType.Graph: + await updateGremlinGraphOffer(params); + break; + case DefaultAccountExperienceType.Table: + await updateTableOffer(params); + break; + default: + throw new Error(`Unsupported default experience type: ${userContext.defaultExperience}`); + } + } catch (error) { + if (error.code !== "MethodNotAllowed") { + throw error; + } + } + + return await readCollectionOffer({ + collectionId: params.collectionId, + databaseId: params.databaseId, + offerId: params.currentOffer.id + }); +}; + +const updateDatabaseOfferWithARM = async (params: UpdateOfferParams): Promise => { + if (userContext.defaultExperience === DefaultAccountExperienceType.Table) { + throw new Error("Updating database offer is not allowed for tables accounts"); + } + + try { + switch (userContext.defaultExperience) { + case DefaultAccountExperienceType.DocumentDB: + await updateSqlDatabaseOffer(params); + break; + case DefaultAccountExperienceType.MongoDB: + await updateMongoDatabaseOffer(params); + break; + case DefaultAccountExperienceType.Cassandra: + await updateCassandraKeyspaceOffer(params); + break; + case DefaultAccountExperienceType.Graph: + await updateGremlinDatabaseOffer(params); + break; + default: + throw new Error(`Unsupported default experience type: ${userContext.defaultExperience}`); + } + } catch (error) { + if (error.code !== "MethodNotAllowed") { + throw error; + } + } + + return await readDatabaseOffer({ + databaseId: params.databaseId, + offerId: params.currentOffer.id + }); +}; + +const updateSqlContainerOffer = async (params: UpdateOfferParams): Promise => { + const subscriptionId = userContext.subscriptionId; + const resourceGroup = userContext.resourceGroup; + const accountName = userContext.databaseAccount.name; + + if (params.migrateToAutoPilot) { + await migrateSqlContainerToAutoscale( + subscriptionId, + resourceGroup, + accountName, + params.databaseId, + params.collectionId + ); + } else if (params.migrateToManual) { + await migrateSqlContainerToManualThroughput( + subscriptionId, + resourceGroup, + accountName, + params.databaseId, + params.collectionId + ); + } else { + const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params); + await updateSqlContainerThroughput( + subscriptionId, + resourceGroup, + accountName, + params.databaseId, + params.collectionId, + body + ); + } +}; + +const updateMongoCollectionOffer = async (params: UpdateOfferParams): Promise => { + const subscriptionId = userContext.subscriptionId; + const resourceGroup = userContext.resourceGroup; + const accountName = userContext.databaseAccount.name; + + if (params.migrateToAutoPilot) { + await migrateMongoDBCollectionToAutoscale( + subscriptionId, + resourceGroup, + accountName, + params.databaseId, + params.collectionId + ); + } else if (params.migrateToManual) { + await migrateMongoDBCollectionToManualThroughput( + subscriptionId, + resourceGroup, + accountName, + params.databaseId, + params.collectionId + ); + } else { + const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params); + await updateMongoDBCollectionThroughput( + subscriptionId, + resourceGroup, + accountName, + params.databaseId, + params.collectionId, + body + ); + } +}; + +const updateCassandraTableOffer = async (params: UpdateOfferParams): Promise => { + const subscriptionId = userContext.subscriptionId; + const resourceGroup = userContext.resourceGroup; + const accountName = userContext.databaseAccount.name; + + if (params.migrateToAutoPilot) { + await migrateCassandraTableToAutoscale( + subscriptionId, + resourceGroup, + accountName, + params.databaseId, + params.collectionId + ); + } else if (params.migrateToManual) { + await migrateCassandraTableToManualThroughput( + subscriptionId, + resourceGroup, + accountName, + params.databaseId, + params.collectionId + ); + } else { + const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params); + await updateCassandraTableThroughput( + subscriptionId, + resourceGroup, + accountName, + params.databaseId, + params.collectionId, + body + ); + } +}; + +const updateGremlinGraphOffer = async (params: UpdateOfferParams): Promise => { + const subscriptionId = userContext.subscriptionId; + const resourceGroup = userContext.resourceGroup; + const accountName = userContext.databaseAccount.name; + + if (params.migrateToAutoPilot) { + await migrateGremlinGraphToAutoscale( + subscriptionId, + resourceGroup, + accountName, + params.databaseId, + params.collectionId + ); + } else if (params.migrateToManual) { + await migrateGremlinGraphToManualThroughput( + subscriptionId, + resourceGroup, + accountName, + params.databaseId, + params.collectionId + ); + } else { + const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params); + await updateGremlinGraphThroughput( + subscriptionId, + resourceGroup, + accountName, + params.databaseId, + params.collectionId, + body + ); + } +}; + +const updateTableOffer = async (params: UpdateOfferParams): Promise => { + const subscriptionId = userContext.subscriptionId; + const resourceGroup = userContext.resourceGroup; + const accountName = userContext.databaseAccount.name; + + if (params.migrateToAutoPilot) { + await migrateTableToAutoscale(subscriptionId, resourceGroup, accountName, params.collectionId); + } else if (params.migrateToManual) { + await migrateTableToManualThroughput(subscriptionId, resourceGroup, accountName, params.collectionId); + } else { + const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params); + await updateTableThroughput(subscriptionId, resourceGroup, accountName, params.collectionId, body); + } +}; + +const updateSqlDatabaseOffer = async (params: UpdateOfferParams): Promise => { + const subscriptionId = userContext.subscriptionId; + const resourceGroup = userContext.resourceGroup; + const accountName = userContext.databaseAccount.name; + + if (params.migrateToAutoPilot) { + await migrateSqlDatabaseToAutoscale(subscriptionId, resourceGroup, accountName, params.databaseId); + } else if (params.migrateToManual) { + await migrateSqlDatabaseToManualThroughput(subscriptionId, resourceGroup, accountName, params.databaseId); + } else { + const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params); + await updateSqlDatabaseThroughput(subscriptionId, resourceGroup, accountName, params.databaseId, body); + } +}; + +const updateMongoDatabaseOffer = async (params: UpdateOfferParams): Promise => { + const subscriptionId = userContext.subscriptionId; + const resourceGroup = userContext.resourceGroup; + const accountName = userContext.databaseAccount.name; + + if (params.migrateToAutoPilot) { + await migrateMongoDBDatabaseToAutoscale(subscriptionId, resourceGroup, accountName, params.databaseId); + } else if (params.migrateToManual) { + await migrateMongoDBDatabaseToManualThroughput(subscriptionId, resourceGroup, accountName, params.databaseId); + } else { + const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params); + await updateMongoDBDatabaseThroughput(subscriptionId, resourceGroup, accountName, params.databaseId, body); + } +}; + +const updateCassandraKeyspaceOffer = async (params: UpdateOfferParams): Promise => { + const subscriptionId = userContext.subscriptionId; + const resourceGroup = userContext.resourceGroup; + const accountName = userContext.databaseAccount.name; + + if (params.migrateToAutoPilot) { + await migrateCassandraKeyspaceToAutoscale(subscriptionId, resourceGroup, accountName, params.databaseId); + } else if (params.migrateToManual) { + await migrateCassandraKeyspaceToManualThroughput(subscriptionId, resourceGroup, accountName, params.databaseId); + } else { + const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params); + await updateCassandraKeyspaceThroughput(subscriptionId, resourceGroup, accountName, params.databaseId, body); + } +}; + +const updateGremlinDatabaseOffer = async (params: UpdateOfferParams): Promise => { + const subscriptionId = userContext.subscriptionId; + const resourceGroup = userContext.resourceGroup; + const accountName = userContext.databaseAccount.name; + + if (params.migrateToAutoPilot) { + await migrateGremlinDatabaseToAutoscale(subscriptionId, resourceGroup, accountName, params.databaseId); + } else if (params.migrateToManual) { + await migrateGremlinDatabaseToManualThroughput(subscriptionId, resourceGroup, accountName, params.databaseId); + } else { + const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params); + await updateGremlinDatabaseThroughput(subscriptionId, resourceGroup, accountName, params.databaseId, body); + } +}; + +const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpdateParameters => { + const body: ThroughputSettingsUpdateParameters = { + properties: { + resource: {} + } + }; + + if (params.autopilotThroughput) { + body.properties.resource.autoscaleSettings = { + maxThroughput: params.autopilotThroughput + }; + } else { + body.properties.resource.throughput = params.manualThroughput; + } + + return body; +}; + +const updateOfferWithSDK = async (params: UpdateOfferParams): Promise => { + const currentOffer = params.currentOffer; + const newOffer: Offer = { + 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 + }; + + if (params.autopilotThroughput) { + newOffer.content.offerAutopilotSettings = { + maxThroughput: params.autopilotThroughput + }; + } else { + newOffer.content.offerThroughput = params.manualThroughput; + } + + const options: RequestOptions = {}; + if (params.migrateToAutoPilot) { + options.initialHeaders[HttpHeaders.migrateOfferToAutopilot] = "true"; + delete newOffer.content.offerAutopilotSettings; + } else if (params.migrateToManual) { + options.initialHeaders[HttpHeaders.migrateOfferToManualThroughput] = "true"; + newOffer.content.offerAutopilotSettings = { maxThroughput: 0 }; + } + + const sdkResponse = await client() + .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; +}; diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index a2a66f332..4396e6721 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -303,6 +303,16 @@ export interface ReadCollectionOfferParams { offerId?: string; } +export interface UpdateOfferParams { + currentOffer: Offer; + databaseId: string; + autopilotThroughput: number; + manualThroughput: number; + collectionId?: string; + migrateToAutoPilot?: boolean; + migrateToManual?: boolean; +} + export interface Notification { id: string; kind: string; diff --git a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx index 68910e929..0aae07d26 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx @@ -20,8 +20,8 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({ geospatialConfig: undefined } as DataModels.Collection) })); -import { updateOffer } from "../../../Common/DocumentClientUtilityBase"; -jest.mock("../../../Common/DocumentClientUtilityBase", () => ({ +import { updateOffer } from "../../../Common/dataAccess/updateOffer"; +jest.mock("../../../Common/dataAccess/updateOffer", () => ({ updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer) })); diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index c83f3f15f..de78632ee 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -10,7 +10,7 @@ import { traceStart, traceFailure, traceSuccess } from "../../../Shared/Telemetr import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import { RequestOptions } from "@azure/cosmos/dist-esm"; import Explorer from "../../Explorer"; -import { updateOffer } from "../../../Common/DocumentClientUtilityBase"; +import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { updateCollection } from "../../../Common/dataAccess/updateCollection"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { userContext } from "../../../UserContext"; @@ -426,7 +426,21 @@ export class SettingsComponent extends React.Component => { - let promises: Q.Promise[] = []; + public onSaveClick = async (): Promise => { this.isExecutionError(false); this.isExecuting(true); @@ -470,163 +469,81 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. const headerOptions: RequestOptions = { initialHeaders: {} }; - if (this.isAutoPilotSelected()) { - const offer = this.database.offer(); - let offerAutopilotSettings: any = {}; - if (!this.hasAutoPilotV2FeatureFlag()) { - offerAutopilotSettings.maxThroughput = this.autoPilotThroughput(); + 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 updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams); + this.database.offer(updatedOffer); + this.database.offer.valueHasMutated(); + this._wasAutopilotOriginallySet(this.isAutoPilotSelected()); } else { - offerAutopilotSettings.tier = this.selectedAutoPilotTier(); - } - const newOffer: DataModels.Offer = { - content: { - offerThroughput: undefined, - offerIsRUPerMinuteThroughputEnabled: false, - offerAutopilotSettings - }, - _etag: undefined, - _ts: undefined, - _rid: offer._rid, - _self: offer._self, - id: offer.id, - offerResourceId: offer.offerResourceId, - offerVersion: offer.offerVersion, - offerType: offer.offerType, - resource: offer.resource - }; + if (this.throughput.editableIsDirty() || this.isAutoPilotSelected.editableIsDirty()) { + const originalThroughputValue = this.throughput.getEditableOriginalValue(); + const newThroughput = this.throughput(); - // user has changed from provisioned --> autoscale - if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) { - headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true"; - delete newOffer.content.offerAutopilotSettings; - } - - const updateOfferPromise = updateOffer(this.database.offer(), newOffer, headerOptions).then( - (updatedOffer: DataModels.Offer) => { - this.database.offer(updatedOffer); - this.database.offer.valueHasMutated(); - this._wasAutopilotOriginallySet(this.isAutoPilotSelected()); - } - ); - promises.push(updateOfferPromise); - } else { - if (this.throughput.editableIsDirty() || this.isAutoPilotSelected.editableIsDirty()) { - const offer = this.database.offer(); - 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 - }; - const updateOfferBeyondLimitPromise = updateOfferThroughputBeyondLimit(requestPayload).then( - () => { - 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 - }, - (error: any) => { - TelemetryProcessor.traceFailure( - Action.UpdateSettings, - { - databaseAccountName: this.container.databaseAccount().name, - databaseName: this.database && this.database.id(), - defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: error - }, - startKey - ); - } - ); - promises.push(Q(updateOfferBeyondLimitPromise)); - } else { - const newOffer: DataModels.Offer = { - content: { - offerThroughput: newThroughput, + 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 - }, - _etag: undefined, - _ts: undefined, - _rid: offer._rid, - _self: offer._self, - id: offer.id, - offerResourceId: offer.offerResourceId, - offerVersion: offer.offerVersion, - offerType: offer.offerType, - resource: offer.resource - }; + }; + 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() + }; - // user has changed from autoscale --> provisioned - if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) { - headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true"; - newOffer.content.offerAutopilotSettings = { maxThroughput: 0 }; + const updatedOffer = await updateOffer(updateOfferParams); + this._wasAutopilotOriginallySet(this.isAutoPilotSelected()); + this.database.offer(updatedOffer); + this.database.offer.valueHasMutated(); } - - const updateOfferPromise = updateOffer(this.database.offer(), newOffer, headerOptions).then( - (updatedOffer: DataModels.Offer) => { - this._wasAutopilotOriginallySet(this.isAutoPilotSelected()); - this.database.offer(updatedOffer); - this.database.offer.valueHasMutated(); - } - ); - - promises.push(updateOfferPromise); } } - } - - if (promises.length === 0) { + } catch (error) { + this.container.isRefreshingExplorer(false); + this.isExecutionError(true); + console.error(error); + this.displayedError(ErrorParserUtility.parse(error)[0].message); + TelemetryProcessor.traceFailure( + Action.UpdateSettings, + { + databaseAccountName: this.container.databaseAccount().name, + databaseName: this.database && this.database.id(), + defaultExperience: this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + error: error + }, + startKey + ); + } finally { this.isExecuting(false); } - - return Q.all(promises) - .then( - () => { - this.container.isRefreshingExplorer(false); - this._setBaseline(); - TelemetryProcessor.traceSuccess( - Action.UpdateSettings, - { - databaseAccountName: this.container.databaseAccount().name, - defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() - }, - startKey - ); - }, - (reason: any) => { - this.container.isRefreshingExplorer(false); - this.isExecutionError(true); - console.error(reason); - this.displayedError(ErrorParserUtility.parse(reason)[0].message); - TelemetryProcessor.traceFailure( - Action.UpdateSettings, - { - databaseAccountName: this.container.databaseAccount().name, - defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() - }, - startKey - ); - } - ) - .finally(() => this.isExecuting(false)); }; public onRevertClick = (): Q.Promise => { diff --git a/src/Explorer/Tabs/SettingsTab.ts b/src/Explorer/Tabs/SettingsTab.ts index 53e2cc25a..4722b6875 100644 --- a/src/Explorer/Tabs/SettingsTab.ts +++ b/src/Explorer/Tabs/SettingsTab.ts @@ -17,7 +17,7 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { PlatformType } from "../../PlatformType"; import { RequestOptions } from "@azure/cosmos/dist-esm"; import Explorer from "../Explorer"; -import { updateOffer } from "../../Common/DocumentClientUtilityBase"; +import { updateOffer } from "../../Common/dataAccess/updateOffer"; import { updateCollection } from "../../Common/dataAccess/updateCollection"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { userContext } from "../../UserContext"; @@ -1175,7 +1175,21 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor ); this.throughput.valueHasMutated(); // force component re-render } else { - const updatedOffer: DataModels.Offer = await updateOffer(this.collection.offer(), newOffer, headerOptions); + const updateOfferParams: DataModels.UpdateOfferParams = { + databaseId: this.collection.databaseId, + collectionId: this.collection.id(), + currentOffer: this.collection.offer(), + autopilotThroughput: this.isAutoPilotSelected() ? this.autoPilotThroughput() : undefined, + manualThroughput: this.isAutoPilotSelected() ? undefined : newThroughput + }; + if (this._hasProvisioningTypeChanged()) { + if (this.isAutoPilotSelected()) { + updateOfferParams.migrateToAutoPilot = true; + } else { + updateOfferParams.migrateToManual = true; + } + } + const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams); this.collection.offer(updatedOffer); this.collection.offer.valueHasMutated(); } diff --git a/src/Explorer/Tree/Database.ts b/src/Explorer/Tree/Database.ts index a8cbce7a5..7ca8a23ef 100644 --- a/src/Explorer/Tree/Database.ts +++ b/src/Explorer/Tree/Database.ts @@ -14,6 +14,7 @@ import * as Logger from "../../Common/Logger"; import Explorer from "../Explorer"; import { readCollections } from "../../Common/dataAccess/readCollections"; import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer"; +import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; export default class Database implements ViewModels.Database { public nodeKind: string; @@ -200,7 +201,11 @@ export default class Database implements ViewModels.Database { } public async loadOffer(): Promise { - if (!this.container.isServerlessEnabled() && !this.offer()) { + if ( + !this.container.isServerlessEnabled() && + this.container.defaultExperience() !== DefaultAccountExperienceType.Table && + !this.offer() + ) { const params: DataModels.ReadDatabaseOfferParams = { databaseId: this.id(), databaseResourceId: this.self