import * as AutoPilotUtils from "../../Utils/AutoPilotUtils"; import * as Constants from "../../Common/Constants"; import * as DataModels from "../../Contracts/DataModels"; import * as ErrorParserUtility from "../../Common/ErrorParserUtility"; import * as ko from "knockout"; import * as PricingUtils from "../../Utils/PricingUtils"; import * as SharedConstants from "../../Shared/Constants"; import * as ViewModels from "../../Contracts/ViewModels"; import DiscardIcon from "../../../images/discard.svg"; import editable from "../../Common/EditableUtility"; import Q from "q"; import SaveIcon from "../../../images/save-cosmos.svg"; import TabsBase from "./TabsBase"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; 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 { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { userContext } from "../../UserContext"; import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit"; const updateThroughputBeyondLimitWarningMessage: string = ` You are about to request an increase in throughput beyond the pre-allocated capacity. The service will scale out and increase throughput for the selected database. This operation will take 1-3 business days to complete. You can track the status of this request in Notifications.`; const updateThroughputDelayedApplyWarningMessage: string = ` You are about to request an increase in throughput beyond the pre-allocated capacity. This operation will take some time to complete.`; const currentThroughput: (isAutoscale: boolean, throughput: number) => string = (isAutoscale, throughput) => isAutoscale ? `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.
Database: ${databaseName}, ${currentThroughput(isAutoscale, throughput)}`; const throughputApplyLongDelayMessage = (isAutoscale: boolean, throughput: number, databaseName: string) => `A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
Database: ${databaseName}, ${currentThroughput(isAutoscale, throughput)}`; export default class DatabaseSettingsTab extends TabsBase implements ViewModels.WaitsForTemplate { // editables public isAutoPilotSelected: ViewModels.Editable; public throughput: ViewModels.Editable; public selectedAutoPilotTier: ViewModels.Editable; public autoPilotThroughput: ViewModels.Editable; public throughputIncreaseFactor: number = Constants.ClientDefaults.databaseThroughputIncreaseFactor; public saveSettingsButton: ViewModels.Button; public discardSettingsChangesButton: ViewModels.Button; public canRequestSupport: ko.PureComputed; public canThroughputExceedMaximumValue: ko.Computed; public costsVisible: ko.Computed; public displayedError: ko.Observable; public isTemplateReady: ko.Observable; public minRUAnotationVisible: ko.Computed; public minRUs: ko.Computed; public maxRUs: ko.Computed; public maxRUsText: ko.PureComputed; public maxRUThroughputInputLimit: ko.Computed; public notificationStatusInfo: ko.Observable; public pendingNotification: ko.Observable; public requestUnitsUsageCost: ko.PureComputed; public autoscaleCost: ko.PureComputed; public shouldShowNotificationStatusPrompt: ko.Computed; public shouldDisplayPortalUsePrompt: ko.Computed; public shouldShowStatusBar: ko.Computed; public throughputTitle: ko.PureComputed; public throughputAriaLabel: ko.PureComputed; public userCanChangeProvisioningTypes: ko.Observable; public autoPilotTiersList: ko.ObservableArray>; public autoPilotUsageCost: ko.PureComputed; public warningMessage: ko.Computed; public canExceedMaximumValue: ko.PureComputed; public hasAutoPilotV2FeatureFlag: ko.PureComputed; public overrideWithAutoPilotSettings: ko.Computed; public overrideWithProvisionedThroughputSettings: ko.Computed; public testId: string; public throughputAutoPilotRadioId: string; public throughputProvisionedRadioId: string; public throughputModeRadioName: string; private _hasProvisioningTypeChanged: ko.Computed; private _wasAutopilotOriginallySet: ko.Observable; private _offerReplacePending: ko.Computed; private container: Explorer; constructor(options: ViewModels.TabOptions) { super(options); this.container = options.node && (options.node as ViewModels.Database).container; this.hasAutoPilotV2FeatureFlag = ko.pureComputed(() => this.container.hasAutoPilotV2FeatureFlag()); this.selectedAutoPilotTier = editable.observable(); this.autoPilotTiersList = ko.observableArray>(); this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue()); // html element ids this.testId = `scaleSettingThroughputValue${this.tabId}`; this.throughputAutoPilotRadioId = `editContainerThroughput-autoPilotRadio${this.tabId}`; this.throughputProvisionedRadioId = `editContainerThroughput-manualRadio${this.tabId}`; this.throughputModeRadioName = `throughputModeRadio${this.tabId}`; this.throughput = editable.observable(); 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(!!offerAutopilotSettings || !this.hasAutoPilotV2FeatureFlag()); if (!this.hasAutoPilotV2FeatureFlag()) { if (offerAutopilotSettings && offerAutopilotSettings.maxThroughput) { if (AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)) { this._wasAutopilotOriginallySet(true); this.isAutoPilotSelected(true); this.autoPilotThroughput(offerAutopilotSettings.maxThroughput); } } } else { if (offerAutopilotSettings && offerAutopilotSettings.tier) { if (AutoPilotUtils.isValidAutoPilotTier(offerAutopilotSettings.tier)) { this._wasAutopilotOriginallySet(true); this.isAutoPilotSelected(true); this.selectedAutoPilotTier(offerAutopilotSettings.tier); this.autoPilotTiersList(AutoPilotUtils.getAvailableAutoPilotTiersOptions(offerAutopilotSettings.tier)); } } } this._hasProvisioningTypeChanged = ko.pureComputed(() => { if (!this.userCanChangeProvisioningTypes()) { return false; } if (this._wasAutopilotOriginallySet() !== this.isAutoPilotSelected()) { return true; } return false; }); this.autoPilotUsageCost = ko.pureComputed(() => { const autoPilot = !this.hasAutoPilotV2FeatureFlag() ? this.autoPilotThroughput() : this.selectedAutoPilotTier(); if (!autoPilot) { return ""; } return !this.hasAutoPilotV2FeatureFlag() ? PricingUtils.getAutoPilotV3SpendHtml(autoPilot, true /* isDatabaseThroughput */) : PricingUtils.getAutoPilotV2SpendHtml(autoPilot, true /* isDatabaseThroughput */); }); this.requestUnitsUsageCost = ko.pureComputed(() => { const account = this.container.databaseAccount(); if (!account) { return ""; } const serverId = this.container.serverId(); const regions = (account && account.properties && account.properties.readLocations && account.properties.readLocations.length) || 1; const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false; let estimatedSpend: string; if (!this.isAutoPilotSelected()) { estimatedSpend = PricingUtils.getEstimatedSpendHtml( // if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set... this.overrideWithAutoPilotSettings() ? this.autoPilotThroughput() : this.throughput(), serverId, regions, multimaster, false /*rupmEnabled*/ ); } else { estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( this.autoPilotThroughput(), serverId, regions, multimaster ); } return estimatedSpend; }); this.costsVisible = ko.computed(() => { return !this.container.isEmulator; }); this.shouldDisplayPortalUsePrompt = ko.pureComputed( () => this.container.getPlatformType() === PlatformType.Hosted ); this.canThroughputExceedMaximumValue = ko.pureComputed( () => this.container.getPlatformType() === PlatformType.Portal && !this.container.isRunningOnNationalCloud() ); this.canRequestSupport = ko.pureComputed(() => { if ( !!this.container.isEmulator || this.container.getPlatformType() === PlatformType.Hosted || this.canThroughputExceedMaximumValue() ) { return false; } return true; }); this.overrideWithAutoPilotSettings = ko.pureComputed(() => { if (this.hasAutoPilotV2FeatureFlag()) { return false; } return this._hasProvisioningTypeChanged() && this._wasAutopilotOriginallySet(); }); this.overrideWithProvisionedThroughputSettings = ko.pureComputed(() => { if (this.hasAutoPilotV2FeatureFlag()) { return false; } 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.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.maxRUThroughputInputLimit = ko.pureComputed(() => { if (this.container && this.container.getPlatformType() === PlatformType.Hosted) { return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million; } return this.maxRUs(); }); this.maxRUsText = ko.pureComputed(() => { return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million.toLocaleString(); }); this.throughputTitle = ko.pureComputed(() => { if (this.isAutoPilotSelected()) { return AutoPilotUtils.getAutoPilotHeaderText(this.hasAutoPilotV2FeatureFlag()); } return `Throughput (${this.minRUs().toLocaleString()} - unlimited RU/s)`; }); this.throughputAriaLabel = ko.pureComputed(() => { 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.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.hasAutoPilotV2FeatureFlag() && this.overrideWithProvisionedThroughputSettings()) { return AutoPilotUtils.manualToAutoscaleDisclaimer; } if ( offer && offer.hasOwnProperty("headers") && !!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending] ) { const throughput = offer.content.offerAutopilotSettings ? !this.hasAutoPilotV2FeatureFlag() ? offer.content.offerAutopilotSettings.maxThroughput : offer.content.offerAutopilotSettings.maximumTierThroughput : offer.content.offerThroughput; return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id()); } if ( this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million && this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million && this.canThroughputExceedMaximumValue() ) { return updateThroughputBeyondLimitWarningMessage; } if (this.throughput() > this.maxRUs()) { return updateThroughputDelayedApplyWarningMessage; } if (this.pendingNotification()) { const matches: string[] = this.pendingNotification().description.match("Throughput update for (.*) RU/s"); const throughput: number = matches.length > 1 && Number(matches[1]); if (throughput) { return throughputApplyLongDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id()); } } return ""; }); this.warningMessage.subscribe((warning: string) => { if (warning.length > 0) { this.notificationStatusInfo(""); } }); this.shouldShowStatusBar = ko.computed( () => this.shouldShowNotificationStatusPrompt() || (this.warningMessage && this.warningMessage().length > 0) ); this.displayedError = ko.observable(""); this._setBaseline(); this.saveSettingsButton = { enabled: ko.computed(() => { if (this._hasProvisioningTypeChanged()) { return true; } if (this._offerReplacePending && this._offerReplacePending()) { return false; } const isAutoPilot = this.isAutoPilotSelected(); const isManual = !this.isAutoPilotSelected(); if (isAutoPilot) { if ( (!this.hasAutoPilotV2FeatureFlag() && !AutoPilotUtils.isValidAutoPilotThroughput(this.autoPilotThroughput())) || (this.hasAutoPilotV2FeatureFlag() && !AutoPilotUtils.isValidAutoPilotTier(this.selectedAutoPilotTier())) ) { return false; } if (this.isAutoPilotSelected.editableIsDirty()) { return true; } if ( (!this.hasAutoPilotV2FeatureFlag() && this.autoPilotThroughput.editableIsDirty()) || (this.hasAutoPilotV2FeatureFlag() && this.selectedAutoPilotTier.editableIsDirty()) ) { return true; } } if (isManual) { if (!this.throughput()) { return false; } if (this.throughput() < this.minRUs()) { return false; } if ( !this.canThroughputExceedMaximumValue() && this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million ) { return false; } if (this.throughput.editableIsDirty()) { return true; } if ( (!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected.editableIsDirty()) || (this.hasAutoPilotV2FeatureFlag() && this.selectedAutoPilotTier.editableIsDirty()) ) { return true; } } return false; }), visible: ko.computed(() => { return true; }) }; this.discardSettingsChangesButton = { enabled: ko.computed(() => { if (this.throughput.editableIsDirty()) { return true; } if (this.isAutoPilotSelected.editableIsDirty()) { return true; } if (this.autoPilotThroughput.editableIsDirty()) { return true; } return false; }), visible: ko.computed(() => { return true; }) }; this.isTemplateReady = ko.observable(false); this._buildCommandBarOptions(); } public onSaveClick = (): Q.Promise => { let promises: Q.Promise[] = []; this.isExecutionError(false); this.isExecuting(true); const startKey: number = TelemetryProcessor.traceStart(Action.UpdateSettings, { databaseAccountName: this.container.databaseAccount().name, defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle() }); const headerOptions: RequestOptions = { initialHeaders: {} }; if (this.isAutoPilotSelected()) { const offer = this.database.offer(); let offerAutopilotSettings: any = {}; if (!this.hasAutoPilotV2FeatureFlag()) { offerAutopilotSettings.maxThroughput = this.autoPilotThroughput(); } 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 }; // 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, 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 }; // user has changed from autoscale --> provisioned if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) { headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true"; newOffer.content.offerAutopilotSettings = { maxThroughput: 0 }; } 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) { 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 => { this.throughput.setBaseline(this.throughput.getEditableOriginalValue()); this.isAutoPilotSelected.setBaseline(this.isAutoPilotSelected.getEditableOriginalValue()); if (!this.hasAutoPilotV2FeatureFlag()) { this.autoPilotThroughput.setBaseline(this.autoPilotThroughput.getEditableOriginalValue()); } else { this.selectedAutoPilotTier.setBaseline(this.selectedAutoPilotTier.getEditableOriginalValue()); } return Q(); }; public onActivate(): Q.Promise { return super.onActivate().then(async () => { this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings); await this.database.loadOffer(); }); } 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.userCanChangeProvisioningTypes(!!offerAutopilotSettings || !this.hasAutoPilotV2FeatureFlag()); if (this.hasAutoPilotV2FeatureFlag()) { const selectedAutoPilotTier = offerAutopilotSettings && offerAutopilotSettings.tier; this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotTier(selectedAutoPilotTier)); this.selectedAutoPilotTier.setBaseline(selectedAutoPilotTier); } else { const maxThroughputForAutoPilot = offerAutopilotSettings && offerAutopilotSettings.maxThroughput; this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(maxThroughputForAutoPilot)); this.autoPilotThroughput.setBaseline(maxThroughputForAutoPilot || AutoPilotUtils.minAutoPilotThroughput); } } protected getTabsButtons(): CommandButtonComponentProps[] { const buttons: CommandButtonComponentProps[] = []; const label = "Save"; if (this.saveSettingsButton.visible()) { buttons.push({ iconSrc: SaveIcon, iconAlt: label, onCommandClick: this.onSaveClick, commandButtonLabel: label, ariaLabel: label, hasPopup: false, disabled: !this.saveSettingsButton.enabled() }); } if (this.discardSettingsChangesButton.visible()) { const label = "Discard"; buttons.push({ iconSrc: DiscardIcon, iconAlt: label, onCommandClick: this.onRevertClick, commandButtonLabel: label, ariaLabel: label, hasPopup: false, disabled: !this.discardSettingsChangesButton.enabled() }); } return buttons; } private _buildCommandBarOptions(): void { ko.computed(() => ko.toJSON([ this.saveSettingsButton.visible, this.saveSettingsButton.enabled, this.discardSettingsChangesButton.visible, this.discardSettingsChangesButton.enabled ]) ).subscribe(() => this.updateNavbarWithTabsButtons()); this.updateNavbarWithTabsButtons(); } }