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 editable from "../../Common/EditableUtility"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { ContextualPaneBase } from "./ContextualPaneBase"; import { createDatabase } from "../../Common/dataAccess/createDatabase"; import { PlatformType } from "../../PlatformType"; export default class AddDatabasePane extends ContextualPaneBase { public defaultExperience: ko.Computed; public databaseIdLabel: ko.Computed; public databaseIdPlaceHolder: ko.Computed; public databaseId: ko.Observable; public databaseIdTooltipText: ko.Computed; public databaseLevelThroughputTooltipText: ko.Computed; public databaseCreateNewShared: ko.Observable; public formErrorsDetails: ko.Observable; public throughput: ViewModels.Editable; public maxThroughputRU: ko.Observable; public minThroughputRU: ko.Observable; public maxThroughputRUText: ko.PureComputed; public throughputRangeText: ko.Computed; public throughputSpendAckText: ko.Observable; public throughputSpendAck: ko.Observable; public throughputSpendAckVisible: ko.Computed; public requestUnitsUsageCost: ko.Computed; public canRequestSupport: ko.PureComputed; public costsVisible: ko.PureComputed; public upsellMessage: ko.PureComputed; public upsellMessageAriaLabel: ko.PureComputed; public upsellAnchorUrl: ko.PureComputed; public upsellAnchorText: ko.PureComputed; public isAutoPilotSelected: ko.Observable; public selectedAutoPilotTier: ko.Observable; public autoPilotTiersList: ko.ObservableArray>; public maxAutoPilotThroughputSet: ko.Observable; public autoPilotUsageCost: ko.Computed; public canExceedMaximumValue: ko.PureComputed; public hasAutoPilotV2FeatureFlag: ko.PureComputed; public ruToolTipText: ko.Computed; public isFreeTierAccount: ko.Computed; public canConfigureThroughput: ko.PureComputed; public showUpsellMessage: ko.PureComputed; constructor(options: ViewModels.PaneOptions) { super(options); this.title((this.container && this.container.addDatabaseText()) || "New Database"); this.databaseId = ko.observable(); this.hasAutoPilotV2FeatureFlag = ko.pureComputed(() => this.container.hasAutoPilotV2FeatureFlag()); this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText(this.hasAutoPilotV2FeatureFlag())); this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled()); this.showUpsellMessage = ko.pureComputed(() => !this.container.isServerlessEnabled()); this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue()); // TODO 388844: get defaults from parent frame this.databaseCreateNewShared = ko.observable(this.getSharedThroughputDefault()); this.container.subscriptionType && this.container.subscriptionType.subscribe(subscriptionType => { this.databaseCreateNewShared(this.getSharedThroughputDefault()); }); this.databaseIdLabel = ko.computed(() => this.container.isPreferredApiCassandra() ? "Keyspace id" : "Database id" ); this.databaseIdPlaceHolder = ko.computed(() => this.container.isPreferredApiCassandra() ? "Type a new keyspace id" : "Type a new database id" ); this.databaseIdTooltipText = ko.computed(() => { const isCassandraAccount: boolean = this.container.isPreferredApiCassandra(); return `A ${isCassandraAccount ? "keyspace" : "database"} is a logical container of one or more ${ isCassandraAccount ? "tables" : "collections" }`; }); this.databaseLevelThroughputTooltipText = ko.computed(() => { const isCassandraAccount: boolean = this.container.isPreferredApiCassandra(); const databaseLabel: string = isCassandraAccount ? "keyspace" : "database"; const collectionsLabel: string = isCassandraAccount ? "tables" : "collections"; return `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`; }); this.throughput = editable.observable(); this.maxThroughputRU = ko.observable(); this.minThroughputRU = ko.observable(); this.throughputSpendAckText = ko.observable(); this.throughputSpendAck = ko.observable(false); this.selectedAutoPilotTier = ko.observable(); this.autoPilotTiersList = ko.observableArray>( AutoPilotUtils.getAvailableAutoPilotTiersOptions() ); this.isAutoPilotSelected = ko.observable(false); this.maxAutoPilotThroughputSet = ko.observable(AutoPilotUtils.minAutoPilotThroughput); this.autoPilotUsageCost = ko.pureComputed(() => { const autoPilot = this._isAutoPilotSelectedAndWhatTier(); if (!autoPilot) { return ""; } return !this.hasAutoPilotV2FeatureFlag() ? PricingUtils.getAutoPilotV3SpendHtml(autoPilot.maxThroughput, true /* isDatabaseThroughput */) : PricingUtils.getAutoPilotV2SpendHtml(autoPilot.autopilotTier, true /* isDatabaseThroughput */); }); this.throughputRangeText = ko.pureComputed(() => { if (this.isAutoPilotSelected()) { return AutoPilotUtils.getAutoPilotHeaderText(this.hasAutoPilotV2FeatureFlag()); } return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`; }); this.requestUnitsUsageCost = ko.computed(() => { const offerThroughput: number = this.throughput(); if ( offerThroughput < this.minThroughputRU() || (offerThroughput > this.maxThroughputRU() && !this.canExceedMaximumValue()) ) { return ""; } 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 estimatedSpendAcknowledge: string; let estimatedSpend: string; if (!this.isAutoPilotSelected()) { estimatedSpend = PricingUtils.getEstimatedSpendHtml( offerThroughput, serverId, regions, multimaster, false /*rupmEnabled*/ ); estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( offerThroughput, serverId, regions, multimaster, false /*rupmEnabled*/, this.isAutoPilotSelected() ); } else { estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( this.maxAutoPilotThroughputSet(), serverId, regions, multimaster ); estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( this.maxAutoPilotThroughputSet(), serverId, regions, multimaster, false /*rupmEnabled*/, this.isAutoPilotSelected() ); } // TODO: change throughputSpendAckText to be a computed value, instead of having this side effect this.throughputSpendAckText(estimatedSpendAcknowledge); return estimatedSpend; }); this.canRequestSupport = ko.pureComputed(() => { if ( !this.container.isEmulator && !this.container.isTryCosmosDBSubscription() && this.container.getPlatformType() !== PlatformType.Portal ) { const offerThroughput: number = this.throughput(); return offerThroughput <= 100000; } return false; }); this.isFreeTierAccount = ko.computed(() => { const databaseAccount = this.container && this.container.databaseAccount && this.container.databaseAccount(); const isFreeTierAccount = databaseAccount && databaseAccount.properties && databaseAccount.properties.enableFreeTier; return isFreeTierAccount; }); this.maxThroughputRUText = ko.pureComputed(() => { return this.maxThroughputRU().toLocaleString(); }); this.costsVisible = ko.pureComputed(() => { return !this.container.isEmulator; }); this.throughputSpendAckVisible = ko.pureComputed(() => { const autoscaleThroughput = this.maxAutoPilotThroughputSet() * 1; if (!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected()) { return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K; } const selectedThroughput: number = this.throughput(); const maxRU: number = this.maxThroughputRU && this.maxThroughputRU(); const isMaxRUGreaterThanDefault: boolean = maxRU > SharedConstants.CollectionCreation.DefaultCollectionRUs100K; const isThroughputSetGreaterThanDefault: boolean = selectedThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K; if (this.canExceedMaximumValue()) { return isThroughputSetGreaterThanDefault; } return isThroughputSetGreaterThanDefault && isMaxRUGreaterThanDefault; }); this.databaseCreateNewShared.subscribe((useShared: boolean) => { this._updateThroughputLimitByDatabase(); }); this.resetData(); this.container.flight.subscribe(() => { this.resetData(); }); this.upsellMessage = ko.pureComputed(() => { return PricingUtils.getUpsellMessage(this.container.serverId(), this.isFreeTierAccount()); }); this.upsellMessageAriaLabel = ko.pureComputed(() => { return `${this.upsellMessage()}. Click ${this.isFreeTierAccount() ? "to learn more" : "for more details"}`; }); this.upsellAnchorUrl = ko.pureComputed(() => { return this.isFreeTierAccount() ? Constants.Urls.freeTierInformation : Constants.Urls.cosmosPricing; }); this.upsellAnchorText = ko.pureComputed(() => { return this.isFreeTierAccount() ? "Learn more" : "More details"; }); } public onMoreDetailsKeyPress = (source: any, event: KeyboardEvent): boolean => { if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { this.showErrorDetails(); return false; } return true; }; public open() { super.open(); this.resetData(); const addDatabasePaneOpenMessage = { databaseAccountName: this.container.databaseAccount().name, defaultExperience: this.container.defaultExperience(), subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()], subscriptionQuotaId: this.container.quotaId(), defaultsCheck: { throughput: this.throughput(), flight: this.container.flight() }, dataExplorerArea: Constants.Areas.ContextualPane }; const focusElement = document.getElementById("database-id"); focusElement && focusElement.focus(); TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage); } public submit() { if (!this._isValid()) { return; } const offerThroughput: number = this._computeOfferThroughput(); const addDatabasePaneStartMessage = { databaseAccountName: this.container.databaseAccount().name, defaultExperience: this.container.defaultExperience(), database: ko.toJS({ id: this.databaseId(), shared: this.databaseCreateNewShared() }), offerThroughput, subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()], subscriptionQuotaId: this.container.quotaId(), defaultsCheck: { flight: this.container.flight() }, dataExplorerArea: Constants.Areas.ContextualPane }; const startKey: number = TelemetryProcessor.traceStart(Action.CreateDatabase, addDatabasePaneStartMessage); this.formErrors(""); this.isExecuting(true); const createDatabaseParams: DataModels.CreateDatabaseParams = { autoPilotMaxThroughput: this.maxAutoPilotThroughputSet(), databaseId: addDatabasePaneStartMessage.database.id, databaseLevelThroughput: addDatabasePaneStartMessage.database.shared, offerThroughput: addDatabasePaneStartMessage.offerThroughput }; createDatabase(createDatabaseParams).then( (database: DataModels.Database) => { this._onCreateDatabaseSuccess(offerThroughput, startKey); }, (reason: any) => { this._onCreateDatabaseFailure(reason, offerThroughput, reason); } ); } public resetData() { this.databaseId(""); this.databaseCreateNewShared(this.getSharedThroughputDefault()); this.selectedAutoPilotTier(undefined); this.isAutoPilotSelected(false); this.maxAutoPilotThroughputSet(AutoPilotUtils.minAutoPilotThroughput); this._updateThroughputLimitByDatabase(); this.throughputSpendAck(false); super.resetData(); } public getSharedThroughputDefault(): boolean { const subscriptionType: ViewModels.SubscriptionType = this.container.subscriptionType && this.container.subscriptionType(); if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) { return false; } return true; } private _onCreateDatabaseSuccess(offerThroughput: number, startKey: number): void { this.isExecuting(false); this.close(); this.container.refreshAllDatabases(); const addDatabasePaneSuccessMessage = { databaseAccountName: this.container.databaseAccount().name, defaultExperience: this.container.defaultExperience(), database: ko.toJS({ id: this.databaseId(), shared: this.databaseCreateNewShared() }), offerThroughput: offerThroughput, subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()], subscriptionQuotaId: this.container.quotaId(), defaultsCheck: { flight: this.container.flight() }, dataExplorerArea: Constants.Areas.ContextualPane }; TelemetryProcessor.traceSuccess(Action.CreateDatabase, addDatabasePaneSuccessMessage, startKey); this.resetData(); } private _onCreateDatabaseFailure(reason: any, offerThroughput: number, startKey: number): void { this.isExecuting(false); const message = ErrorParserUtility.parse(reason); this.formErrors(message[0].message); this.formErrorsDetails(message[0].message); const addDatabasePaneFailedMessage = { databaseAccountName: this.container.databaseAccount().name, defaultExperience: this.container.defaultExperience(), database: ko.toJS({ id: this.databaseId(), shared: this.databaseCreateNewShared() }), offerThroughput: offerThroughput, subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()], subscriptionQuotaId: this.container.quotaId(), defaultsCheck: { flight: this.container.flight() }, dataExplorerArea: Constants.Areas.ContextualPane, error: reason }; TelemetryProcessor.traceFailure(Action.CreateDatabase, addDatabasePaneFailedMessage, startKey); } private _getThroughput(): number { const throughput: number = this.throughput(); return isNaN(throughput) ? 0 : Number(throughput); } private _computeOfferThroughput(): number { if (!this.canConfigureThroughput()) { return undefined; } if (this.isAutoPilotSelected()) { return undefined; } return this._getThroughput(); } private _isValid(): boolean { // TODO add feature flag that disables validation for customers with custom accounts if (this.isAutoPilotSelected()) { const autoPilot = this._isAutoPilotSelectedAndWhatTier(); if ( (!this.hasAutoPilotV2FeatureFlag() && (!autoPilot || !autoPilot.maxThroughput || !AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput))) || (this.hasAutoPilotV2FeatureFlag() && (!autoPilot || !autoPilot.autopilotTier || !AutoPilotUtils.isValidAutoPilotTier(autoPilot.autopilotTier))) ) { this.formErrors( !this.hasAutoPilotV2FeatureFlag() ? `Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput` : "Please select an Autopilot tier from the list." ); return false; } } const throughput = this._getThroughput(); if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) { this.formErrors(`Please acknowledge the estimated daily spend.`); return false; } const autoscaleThroughput = this.maxAutoPilotThroughputSet() * 1; if ( !this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck() ) { this.formErrors(`Please acknowledge the estimated monthly spend.`); return false; } return true; } private _isAutoPilotSelectedAndWhatTier(): DataModels.AutoPilotCreationSettings { if ( (!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.maxAutoPilotThroughputSet()) || (this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.selectedAutoPilotTier()) ) { return !this.hasAutoPilotV2FeatureFlag() ? { maxThroughput: this.maxAutoPilotThroughputSet() * 1 } : { autopilotTier: this.selectedAutoPilotTier() }; } return undefined; } private _updateThroughputLimitByDatabase() { const throughputDefaults = this.container.collectionCreationDefaults.throughput; this.throughput(throughputDefaults.shared); this.maxThroughputRU(throughputDefaults.unlimitedmax); this.minThroughputRU(throughputDefaults.unlimitedmin); } }