From 33969581ac9772ac93d2eb338fdf350f2f3392ac Mon Sep 17 00:00:00 2001 From: Tanuj Mittal Date: Fri, 24 Jul 2020 13:13:54 -0700 Subject: [PATCH] Support serverless accounts (#109) * Changes for serverless accounts * Dont show upsell message for serverless accounts * Update CassandraAddCollectionPane to support serverless --- src/Common/Constants.ts | 1 + src/Explorer/Explorer.ts | 173 ++++++++++-------- src/Explorer/Panes/AddCollectionPane.html | 18 +- src/Explorer/Panes/AddCollectionPane.ts | 24 ++- src/Explorer/Panes/AddDatabasePane.html | 6 +- src/Explorer/Panes/AddDatabasePane.ts | 14 +- .../Panes/CassandraAddCollectionPane.html | 9 + .../Panes/CassandraAddCollectionPane.ts | 4 +- src/Explorer/Tree/Collection.ts | 7 +- src/Explorer/Tree/Database.ts | 4 + src/Explorer/Tree/ResourceTreeAdapter.tsx | 2 +- 11 files changed, 171 insertions(+), 91 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 9da0ab448..79df492a0 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -101,6 +101,7 @@ export class CapabilityNames { public static readonly EnableNotebooks: string = "EnableNotebooks"; public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics"; public static readonly EnableMongo: string = "EnableMongo"; + public static readonly EnableServerless: string = "EnableServerless"; } export class Features { diff --git a/src/Explorer/Explorer.ts b/src/Explorer/Explorer.ts index 0f146c71d..7926f7c07 100644 --- a/src/Explorer/Explorer.ts +++ b/src/Explorer/Explorer.ts @@ -118,6 +118,7 @@ export default class Explorer { public isPreferredApiGraph: ko.Computed; public isPreferredApiTable: ko.Computed; public isFixedCollectionWithSharedThroughputSupported: ko.Computed; + public isServerlessEnabled: ko.Computed; public isEmulator: boolean; public isAccountReady: ko.Observable; public canSaveQueries: ko.Computed; @@ -509,6 +510,14 @@ export default class Explorer { return false; }); + this.isServerlessEnabled = ko.computed( + () => + this.databaseAccount && + this.databaseAccount()?.properties?.capabilities?.find( + item => item.name === Constants.CapabilityNames.EnableServerless + ) !== undefined + ); + this.isPreferredApiMongoDB = ko.computed(() => { const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.MongoDB.toLowerCase()) { @@ -1406,87 +1415,97 @@ export default class Explorer { dataExplorerArea: Constants.Areas.ResourceTree }); } + // TODO: Refactor const deferred: Q.Deferred = Q.defer(); - const offerPromise: Q.Promise = this.documentClientUtility.readOffers(); - this._setLoadingStatusText("Fetching offers..."); - offerPromise.then( - (offers: DataModels.Offer[]) => { - this._setLoadingStatusText("Successfully fetched offers."); - this._setLoadingStatusText("Fetching databases..."); - this.documentClientUtility.readDatabases(null /*options*/).then( - (databases: DataModels.Database[]) => { - this._setLoadingStatusText("Successfully fetched databases."); - TelemetryProcessor.traceSuccess( - Action.LoadDatabases, - { - databaseAccountName: this.databaseAccount().name, - defaultExperience: this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + const refreshDatabases = (offers?: DataModels.Offer[]) => { + this._setLoadingStatusText("Fetching databases..."); + this.documentClientUtility.readDatabases(null /*options*/).then( + (databases: DataModels.Database[]) => { + this._setLoadingStatusText("Successfully fetched databases."); + TelemetryProcessor.traceSuccess( + Action.LoadDatabases, + { + databaseAccountName: this.databaseAccount().name, + defaultExperience: this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree + }, + startKey + ); + const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode(); + const deltaDatabases = this.getDeltaDatabases(databases, offers); + this.addDatabasesToList(deltaDatabases.toAdd); + this.deleteDatabasesFromList(deltaDatabases.toDelete); + this.selectedNode(currentlySelectedNode); + this._setLoadingStatusText("Fetching containers..."); + this.refreshAndExpandNewDatabases(deltaDatabases.toAdd) + .then( + () => { + this._setLoadingStatusText("Successfully fetched containers."); + deferred.resolve(); }, - startKey - ); - const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode(); - const deltaDatabases = this.getDeltaDatabases(databases, offers); - this.addDatabasesToList(deltaDatabases.toAdd); - this.deleteDatabasesFromList(deltaDatabases.toDelete); - this.selectedNode(currentlySelectedNode); - this._setLoadingStatusText("Fetching containers..."); - this.refreshAndExpandNewDatabases(deltaDatabases.toAdd) - .then( - () => { - this._setLoadingStatusText("Successfully fetched containers."); - deferred.resolve(); - }, - reason => { - this._setLoadingStatusText("Failed to fetch containers."); - deferred.reject(reason); - } - ) - .finally(() => this.isRefreshingExplorer(false)); - }, - error => { - this._setLoadingStatusText("Failed to fetch databases."); - this.isRefreshingExplorer(false); - deferred.reject(error); - TelemetryProcessor.traceFailure( - Action.LoadDatabases, - { - databaseAccountName: this.databaseAccount().name, - defaultExperience: this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree, - error: JSON.stringify(error) - }, - startKey - ); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Error while refreshing databases: ${JSON.stringify(error)}` - ); - } - ); - }, - error => { - this._setLoadingStatusText("Failed to fetch offers."); - this.isRefreshingExplorer(false); - deferred.reject(error); - TelemetryProcessor.traceFailure( - Action.LoadDatabases, - { - databaseAccountName: this.databaseAccount().name, - defaultExperience: this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree, - error: JSON.stringify(error) - }, - startKey - ); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Error while refreshing databases: ${JSON.stringify(error)}` - ); - } - ); + reason => { + this._setLoadingStatusText("Failed to fetch containers."); + deferred.reject(reason); + } + ) + .finally(() => this.isRefreshingExplorer(false)); + }, + error => { + this._setLoadingStatusText("Failed to fetch databases."); + this.isRefreshingExplorer(false); + deferred.reject(error); + TelemetryProcessor.traceFailure( + Action.LoadDatabases, + { + databaseAccountName: this.databaseAccount().name, + defaultExperience: this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + error: JSON.stringify(error) + }, + startKey + ); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Error while refreshing databases: ${JSON.stringify(error)}` + ); + } + ); + }; + + if (this.isServerlessEnabled()) { + // Serverless accounts don't support offers call + refreshDatabases(); + } else { + const offerPromise: Q.Promise = this.documentClientUtility.readOffers(); + this._setLoadingStatusText("Fetching offers..."); + offerPromise.then( + (offers: DataModels.Offer[]) => { + this._setLoadingStatusText("Successfully fetched offers."); + refreshDatabases(offers); + }, + error => { + this._setLoadingStatusText("Failed to fetch offers."); + this.isRefreshingExplorer(false); + deferred.reject(error); + TelemetryProcessor.traceFailure( + Action.LoadDatabases, + { + databaseAccountName: this.databaseAccount().name, + defaultExperience: this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + error: JSON.stringify(error) + }, + startKey + ); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Error while refreshing databases: ${JSON.stringify(error)}` + ); + } + ); + } return deferred.promise.then( () => { diff --git a/src/Explorer/Panes/AddCollectionPane.html b/src/Explorer/Panes/AddCollectionPane.html index 58d7cd3ef..21faab149 100644 --- a/src/Explorer/Panes/AddCollectionPane.html +++ b/src/Explorer/Panes/AddCollectionPane.html @@ -56,7 +56,7 @@ -
+
Promo @@ -112,7 +112,9 @@ + +
+
@@ -414,7 +417,10 @@ more

- + + + +
@@ -434,7 +440,8 @@ level.
- + +
@@ -467,6 +474,7 @@
+
@@ -499,8 +507,10 @@
- + + +
diff --git a/src/Explorer/Panes/AddCollectionPane.ts b/src/Explorer/Panes/AddCollectionPane.ts index 94d3996d1..c6d6b5eb0 100644 --- a/src/Explorer/Panes/AddCollectionPane.ts +++ b/src/Explorer/Panes/AddCollectionPane.ts @@ -93,6 +93,8 @@ export default class AddCollectionPane extends ContextualPaneBase { public canExceedMaximumValue: ko.PureComputed; public hasAutoPilotV2FeatureFlag: ko.PureComputed; public ruToolTipText: ko.Computed; + public canConfigureThroughput: ko.PureComputed; + public showUpsellMessage: ko.PureComputed; private _databaseOffers: HashMap; private _isSynapseLinkEnabled: ko.Computed; @@ -102,6 +104,8 @@ export default class AddCollectionPane extends ContextualPaneBase { this._databaseOffers = new HashMap(); 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.formWarnings = ko.observable(); this.collectionId = ko.observable(); this.databaseId = ko.observable(); @@ -591,6 +595,11 @@ export default class AddCollectionPane extends ContextualPaneBase { if (config.platform === Platform.Emulator) { return false; } + + if (this.container.isServerlessEnabled()) { + return false; + } + if (this.container.isPreferredApiDocumentDB()) { return true; } @@ -723,10 +732,19 @@ export default class AddCollectionPane extends ContextualPaneBase { } private _computeOfferThroughput(): number { - if (this.databaseCreateNewShared()) { - return this.isSharedAutoPilotSelected() ? undefined : this._getThroughput(); + if (!this.canConfigureThroughput()) { + return undefined; } - return this.isAutoPilotSelected() ? undefined : this._getThroughput(); + + if (this.isAutoPilotSelected()) { + return undefined; + } + + if (this.databaseCreateNewShared() && this.isSharedAutoPilotSelected()) { + return undefined; + } + + return this._getThroughput(); } public submit() { diff --git a/src/Explorer/Panes/AddDatabasePane.html b/src/Explorer/Panes/AddDatabasePane.html index e652ce7bf..4df48d3f8 100644 --- a/src/Explorer/Panes/AddDatabasePane.html +++ b/src/Explorer/Panes/AddDatabasePane.html @@ -48,7 +48,7 @@ -
+
Promo @@ -76,6 +76,9 @@ title="May not end with space nor contain characters '\' '/' '#' '?'" placeholder="Type a new database id" size="40" class="collid" data-bind="textInput: databaseId, hasFocus: firstFieldHasFocus" aria-label="Database id" autofocus> + + +
@@ -156,6 +159,7 @@ support for more than RU/s.

+
diff --git a/src/Explorer/Panes/AddDatabasePane.ts b/src/Explorer/Panes/AddDatabasePane.ts index f2e32acbb..4571fe24e 100644 --- a/src/Explorer/Panes/AddDatabasePane.ts +++ b/src/Explorer/Panes/AddDatabasePane.ts @@ -49,6 +49,8 @@ export default class AddDatabasePane extends ContextualPaneBase { 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); @@ -56,6 +58,8 @@ export default class AddDatabasePane extends ContextualPaneBase { 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()); @@ -522,7 +526,15 @@ export default class AddDatabasePane extends ContextualPaneBase { } private _computeOfferThroughput(): number { - return this.isAutoPilotSelected() ? undefined : this._getThroughput(); + if (!this.canConfigureThroughput()) { + return undefined; + } + + if (this.isAutoPilotSelected()) { + return undefined; + } + + return this._getThroughput(); } private _isValid(): boolean { diff --git a/src/Explorer/Panes/CassandraAddCollectionPane.html b/src/Explorer/Panes/CassandraAddCollectionPane.html index 7728026f0..ae3f8bd6f 100644 --- a/src/Explorer/Panes/CassandraAddCollectionPane.html +++ b/src/Explorer/Panes/CassandraAddCollectionPane.html @@ -118,6 +118,8 @@ + +
+ +

@@ -231,6 +235,9 @@ style="height:125px; width: calc(100% - 80px); resize: vertical;" >

+ + +
+ +
diff --git a/src/Explorer/Panes/CassandraAddCollectionPane.ts b/src/Explorer/Panes/CassandraAddCollectionPane.ts index 21d505537..00097e86f 100644 --- a/src/Explorer/Panes/CassandraAddCollectionPane.ts +++ b/src/Explorer/Panes/CassandraAddCollectionPane.ts @@ -51,6 +51,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { public hasAutoPilotV2FeatureFlag: ko.PureComputed; public isFreeTierAccount: ko.Computed; public ruToolTipText: ko.Computed; + public canConfigureThroughput: ko.PureComputed; private keyspaceOffers: HashMap; @@ -61,6 +62,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { this.keyspaceCreateNew = ko.observable(true); this.hasAutoPilotV2FeatureFlag = ko.pureComputed(() => this.container.hasAutoPilotV2FeatureFlag()); this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText(this.hasAutoPilotV2FeatureFlag())); + this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled()); this.keyspaceOffers = new HashMap(); this.keyspaceIds = ko.observableArray(); this.keyspaceHasSharedOffer = ko.observable(false); @@ -365,7 +367,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { const createTableQueryPrefix: string = `${this.createTableQuery()}${this.tableId().trim()} ${this.userTableQuery()}`; let createTableQuery: string; - if (this.dedicateTableThroughput() || !this.keyspaceHasSharedOffer()) { + if (this.canConfigureThroughput() && (this.dedicateTableThroughput() || !this.keyspaceHasSharedOffer())) { if (this.isAutoPilotSelected() && this.selectedAutoPilotThroughput()) { createTableQuery = `${createTableQueryPrefix} WITH ${autoPilotCommand}=${this.selectedAutoPilotThroughput()};`; } else { diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index afda94cce..0c0ee8e08 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -553,7 +553,7 @@ export default class Collection implements ViewModels.Collection { dataExplorerArea: Constants.Areas.ResourceTree }); - const tabTitle = "Scale & Settings"; + const tabTitle = !this.offer() ? "Settings" : "Scale & Settings"; const pendingNotificationsPromise: Q.Promise = this._getPendingThroughputSplitNotification(); const matchingTabs: ViewModels.Tab[] = this.container.tabsManager.getTabs( ViewModels.CollectionTabKind.Settings, @@ -561,6 +561,7 @@ export default class Collection implements ViewModels.Collection { return tab.collection && tab.collection.rid === this.rid; } ); + let settingsTab: SettingsTab = matchingTabs && (matchingTabs[0] as SettingsTab); if (!settingsTab) { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { @@ -582,7 +583,6 @@ export default class Collection implements ViewModels.Collection { documentClientUtility: this.container.documentClientUtility, collection: this, node: this, - selfLink: this.self, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/settings`, isActive: ko.observable(false), @@ -658,7 +658,8 @@ export default class Collection implements ViewModels.Collection { const collectionOffer = this._getOfferForCollection(offerInfoPromise.valueOf(), collectionDataModel); const isDatabaseShared = this.getDatabase() && this.getDatabase().isDatabaseShared(); - if (isDatabaseShared && !collectionOffer) { + const isServerless = this.container.isServerlessEnabled(); + if ((isDatabaseShared || isServerless) && !collectionOffer) { this.quotaInfo(quotaInfo); TelemetryProcessor.traceSuccess( Action.LoadOffers, diff --git a/src/Explorer/Tree/Database.ts b/src/Explorer/Tree/Database.ts index 3e72f4ffa..24891c957 100644 --- a/src/Explorer/Tree/Database.ts +++ b/src/Explorer/Tree/Database.ts @@ -122,6 +122,10 @@ export default class Database implements ViewModels.Database { public readSettings(): Q.Promise { const deferred: Q.Deferred = Q.defer(); + if (this.container.isServerlessEnabled()) { + deferred.resolve(); + } + this.container.isRefreshingExplorer(true); const databaseDataModel: DataModels.Database = { id: this.id(), diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index e4628aecc..2b00a37d9 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -235,7 +235,7 @@ export class ResourceTreeAdapter implements ReactAdapter { }); children.push({ - label: database.isDatabaseShared() ? "Settings" : "Scale & Settings", + label: database.isDatabaseShared() || this.container.isServerlessEnabled() ? "Settings" : "Scale & Settings", onClick: collection.onSettingsClick.bind(collection), isSelected: () => this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.Settings) });