import * as ko from "knockout"; import * as _ from "underscore"; import * as Constants from "../../Common/Constants"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { configContext, Platform } from "../../ConfigContext"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import * as AddCollectionUtility from "../../Shared/AddCollectionUtility"; import * as SharedConstants from "../../Shared/Constants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import * as AutoPilotUtils from "../../Utils/AutoPilotUtils"; import * as PricingUtils from "../../Utils/PricingUtils"; import { CassandraAPIDataClient } from "../Tables/TableDataClient"; import { ContextualPaneBase } from "./ContextualPaneBase"; export default class CassandraAddCollectionPane extends ContextualPaneBase { public createTableQuery: ko.Observable; public keyspaceId: ko.Observable; public maxThroughputRU: ko.Observable; public minThroughputRU: ko.Observable; public tableId: ko.Observable; public throughput: ko.Observable; public throughputRangeText: ko.Computed; public sharedThroughputRangeText: ko.Computed; public userTableQuery: ko.Observable; public requestUnitsUsageCostDedicated: ko.Computed; public requestUnitsUsageCostShared: ko.Computed; public costsVisible: ko.PureComputed; public keyspaceHasSharedOffer: ko.Observable; public keyspaceIds: ko.ObservableArray; public keyspaceThroughput: ko.Observable; public keyspaceCreateNew: ko.Observable; public dedicateTableThroughput: ko.Observable; public canRequestSupport: ko.PureComputed; public throughputSpendAckText: ko.Observable; public throughputSpendAck: ko.Observable; public sharedThroughputSpendAck: ko.Observable; public sharedThroughputSpendAckText: ko.Observable; public isAutoPilotSelected: ko.Observable; public isSharedAutoPilotSelected: ko.Observable; public selectedAutoPilotThroughput: ko.Observable; public sharedAutoPilotThroughput: ko.Observable; public autoPilotUsageCost: ko.Computed; public sharedThroughputSpendAckVisible: ko.Computed; public throughputSpendAckVisible: ko.Computed; public canExceedMaximumValue: ko.PureComputed; public isFreeTierAccount: ko.Computed; public ruToolTipText: ko.Computed; public canConfigureThroughput: ko.PureComputed; private keyspaceOffers: Map; constructor(options: ViewModels.PaneOptions) { super(options); this.title("Add Table"); this.createTableQuery = ko.observable("CREATE TABLE "); this.keyspaceCreateNew = ko.observable(true); this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText()); this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled()); this.keyspaceOffers = new Map(); this.keyspaceIds = ko.observableArray(); this.keyspaceHasSharedOffer = ko.observable(false); this.keyspaceThroughput = ko.observable(); this.keyspaceId = ko.observable(""); this.keyspaceId.subscribe((keyspaceId: string) => { if (this.keyspaceIds.indexOf(keyspaceId) >= 0) { this.keyspaceHasSharedOffer(this.keyspaceOffers.has(keyspaceId)); } }); this.keyspaceId.extend({ rateLimit: 100 }); this.dedicateTableThroughput = ko.observable(false); const throughputDefaults = this.container.collectionCreationDefaults.throughput; this.maxThroughputRU = ko.observable(throughputDefaults.unlimitedmax); this.minThroughputRU = ko.observable(throughputDefaults.unlimitedmin); this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue()); this.isFreeTierAccount = ko.computed(() => { return userContext?.databaseAccount?.properties?.enableFreeTier; }); this.tableId = ko.observable(""); this.isAutoPilotSelected = ko.observable(false); this.isSharedAutoPilotSelected = ko.observable(false); this.selectedAutoPilotThroughput = ko.observable(); this.sharedAutoPilotThroughput = ko.observable(); this.throughput = ko.observable(); this.throughputRangeText = ko.pureComputed(() => { const enableAutoPilot = this.isAutoPilotSelected(); if (!enableAutoPilot) { return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`; } return AutoPilotUtils.getAutoPilotHeaderText(); }); this.sharedThroughputRangeText = ko.pureComputed(() => { if (this.isSharedAutoPilotSelected()) { return AutoPilotUtils.getAutoPilotHeaderText(); } return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`; }); this.userTableQuery = ko.observable("(userid int, name text, email text, PRIMARY KEY (userid))"); this.keyspaceId.subscribe((keyspaceId) => { this.createTableQuery(`CREATE TABLE ${keyspaceId}.`); }); this.throughputSpendAckText = ko.observable(); this.throughputSpendAck = ko.observable(false); this.sharedThroughputSpendAck = ko.observable(false); this.sharedThroughputSpendAckText = ko.observable(); this.resetData(); this.requestUnitsUsageCostDedicated = ko.computed(() => { const { databaseAccount: account } = userContext; if (!account) { return ""; } const regions = (account && account.properties && account.properties.readLocations && account.properties.readLocations.length) || 1; const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false; const offerThroughput: number = this.throughput(); let estimatedSpend: string; let estimatedDedicatedSpendAcknowledge: string; if (!this.isAutoPilotSelected()) { estimatedSpend = PricingUtils.getEstimatedSpendHtml( offerThroughput, userContext.portalEnv, regions, multimaster ); estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( offerThroughput, userContext.portalEnv, regions, multimaster, this.isAutoPilotSelected() ); } else { estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( this.selectedAutoPilotThroughput(), userContext.portalEnv, regions, multimaster ); estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( this.selectedAutoPilotThroughput(), userContext.portalEnv, regions, multimaster, this.isAutoPilotSelected() ); } this.throughputSpendAckText(estimatedDedicatedSpendAcknowledge); return estimatedSpend; }); this.requestUnitsUsageCostShared = ko.computed(() => { const { databaseAccount: account } = userContext; if (!account) { return ""; } const regions = account?.properties?.readLocations?.length || 1; const multimaster = account?.properties?.enableMultipleWriteLocations || false; let estimatedSpend: string; let estimatedSharedSpendAcknowledge: string; if (!this.isSharedAutoPilotSelected()) { estimatedSpend = PricingUtils.getEstimatedSpendHtml( this.keyspaceThroughput(), userContext.portalEnv, regions, multimaster ); estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( this.keyspaceThroughput(), userContext.portalEnv, regions, multimaster, this.isSharedAutoPilotSelected() ); } else { estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( this.sharedAutoPilotThroughput(), userContext.portalEnv, regions, multimaster ); estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( this.sharedAutoPilotThroughput(), userContext.portalEnv, regions, multimaster, this.isSharedAutoPilotSelected() ); } this.sharedThroughputSpendAckText(estimatedSharedSpendAcknowledge); return estimatedSpend; }); this.costsVisible = ko.pureComputed(() => { return configContext.platform !== Platform.Emulator; }); this.canRequestSupport = ko.pureComputed(() => { if (configContext.platform !== Platform.Emulator && !userContext.isTryCosmosDBSubscription) { const offerThroughput: number = this.throughput(); return offerThroughput <= 100000; } return false; }); this.sharedThroughputSpendAckVisible = ko.computed(() => { const autoscaleThroughput = this.sharedAutoPilotThroughput() * 1; if (this.isSharedAutoPilotSelected()) { return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K; } return this.keyspaceThroughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs100K; }); this.throughputSpendAckVisible = ko.pureComputed(() => { const autoscaleThroughput = this.selectedAutoPilotThroughput() * 1; if (this.isAutoPilotSelected()) { return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K; } return this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs100K; }); if (!!this.container) { const updateKeyspaceIds: (keyspaces: ViewModels.Database[]) => void = ( newKeyspaceIds: ViewModels.Database[] ): void => { const cachedKeyspaceIdsList = _.map(newKeyspaceIds, (keyspace: ViewModels.Database) => { if (keyspace && keyspace.offer && !!keyspace.offer()) { this.keyspaceOffers.set(keyspace.id(), keyspace.offer()); } return keyspace.id(); }); this.keyspaceIds(cachedKeyspaceIdsList); }; this.container.databases.subscribe((newDatabases: ViewModels.Database[]) => updateKeyspaceIds(newDatabases)); updateKeyspaceIds(this.container.databases()); } this.autoPilotUsageCost = ko.pureComputed(() => { const autoPilot = this._getAutoPilot(); if (!autoPilot) { return ""; } const isDatabaseThroughput: boolean = this.keyspaceCreateNew(); return PricingUtils.getAutoPilotV3SpendHtml(autoPilot.maxThroughput, isDatabaseThroughput); }); } public decreaseThroughput() { let offerThroughput: number = this.throughput(); if (offerThroughput > this.minThroughputRU()) { offerThroughput -= 100; this.throughput(offerThroughput); } } public increaseThroughput() { let offerThroughput: number = this.throughput(); if (offerThroughput < this.maxThroughputRU()) { offerThroughput += 100; this.throughput(offerThroughput); } } public open() { super.open(); if (!this.container.isServerlessEnabled()) { this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); } const addCollectionPaneOpenMessage = { collection: ko.toJS({ id: this.tableId(), storage: Constants.BackendDefaults.multiPartitionStorageInGb, offerThroughput: this.throughput(), partitionKey: "", databaseId: this.keyspaceId(), }), subscriptionType: userContext.subscriptionType, subscriptionQuotaId: userContext.quotaId, defaultsCheck: { storage: "u", throughput: this.throughput(), flight: userContext.addCollectionFlight, }, dataExplorerArea: Constants.Areas.ContextualPane, }; const focusElement = document.getElementById("keyspace-id"); focusElement && focusElement.focus(); TelemetryProcessor.trace(Action.CreateCollection, ActionModifiers.Open, addCollectionPaneOpenMessage); } public submit() { if (!this._isValid()) { return; } this.isExecuting(true); const autoPilotCommand = `cosmosdb_autoscale_max_throughput`; let createTableAndKeyspacePromise: Q.Promise; const toCreateKeyspace: boolean = this.keyspaceCreateNew(); const useAutoPilotForKeyspace: boolean = this.isSharedAutoPilotSelected() && !!this.sharedAutoPilotThroughput(); const createKeyspaceQueryPrefix: string = `CREATE KEYSPACE ${this.keyspaceId().trim()} WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 }`; const createKeyspaceQuery: string = this.keyspaceHasSharedOffer() ? useAutoPilotForKeyspace ? `${createKeyspaceQueryPrefix} AND ${autoPilotCommand}=${this.sharedAutoPilotThroughput()};` : `${createKeyspaceQueryPrefix} AND cosmosdb_provisioned_throughput=${this.keyspaceThroughput()};` : `${createKeyspaceQueryPrefix};`; const createTableQueryPrefix: string = `${this.createTableQuery()}${this.tableId().trim()} ${this.userTableQuery()}`; let createTableQuery: string; if (this.canConfigureThroughput() && (this.dedicateTableThroughput() || !this.keyspaceHasSharedOffer())) { if (this.isAutoPilotSelected() && this.selectedAutoPilotThroughput()) { createTableQuery = `${createTableQueryPrefix} WITH ${autoPilotCommand}=${this.selectedAutoPilotThroughput()};`; } else { createTableQuery = `${createTableQueryPrefix} WITH cosmosdb_provisioned_throughput=${this.throughput()};`; } } else { createTableQuery = `${createTableQueryPrefix};`; } const addCollectionPaneStartMessage = { collection: ko.toJS({ id: this.tableId(), storage: Constants.BackendDefaults.multiPartitionStorageInGb, offerThroughput: this.throughput(), partitionKey: "", databaseId: this.keyspaceId(), hasDedicatedThroughput: this.dedicateTableThroughput(), }), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), subscriptionType: userContext.subscriptionType, subscriptionQuotaId: userContext.quotaId, defaultsCheck: { storage: "u", throughput: this.throughput(), flight: userContext.addCollectionFlight, }, dataExplorerArea: Constants.Areas.ContextualPane, toCreateKeyspace: toCreateKeyspace, createKeyspaceQuery: createKeyspaceQuery, createTableQuery: createTableQuery, }; const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, addCollectionPaneStartMessage); const { databaseAccount } = userContext; if (toCreateKeyspace) { createTableAndKeyspacePromise = (this.container.tableDataClient).createTableAndKeyspace( databaseAccount?.properties.cassandraEndpoint, databaseAccount?.id, this.container, createTableQuery, createKeyspaceQuery ); } else { createTableAndKeyspacePromise = (this.container.tableDataClient).createTableAndKeyspace( databaseAccount?.properties.cassandraEndpoint, databaseAccount?.id, this.container, createTableQuery ); } createTableAndKeyspacePromise.then( () => { this.container.refreshAllDatabases(); this.isExecuting(false); this.close(); const addCollectionPaneSuccessMessage = { collection: ko.toJS({ id: this.tableId(), storage: Constants.BackendDefaults.multiPartitionStorageInGb, offerThroughput: this.throughput(), partitionKey: "", databaseId: this.keyspaceId(), hasDedicatedThroughput: this.dedicateTableThroughput(), }), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), subscriptionType: userContext.subscriptionType, subscriptionQuotaId: userContext.quotaId, defaultsCheck: { storage: "u", throughput: this.throughput(), flight: userContext.addCollectionFlight, }, dataExplorerArea: Constants.Areas.ContextualPane, toCreateKeyspace: toCreateKeyspace, createKeyspaceQuery: createKeyspaceQuery, createTableQuery: createTableQuery, }; TelemetryProcessor.traceSuccess(Action.CreateCollection, addCollectionPaneSuccessMessage, startKey); }, (error) => { const errorMessage = getErrorMessage(error); this.formErrors(errorMessage); this.isExecuting(false); const addCollectionPaneFailedMessage = { collection: { id: this.tableId(), storage: Constants.BackendDefaults.multiPartitionStorageInGb, offerThroughput: this.throughput(), partitionKey: "", databaseId: this.keyspaceId(), hasDedicatedThroughput: this.dedicateTableThroughput(), }, keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), subscriptionType: userContext.subscriptionType, subscriptionQuotaId: userContext.quotaId, defaultsCheck: { storage: "u", throughput: this.throughput(), flight: userContext.addCollectionFlight, }, dataExplorerArea: Constants.Areas.ContextualPane, toCreateKeyspace: toCreateKeyspace, createKeyspaceQuery: createKeyspaceQuery, createTableQuery: createTableQuery, error: errorMessage, errorStack: getErrorStack(error), }; TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey); } ); } public resetData() { super.resetData(); const throughputDefaults = this.container.collectionCreationDefaults.throughput; this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); this.isSharedAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); this.selectedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.throughput(AddCollectionUtility.getMaxThroughput(this.container.collectionCreationDefaults, this.container)); this.keyspaceThroughput(throughputDefaults.shared); this.maxThroughputRU(throughputDefaults.unlimitedmax); this.minThroughputRU(throughputDefaults.unlimitedmin); this.createTableQuery("CREATE TABLE "); this.userTableQuery("(userid int, name text, email text, PRIMARY KEY (userid))"); this.tableId(""); this.keyspaceId(""); this.throughputSpendAck(false); this.keyspaceHasSharedOffer(false); this.keyspaceCreateNew(true); } private _isValid(): boolean { const throughput = this.throughput(); const keyspaceThroughput = this.keyspaceThroughput(); const sharedAutoscaleThroughput = this.sharedAutoPilotThroughput() * 1; if ( this.isSharedAutoPilotSelected() && sharedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.sharedThroughputSpendAck() ) { this.formErrors(`Please acknowledge the estimated monthly spend.`); return false; } const dedicatedAutoscaleThroughput = this.selectedAutoPilotThroughput() * 1; if ( this.isAutoPilotSelected() && dedicatedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck() ) { this.formErrors(`Please acknowledge the estimated monthly spend.`); return false; } if ( (this.keyspaceCreateNew() && this.keyspaceHasSharedOffer() && this.isSharedAutoPilotSelected()) || this.isAutoPilotSelected() ) { const autoPilot = this._getAutoPilot(); if ( !autoPilot || !autoPilot.maxThroughput || !AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput) ) { this.formErrors( `Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput` ); return false; } return true; } if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) { this.formErrors(`Please acknowledge the estimated daily spend.`); return false; } if ( this.keyspaceHasSharedOffer() && this.keyspaceCreateNew() && keyspaceThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.sharedThroughputSpendAck() ) { this.formErrors("Please acknowledge the estimated daily spend"); return false; } return true; } private _getAutoPilot(): DataModels.AutoPilotCreationSettings { if ( this.keyspaceCreateNew() && this.keyspaceHasSharedOffer() && this.isSharedAutoPilotSelected() && this.sharedAutoPilotThroughput() ) { return { maxThroughput: this.sharedAutoPilotThroughput() * 1, }; } if (this.selectedAutoPilotThroughput()) { return { maxThroughput: this.selectedAutoPilotThroughput() * 1, }; } return undefined; } }