diff --git a/src/Common/DataAccessUtilityBase.ts b/src/Common/DataAccessUtilityBase.ts index 56da586d9..31eb162bb 100644 --- a/src/Common/DataAccessUtilityBase.ts +++ b/src/Common/DataAccessUtilityBase.ts @@ -184,86 +184,6 @@ export function deleteConflict( ); } -export function readCollectionQuotaInfo( - collection: ViewModels.Collection, - options: any -): Q.Promise { - options = options || {}; - options.populateQuotaInfo = true; - options.initialHeaders = options.initialHeaders || {}; - options.initialHeaders[Constants.HttpHeaders.populatePartitionStatistics] = true; - - return Q( - client() - .database(collection.databaseId) - .container(collection.id()) - .read(options) - // TODO any needed because SDK does not properly type response.resource.statistics - .then((response: any) => { - let quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers); - quota["usageSizeInKB"] = response.resource.statistics.reduce( - ( - previousValue: number, - currentValue: DataModels.Statistic, - currentIndex: number, - array: DataModels.Statistic[] - ) => { - return previousValue + currentValue.sizeInKB; - }, - 0 - ); - quota["numPartitions"] = response.resource.statistics.length; - quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617) - return quota; - }) - ); -} - -export function readOffers(options: any): Q.Promise { - if (options.isServerless) { - return Q([]); // Reading offers is not supported for serverless accounts - } - - try { - if (configContext.platform === Platform.Portal) { - return sendCachedDataMessage(MessageTypes.AllOffers, [ - (window).dataExplorer.databaseAccount().id, - Constants.ClientDefaults.portalCacheTimeoutMs - ]); - } - } catch (error) { - // If error getting cached Offers, continue on and read via SDK - } - return Q( - client() - .offers.readAll() - .fetchAll() - .then(response => response.resources) - .catch(error => { - // This should be removed when we can correctly identify if an account is serverless when connected using connection string too. - if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) { - return []; - } - throw error; - }) - ); -} - -export function readOffer(requestedResource: DataModels.Offer, options: any): Q.Promise { - options = options || {}; - options.initialHeaders = options.initialHeaders || {}; - if (!OfferUtils.isOfferV1(requestedResource)) { - options.initialHeaders[Constants.HttpHeaders.populateCollectionThroughputInfo] = true; - } - - return Q( - client() - .offer(requestedResource.id) - .read(options) - .then(response => ({ ...response.resource, headers: response.headers })) - ); -} - export function refreshCachedOffers(): Q.Promise { if (configContext.platform === Platform.Portal) { return sendCachedDataMessage(MessageTypes.RefreshOffers, []); diff --git a/src/Common/DocumentClientUtilityBase.ts b/src/Common/DocumentClientUtilityBase.ts index a1d47476a..f60d2b09d 100644 --- a/src/Common/DocumentClientUtilityBase.ts +++ b/src/Common/DocumentClientUtilityBase.ts @@ -277,78 +277,3 @@ export function refreshCachedResources(options: any = {}): Q.Promise { export function refreshCachedOffers(): Q.Promise { return DataAccessUtilityBase.refreshCachedOffers(); } - -export function readCollectionQuotaInfo( - collection: ViewModels.Collection, - options?: any -): Q.Promise { - var deferred = Q.defer(); - - const clearMessage = logConsoleProgress(`Querying quota info for container ${collection.id}`); - DataAccessUtilityBase.readCollectionQuotaInfo(collection, options) - .then( - (quota: DataModels.CollectionQuotaInfo) => { - deferred.resolve(quota); - }, - (error: any) => { - logConsoleError(`Error while querying quota info for container ${collection.id}:\n ${JSON.stringify(error)}`); - Logger.logError(JSON.stringify(error), "ReadCollectionQuotaInfo", error.code); - sendNotificationForError(error); - deferred.reject(error); - } - ) - .finally(() => { - clearMessage(); - }); - - return deferred.promise; -} - -export function readOffers(options: any = {}): Q.Promise { - var deferred = Q.defer(); - - const clearMessage = logConsoleProgress("Querying offers"); - DataAccessUtilityBase.readOffers(options) - .then( - (offers: DataModels.Offer[]) => { - deferred.resolve(offers); - }, - (error: any) => { - logConsoleError(`Error while querying offers:\n ${JSON.stringify(error)}`); - Logger.logError(JSON.stringify(error), "ReadOffers", error.code); - sendNotificationForError(error); - deferred.reject(error); - } - ) - .finally(() => { - clearMessage(); - }); - - return deferred.promise; -} - -export function readOffer( - requestedResource: DataModels.Offer, - options: any = {} -): Q.Promise { - var deferred = Q.defer(); - - const clearMessage = logConsoleProgress("Querying offer"); - DataAccessUtilityBase.readOffer(requestedResource, options) - .then( - (offer: DataModels.OfferWithHeaders) => { - deferred.resolve(offer); - }, - (error: any) => { - logConsoleError(`Error while querying offer:\n ${JSON.stringify(error)}`); - Logger.logError(JSON.stringify(error), "ReadOffer", error.code); - sendNotificationForError(error); - deferred.reject(error); - } - ) - .finally(() => { - clearMessage(); - }); - - return deferred.promise; -} diff --git a/src/Common/dataAccess/readCollectionOffer.ts b/src/Common/dataAccess/readCollectionOffer.ts new file mode 100644 index 000000000..29501a9f2 --- /dev/null +++ b/src/Common/dataAccess/readCollectionOffer.ts @@ -0,0 +1,126 @@ +import * as DataModels from "../../Contracts/DataModels"; +import { AuthType } from "../../AuthType"; +import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; +import { HttpHeaders } from "../Constants"; +import { RequestOptions } from "@azure/cosmos/dist-esm"; +import { client } from "../CosmosClient"; +import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; +import { getCassandraTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; +import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; +import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; +import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils"; +import { logError } from "../Logger"; +import { readOffers } from "./readOffers"; +import { sendNotificationForError } from "./sendNotificationForError"; +import { userContext } from "../../UserContext"; + +export const readCollectionOffer = async ( + params: DataModels.ReadCollectionOfferParams +): Promise => { + const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`); + let offerId = params.offerId; + if (!offerId) { + if (window.authType === AuthType.AAD && !userContext.useSDKOperations) { + try { + offerId = await getCollectionOfferIdWithARM(params.databaseId, params.collectionId); + } catch (error) { + clearMessage(); + if (error.code !== "NotFound") { + throw error; + } + return undefined; + } + } else { + offerId = await getCollectionOfferIdWithSDK(params.collectionResourceId); + if (!offerId) { + clearMessage(); + return undefined; + } + } + } + + const options: RequestOptions = { + initialHeaders: { + [HttpHeaders.populateCollectionThroughputInfo]: true + } + }; + + try { + const response = await client() + .offer(offerId) + .read(options); + return ( + response && { + ...response.resource, + headers: response.headers + } + ); + } catch (error) { + logConsoleError(`Error while querying offer for collection ${params.collectionId}:\n ${JSON.stringify(error)}`); + logError(JSON.stringify(error), "ReadCollectionOffer", error.code); + sendNotificationForError(error); + throw error; + } finally { + clearMessage(); + } +}; + +const getCollectionOfferIdWithARM = async (databaseId: string, collectionId: string): Promise => { + let rpResponse; + const subscriptionId = userContext.subscriptionId; + const resourceGroup = userContext.resourceGroup; + const accountName = userContext.databaseAccount.name; + const defaultExperience = userContext.defaultExperience; + switch (defaultExperience) { + case DefaultAccountExperienceType.DocumentDB: + rpResponse = await getSqlContainerThroughput( + subscriptionId, + resourceGroup, + accountName, + databaseId, + collectionId + ); + break; + case DefaultAccountExperienceType.MongoDB: + rpResponse = await getMongoDBCollectionThroughput( + subscriptionId, + resourceGroup, + accountName, + databaseId, + collectionId + ); + break; + case DefaultAccountExperienceType.Cassandra: + rpResponse = await getCassandraTableThroughput( + subscriptionId, + resourceGroup, + accountName, + databaseId, + collectionId + ); + break; + case DefaultAccountExperienceType.Graph: + rpResponse = await getGremlinGraphThroughput( + subscriptionId, + resourceGroup, + accountName, + databaseId, + collectionId + ); + break; + case DefaultAccountExperienceType.Table: + rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId); + break; + default: + throw new Error(`Unsupported default experience type: ${defaultExperience}`); + } + + return rpResponse?.name; +}; + +const getCollectionOfferIdWithSDK = async (collectionResourceId: string): Promise => { + const offers = await readOffers(); + const offer = offers.find(offer => offer.resource === collectionResourceId); + return offer?.id; +}; diff --git a/src/Common/dataAccess/readCollectionQuotaInfo.ts b/src/Common/dataAccess/readCollectionQuotaInfo.ts new file mode 100644 index 000000000..b2cdbc273 --- /dev/null +++ b/src/Common/dataAccess/readCollectionQuotaInfo.ts @@ -0,0 +1,48 @@ +import * as DataModels from "../../Contracts/DataModels"; +import * as HeadersUtility from "../HeadersUtility"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { ContainerDefinition, Resource } from "@azure/cosmos"; +import { HttpHeaders } from "../Constants"; +import { RequestOptions } from "@azure/cosmos/dist-esm"; +import { client } from "../CosmosClient"; +import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils"; +import { logError } from "../Logger"; +import { sendNotificationForError } from "./sendNotificationForError"; + +interface ResourceWithStatistics { + statistics: DataModels.Statistic[]; +} + +export const readCollectionQuotaInfo = async ( + collection: ViewModels.Collection +): Promise => { + const clearMessage = logConsoleProgress(`Querying containers for database ${collection.id}`); + const options: RequestOptions = {}; + options.populateQuotaInfo = true; + options.initialHeaders = options.initialHeaders || {}; + options.initialHeaders[HttpHeaders.populatePartitionStatistics] = true; + + try { + const response = await client() + .database(collection.databaseId) + .container(collection.id()) + .read(options); + const quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers); + const resource = response.resource as ContainerDefinition & Resource & ResourceWithStatistics; + quota["usageSizeInKB"] = resource.statistics.reduce( + (previousValue: number, currentValue: DataModels.Statistic) => previousValue + currentValue.sizeInKB, + 0 + ); + quota["numPartitions"] = resource.statistics.length; + quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617) + + return quota; + } catch (error) { + logConsoleError(`Error while querying quota info for container ${collection.id}:\n ${JSON.stringify(error)}`); + logError(JSON.stringify(error), "ReadCollectionQuotaInfo", error.code); + sendNotificationForError(error); + throw error; + } finally { + clearMessage(); + } +}; diff --git a/src/Common/dataAccess/readDatabaseOffer.ts b/src/Common/dataAccess/readDatabaseOffer.ts index 9c940de8c..bd31d17e5 100644 --- a/src/Common/dataAccess/readDatabaseOffer.ts +++ b/src/Common/dataAccess/readDatabaseOffer.ts @@ -8,12 +8,16 @@ import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020- import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; +import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils"; +import { logError } from "../Logger"; import { readOffers } from "./readOffers"; +import { sendNotificationForError } from "./sendNotificationForError"; import { userContext } from "../../UserContext"; export const readDatabaseOffer = async ( params: DataModels.ReadDatabaseOfferParams ): Promise => { + const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`); let offerId = params.offerId; if (!offerId) { if ( @@ -24,14 +28,16 @@ export const readDatabaseOffer = async ( try { offerId = await getDatabaseOfferIdWithARM(params.databaseId); } catch (error) { + clearMessage(); if (error.code !== "NotFound") { - throw new Error(error); + throw new error(); } return undefined; } } else { offerId = await getDatabaseOfferIdWithSDK(params.databaseResourceId); if (!offerId) { + clearMessage(); return undefined; } } @@ -43,15 +49,24 @@ export const readDatabaseOffer = async ( } }; - const response = await client() - .offer(offerId) - .read(options); - return ( - response && { - ...response.resource, - headers: response.headers - } - ); + try { + const response = await client() + .offer(offerId) + .read(options); + return ( + response && { + ...response.resource, + headers: response.headers + } + ); + } catch (error) { + logConsoleError(`Error while querying offer for database ${params.databaseId}:\n ${JSON.stringify(error)}`); + logError(JSON.stringify(error), "ReadDatabaseOffer", error.code); + sendNotificationForError(error); + throw error; + } finally { + clearMessage(); + } }; const getDatabaseOfferIdWithARM = async (databaseId: string): Promise => { diff --git a/src/Common/dataAccess/readOffers.ts b/src/Common/dataAccess/readOffers.ts index 8a5f9a30c..1ce036c3b 100644 --- a/src/Common/dataAccess/readOffers.ts +++ b/src/Common/dataAccess/readOffers.ts @@ -3,10 +3,14 @@ import { ClientDefaults } from "../Constants"; import { MessageTypes } from "../../Contracts/ExplorerContracts"; import { Platform, configContext } from "../../ConfigContext"; import { client } from "../CosmosClient"; +import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils"; +import { logError } from "../Logger"; import { sendCachedDataMessage } from "../MessageHandler"; +import { sendNotificationForError } from "./sendNotificationForError"; import { userContext } from "../../UserContext"; export const readOffers = async (): Promise => { + const clearMessage = logConsoleProgress(`Querying offers`); try { if (configContext.platform === Platform.Portal) { return sendCachedDataMessage(MessageTypes.AllOffers, [ @@ -18,15 +22,22 @@ export const readOffers = async (): Promise => { // If error getting cached Offers, continue on and read via SDK } - return client() - .offers.readAll() - .fetchAll() - .then(response => response.resources) - .catch(error => { - // This should be removed when we can correctly identify if an account is serverless when connected using connection string too. - if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) { - return []; - } - throw error; - }); + try { + const response = await client() + .offers.readAll() + .fetchAll(); + return response?.resources; + } catch (error) { + // This should be removed when we can correctly identify if an account is serverless when connected using connection string too. + if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) { + return []; + } + + logConsoleError(`Error while querying offers:\n ${JSON.stringify(error)}`); + logError(JSON.stringify(error), "ReadOffers", error.code); + sendNotificationForError(error); + throw error; + } finally { + clearMessage(); + } }; diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 3c1b8474a..a2a66f332 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -296,6 +296,13 @@ export interface ReadDatabaseOfferParams { offerId?: string; } +export interface ReadCollectionOfferParams { + collectionId: string; + databaseId: string; + collectionResourceId?: string; + offerId?: string; +} + export interface Notification { id: string; kind: string; diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index fd3b10fa8..49e34c7ad 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -133,8 +133,7 @@ export interface Collection extends CollectionBase { onMongoDBDocumentsClick(): void; openTab(): void; - onSettingsClick: () => void; - readSettings(): Q.Promise; + onSettingsClick: () => Promise; onDeleteCollectionContextMenuClick(source: Collection, event: MouseEvent): void; onNewGraphClick(): void; @@ -162,6 +161,7 @@ export interface Collection extends CollectionBase { loadUserDefinedFunctions(): Promise; loadStoredProcedures(): Promise; loadTriggers(): Promise; + loadOffer(): Promise; createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure; createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction; diff --git a/src/Explorer/Tabs/SettingsTab.ts b/src/Explorer/Tabs/SettingsTab.ts index 7abeda7df..fc7f39dab 100644 --- a/src/Explorer/Tabs/SettingsTab.ts +++ b/src/Explorer/Tabs/SettingsTab.ts @@ -1181,7 +1181,6 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor this.container.isRefreshingExplorer(false); this._setBaseline(); - this.collection.readSettings(); this._wasAutopilotOriginallySet(this.isAutoPilotSelected()); TelemetryProcessor.traceSuccess( Action.UpdateSettings, diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 835142ea7..9e7a3bb25 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -8,7 +8,9 @@ import * as Constants from "../../Common/Constants"; import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures"; import { readTriggers } from "../../Common/dataAccess/readTriggers"; import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions"; -import { createDocument, readCollectionQuotaInfo, readOffer, readOffers } from "../../Common/DocumentClientUtilityBase"; +import { createDocument } from "../../Common/DocumentClientUtilityBase"; +import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer"; +import { readCollectionQuotaInfo } from "../../Common/dataAccess/readCollectionQuotaInfo"; import * as Logger from "../../Common/Logger"; import { configContext } from "../../ConfigContext"; import * as DataModels from "../../Contracts/DataModels"; @@ -541,7 +543,7 @@ export default class Collection implements ViewModels.Collection { } }; - public onSettingsClick = () => { + public onSettingsClick = async (): Promise => { this.container.selectedNode(this); this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings); TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { @@ -553,6 +555,7 @@ export default class Collection implements ViewModels.Collection { dataExplorerArea: Constants.Areas.ResourceTree }); + await this.loadOffer(); const tabTitle = !this.offer() ? "Settings" : "Scale & Settings"; const pendingNotificationsPromise: Q.Promise = this._getPendingThroughputSplitNotification(); const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Settings, tab => { @@ -570,12 +573,12 @@ export default class Collection implements ViewModels.Collection { tabTitle: tabTitle }); - Q.all([pendingNotificationsPromise, this.readSettings()]).then( + pendingNotificationsPromise.then( (data: any) => { const pendingNotification: DataModels.Notification = data && data[0]; settingsTab = new SettingsTab({ tabKind: ViewModels.CollectionTabKind.Settings, - title: !this.offer() ? "Settings" : "Scale & Settings", + title: tabTitle, tabPath: "", collection: this, @@ -624,103 +627,12 @@ export default class Collection implements ViewModels.Collection { } }; - public readSettings(): Q.Promise { - const deferred: Q.Deferred = Q.defer(); - this.container.isRefreshingExplorer(true); - const collectionDataModel: DataModels.Collection = { - id: this.id(), - _rid: this.rid, - _self: this.self, - defaultTtl: this.defaultTtl(), - indexingPolicy: this.indexingPolicy(), - partitionKey: this.partitionKey - }; - const startKey: number = TelemetryProcessor.traceStart(Action.LoadOffers, { - databaseAccountName: this.container.databaseAccount().name, - databaseName: this.databaseId, - collectionName: this.id(), - defaultExperience: this.container.defaultExperience() - }); + private async loadCollectionQuotaInfo(): Promise { // TODO: Use the collection entity cache to get quota info - const quotaInfoPromise: Q.Promise = readCollectionQuotaInfo(this); - const offerInfoPromise: Q.Promise = readOffers({ - isServerless: this.container.isServerlessEnabled() - }); - Q.all([quotaInfoPromise, offerInfoPromise]).then( - () => { - this.container.isRefreshingExplorer(false); - const quotaInfoWithUniqueKeyPolicy: DataModels.CollectionQuotaInfo = quotaInfoPromise.valueOf(); - this.uniqueKeyPolicy = quotaInfoWithUniqueKeyPolicy.uniqueKeyPolicy; - const quotaInfo = _.omit(quotaInfoWithUniqueKeyPolicy, "uniqueKeyPolicy"); - - const collectionOffer = this._getOfferForCollection(offerInfoPromise.valueOf(), collectionDataModel); - if (!collectionOffer) { - this.quotaInfo(quotaInfo); - TelemetryProcessor.traceSuccess( - Action.LoadOffers, - { - databaseAccountName: this.container.databaseAccount().name, - databaseName: this.databaseId, - collectionName: this.id(), - defaultExperience: this.container.defaultExperience() - }, - startKey - ); - deferred.resolve(); - return; - } - - readOffer(collectionOffer).then((offerDetail: DataModels.OfferWithHeaders) => { - if (OfferUtils.isNotOfferV1(collectionOffer)) { - const offerThroughputInfo: DataModels.OfferThroughputInfo = { - minimumRUForCollection: - offerDetail.content && - offerDetail.content.collectionThroughputInfo && - offerDetail.content.collectionThroughputInfo.minimumRUForCollection, - numPhysicalPartitions: - offerDetail.content && - offerDetail.content.collectionThroughputInfo && - offerDetail.content.collectionThroughputInfo.numPhysicalPartitions - }; - - collectionOffer.content.collectionThroughputInfo = offerThroughputInfo; - } - - (collectionOffer as DataModels.OfferWithHeaders).headers = offerDetail.headers; - this.offer(collectionOffer); - this.offer.valueHasMutated(); - this.quotaInfo(quotaInfo); - TelemetryProcessor.traceSuccess( - Action.LoadOffers, - { - databaseAccountName: this.container.databaseAccount().name, - databaseName: this.databaseId, - collectionName: this.id(), - defaultExperience: this.container.defaultExperience(), - offerVersion: collectionOffer && collectionOffer.offerVersion - }, - startKey - ); - deferred.resolve(); - }); - }, - (error: any) => { - this.container.isRefreshingExplorer(false); - deferred.reject(error); - TelemetryProcessor.traceFailure( - Action.LoadOffers, - { - databaseAccountName: this.container.databaseAccount().name, - databaseName: this.databaseId, - collectionName: this.id(), - defaultExperience: this.container.defaultExperience() - }, - startKey - ); - } - ); - - return deferred.promise; + const quotaInfoWithUniqueKeyPolicy = await readCollectionQuotaInfo(this); + this.uniqueKeyPolicy = quotaInfoWithUniqueKeyPolicy.uniqueKeyPolicy; + const quotaInfo = _.omit(quotaInfoWithUniqueKeyPolicy, "uniqueKeyPolicy"); + this.quotaInfo(quotaInfo); } public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) { @@ -1399,4 +1311,53 @@ export default class Collection implements ViewModels.Collection { public getDatabase(): ViewModels.Database { return this.container.findDatabaseWithId(this.databaseId); } + + public async loadOffer(): Promise { + if (!this.container.isServerlessEnabled() && !this.offer()) { + this.container.isRefreshingExplorer(true); + const startKey: number = TelemetryProcessor.traceStart(Action.LoadOffers, { + databaseAccountName: this.container.databaseAccount().name, + databaseName: this.databaseId, + collectionName: this.id(), + defaultExperience: this.container.defaultExperience() + }); + + const params: DataModels.ReadCollectionOfferParams = { + collectionId: this.id(), + collectionResourceId: this.self, + databaseId: this.databaseId + }; + + try { + this.offer(await readCollectionOffer(params)); + await this.loadCollectionQuotaInfo(); + + TelemetryProcessor.traceSuccess( + Action.LoadOffers, + { + databaseAccountName: this.container.databaseAccount().name, + databaseName: this.databaseId, + collectionName: this.id(), + defaultExperience: this.container.defaultExperience(), + offerVersion: this.offer()?.offerVersion + }, + startKey + ); + } catch (error) { + TelemetryProcessor.traceFailure( + Action.LoadOffers, + { + databaseAccountName: this.container.databaseAccount().name, + databaseName: this.databaseId, + collectionName: this.id(), + defaultExperience: this.container.defaultExperience() + }, + startKey + ); + throw error; + } finally { + this.container.isRefreshingExplorer(false); + } + } + } }