diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 7645d4b2a..a3ce1d6c6 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -119,6 +119,7 @@ export class Features { public static readonly enableSchema = "enableschema"; public static readonly enableSDKoperations = "enablesdkoperations"; public static readonly showMinRUSurvey = "showminrusurvey"; + public static readonly enableDatabaseSettingsTabV1 = "enabledbsettingsv1"; public static readonly selfServeType = "selfservetype"; public static readonly enableKOPanel = "enablekopanel"; } diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 73e1f5fb8..7526973c4 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -91,6 +91,7 @@ export interface Database extends TreeNode { onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void; onSettingsClick: () => void; loadOffer(): Promise; + getPendingThroughputSplitNotification(): Promise; } export interface CollectionBase extends TreeNode { @@ -177,6 +178,7 @@ export interface Collection extends CollectionBase { uploadFiles(fileList: FileList): Promise; getLabel(): string; + getPendingThroughputSplitNotification(): Promise; } /** @@ -291,10 +293,6 @@ export interface DocumentsTabOptions extends TabOptions { resourceTokenPartitionKey?: string; } -export interface SettingsTabV2Options extends TabOptions { - getPendingNotification: Promise; -} - export interface ConflictsTabOptions extends TabOptions { partitionKey: DataModels.PartitionKey; conflictIds: ko.ObservableArray; @@ -361,7 +359,8 @@ export enum CollectionTabKind { Gallery = 17, NotebookViewer = 18, Schema = 19, - SettingsV2 = 20, + CollectionSettingsV2 = 20, + DatabaseSettingsV2 = 21, } export enum TerminalKind { diff --git a/src/Explorer/ComponentRegisterer.test.ts b/src/Explorer/ComponentRegisterer.test.ts index f55d41185..7ad1dba5b 100644 --- a/src/Explorer/ComponentRegisterer.test.ts +++ b/src/Explorer/ComponentRegisterer.test.ts @@ -45,7 +45,8 @@ describe("Component Registerer", () => { }); it("should register settings-tab-v2 component", () => { - expect(ko.components.isRegistered("settings-tab-v2")).toBe(true); + expect(ko.components.isRegistered("database-settings-tab-v2")).toBe(true); + expect(ko.components.isRegistered("collection-settings-tab-v2")).toBe(true); }); it("should register query-tab component", () => { diff --git a/src/Explorer/ComponentRegisterer.ts b/src/Explorer/ComponentRegisterer.ts index 4d49a52bf..c7e7ee087 100644 --- a/src/Explorer/ComponentRegisterer.ts +++ b/src/Explorer/ComponentRegisterer.ts @@ -31,7 +31,7 @@ ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTa ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab()); ko.components.register("trigger-tab", new TabComponents.TriggerTab()); ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab()); -ko.components.register("settings-tab-v2", new TabComponents.SettingsTabV2()); +ko.components.register("collection-settings-tab-v2", new TabComponents.SettingsTabV2()); ko.components.register("query-tab", new TabComponents.QueryTab()); ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab()); ko.components.register("graph-tab", new TabComponents.GraphTab()); @@ -45,6 +45,7 @@ ko.components.register("notebook-viewer-tab", new TabComponents.NotebookViewerTa // Database Tabs ko.components.register("database-settings-tab", new TabComponents.DatabaseSettingsTab()); +ko.components.register("database-settings-tab-v2", new TabComponents.SettingsTabV2()); // Panes ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent()); diff --git a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx index 6e439e5b3..3d3d31c91 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx @@ -2,7 +2,7 @@ import { shallow } from "enzyme"; import React from "react"; import { SettingsComponentProps, SettingsComponent, SettingsComponentState } from "./SettingsComponent"; import * as ViewModels from "../../../Contracts/ViewModels"; -import SettingsTabV2 from "../../Tabs/SettingsTabV2"; +import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2"; import { collection } from "./TestUtils"; import * as DataModels from "../../../Contracts/DataModels"; import ko from "knockout"; @@ -37,16 +37,15 @@ jest.mock("../../../Common/dataAccess/updateOffer", () => ({ describe("SettingsComponent", () => { const baseProps: SettingsComponentProps = { - settingsTab: new SettingsTabV2({ + settingsTab: new CollectionSettingsTabV2({ collection: collection, - tabKind: ViewModels.CollectionTabKind.SettingsV2, + tabKind: ViewModels.CollectionTabKind.CollectionSettingsV2, title: "Scale & Settings", tabPath: "", node: undefined, hashLocation: "settings", isActive: ko.observable(false), onUpdateTabsButtons: undefined, - getPendingNotification: Promise.resolve(undefined), }), }; @@ -139,6 +138,7 @@ describe("SettingsComponent", () => { readSettings: undefined, onSettingsClick: undefined, loadOffer: undefined, + getPendingThroughputSplitNotification: undefined, } as ViewModels.Database; newCollection.getDatabase = () => newDatabase; newCollection.offer = ko.observable(undefined); diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 63e271918..354fe5929 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -11,7 +11,7 @@ import Explorer from "../../Explorer"; import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; -import SettingsTab from "../../Tabs/SettingsTabV2"; +import { SettingsTabV2 } from "../../Tabs/SettingsTabV2"; import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils"; import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent"; import { @@ -58,7 +58,7 @@ interface ButtonV2 { } export interface SettingsComponentProps { - settingsTab: SettingsTab; + settingsTab: SettingsTabV2; } export interface SettingsComponentState { @@ -116,7 +116,10 @@ export class SettingsComponent extends React.Component { - const autoscaleMaxThroughput = this.collection?.offer()?.autoscaleMaxThroughput; + const autoscaleMaxThroughput = this.offer?.autoscaleMaxThroughput; if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) { this.setState({ @@ -295,7 +309,7 @@ export class SettingsComponent extends React.Component { - return this.collection?.offer()?.offerReplacePending; + return this.offer?.offerReplacePending; }; public onSaveClick = async (): Promise => { @@ -309,174 +323,10 @@ export class SettingsComponent extends React.Component { @@ -693,6 +544,17 @@ export class SettingsComponent extends React.Component { + const offerThroughput = this.offer?.manualThroughput; + + if (!this.isCollectionSettingsTab) { + this.setState({ + throughput: offerThroughput, + throughputBaseline: offerThroughput, + }); + + return; + } + const defaultTtl = this.collection.defaultTtl(); let timeToLive: TtlType = this.state.timeToLive; @@ -725,7 +587,6 @@ export class SettingsComponent extends React.Component => { + if (this.state.isScaleSaveable) { + const updateOfferParams: DataModels.UpdateOfferParams = { + databaseId: this.database.id(), + currentOffer: this.database.offer(), + autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined, + manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput, + }; + if (this.hasProvisioningTypeChanged()) { + if (this.state.isAutoPilotSelected) { + updateOfferParams.migrateToAutoPilot = true; + } else { + updateOfferParams.migrateToManual = true; + } + } + const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams); + this.database.offer(updatedOffer); + this.offer = updatedOffer; + this.setState({ isScaleSaveable: false, isScaleDiscardable: false }); + if (this.state.isAutoPilotSelected) { + this.setState({ + autoPilotThroughput: updatedOffer.autoscaleMaxThroughput, + autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput, + }); + } else { + this.setState({ + throughput: updatedOffer.manualThroughput, + throughputBaseline: updatedOffer.manualThroughput, + }); + } + } + + this.container.isRefreshingExplorer(false); + this.setBaseline(); + this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected }); + traceSuccess( + Action.SettingsV2Updated, + { + databaseAccountName: this.container.databaseAccount()?.name, + databaseName: this.database.id(), + defaultExperience: this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.settingsTab.tabTitle(), + }, + startKey + ); + }; + + private saveCollectionSettings = async (startKey: number): Promise => { + const newCollection: DataModels.Collection = { ...this.collection.rawDataModel }; + + if (this.state.isSubSettingsSaveable || this.state.isIndexingPolicyDirty || this.state.isConflictResolutionDirty) { + let defaultTtl: number; + switch (this.state.timeToLive) { + case TtlType.On: + defaultTtl = Number(this.state.timeToLiveSeconds); + break; + case TtlType.OnNoDefault: + defaultTtl = -1; + break; + case TtlType.Off: + default: + defaultTtl = undefined; + break; + } + + const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty; + newCollection.defaultTtl = defaultTtl; + + newCollection.indexingPolicy = this.state.indexingPolicyContent; + + newCollection.changeFeedPolicy = + this.changeFeedPolicyVisible && this.state.changeFeedPolicy === ChangeFeedPolicyState.On + ? { + retentionDuration: Constants.BackendDefaults.maxChangeFeedRetentionDuration, + } + : undefined; + + newCollection.analyticalStorageTtl = this.getAnalyticalStorageTtl(); + + newCollection.geospatialConfig = { + type: this.state.geospatialConfigType, + }; + + const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy(); + if (conflictResolutionChanges) { + newCollection.conflictResolutionPolicy = conflictResolutionChanges; + } + + const updatedCollection: DataModels.Collection = await updateCollection( + this.collection.databaseId, + this.collection.id(), + newCollection + ); + this.collection.rawDataModel = updatedCollection; + this.collection.defaultTtl(updatedCollection.defaultTtl); + this.collection.analyticalStorageTtl(updatedCollection.analyticalStorageTtl); + this.collection.id(updatedCollection.id); + this.collection.indexingPolicy(updatedCollection.indexingPolicy); + this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy); + this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy); + this.collection.geospatialConfig(updatedCollection.geospatialConfig); + + if (wasIndexingPolicyModified) { + await this.refreshIndexTransformationProgress(); + } + + this.setState({ + isSubSettingsSaveable: false, + isSubSettingsDiscardable: false, + isIndexingPolicyDirty: false, + isConflictResolutionDirty: false, + }); + } + + if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) { + try { + const newMongoIndexes = this.getMongoIndexesToSave(); + const newMongoCollection: MongoDBCollectionResource = { + ...this.mongoDBCollectionResource, + indexes: newMongoIndexes, + }; + + this.mongoDBCollectionResource = await updateMongoDBCollectionThroughRP( + this.collection.databaseId, + this.collection.id(), + newMongoCollection + ); + + await this.refreshIndexTransformationProgress(); + this.setState({ + isMongoIndexingPolicySaveable: false, + indexesToDrop: [], + indexesToAdd: [], + currentMongoIndexes: [...this.mongoDBCollectionResource.indexes], + }); + traceSuccess( + Action.MongoIndexUpdated, + { + databaseAccountName: this.container.databaseAccount()?.name, + databaseName: this.collection?.databaseId, + collectionName: this.collection?.id(), + defaultExperience: this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.settingsTab.tabTitle(), + }, + startKey + ); + } catch (error) { + traceFailure( + Action.MongoIndexUpdated, + { + databaseAccountName: this.container.databaseAccount()?.name, + databaseName: this.collection?.databaseId, + collectionName: this.collection?.id(), + defaultExperience: this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.settingsTab.tabTitle(), + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + startKey + ); + throw error; + } + } + + if (this.state.isScaleSaveable) { + const updateOfferParams: DataModels.UpdateOfferParams = { + databaseId: this.collection.databaseId, + collectionId: this.collection.id(), + currentOffer: this.collection.offer(), + autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined, + manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput, + }; + if (this.hasProvisioningTypeChanged()) { + if (this.state.isAutoPilotSelected) { + updateOfferParams.migrateToAutoPilot = true; + } else { + updateOfferParams.migrateToManual = true; + } + } + const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams); + this.collection.offer(updatedOffer); + this.offer = updatedOffer; + this.setState({ isScaleSaveable: false, isScaleDiscardable: false }); + if (this.state.isAutoPilotSelected) { + this.setState({ + autoPilotThroughput: updatedOffer.autoscaleMaxThroughput, + autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput, + }); + } else { + this.setState({ + throughput: updatedOffer.manualThroughput, + throughputBaseline: updatedOffer.manualThroughput, + }); + } + } + this.container.isRefreshingExplorer(false); + this.setBaseline(); + this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected }); + traceSuccess( + Action.SettingsV2Updated, + { + databaseAccountName: this.container.databaseAccount()?.name, + databaseName: this.collection?.databaseId, + collectionName: this.collection?.id(), + defaultExperience: this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.settingsTab.tabTitle(), + }, + startKey + ); + }; + public render(): JSX.Element { const scaleComponentProps: ScaleComponentProps = { collection: this.collection, + database: this.database, container: this.container, isFixedContainer: this.isFixedContainer, onThroughputChange: this.onThroughputChange, @@ -830,6 +907,16 @@ export class SettingsComponent extends React.Component +
+ +
+ + ); + } + const subSettingsComponentProps: SubSettingsComponentProps = { collection: this.collection, container: this.container, @@ -899,7 +986,7 @@ export class SettingsComponent extends React.Component, diff --git a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx index 003b665f8..b110723d3 100644 --- a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx @@ -375,7 +375,7 @@ export const getThroughputApplyShortDelayMessage = ( A request to increase the throughput is currently in progress. This operation will take some time to complete.
- Database: {databaseName}, Container: {collectionName}{" "} + {collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `} {getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
); @@ -392,7 +392,7 @@ export const getThroughputApplyLongDelayMessage = ( 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}, Container: {collectionName}{" "} + {collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `} {getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)} ); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx index 54b6cc5e4..de31b2d5f 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx @@ -18,6 +18,7 @@ describe("ScaleComponent", () => { const baseProps: ScaleComponentProps = { collection: collection, + database: undefined, container: container, isFixedContainer: false, onThroughputChange: () => { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx index 8819024cb..a45ee4230 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx @@ -21,6 +21,7 @@ import { configContext, Platform } from "../../../../ConfigContext"; export interface ScaleComponentProps { collection: ViewModels.Collection; + database: ViewModels.Database; container: Explorer; isFixedContainer: boolean; onThroughputChange: (newThroughput: number) => void; @@ -39,9 +40,16 @@ export interface ScaleComponentProps { export class ScaleComponent extends React.Component { private isEmulator: boolean; + private offer: DataModels.Offer; + private databaseId: string; + private collectionId: string; + constructor(props: ScaleComponentProps) { super(props); this.isEmulator = configContext.platform === Platform.Emulator; + this.offer = this.props.database?.offer() || this.props.collection?.offer(); + this.databaseId = this.props.database?.id() || this.props.collection.databaseId; + this.collectionId = this.props.collection?.id(); } public isAutoScaleEnabled = (): boolean => { @@ -87,9 +95,7 @@ export class ScaleComponent extends React.Component { return SharedConstants.CollectionCreation.DefaultCollectionRUs400; } - return ( - this.props.collection.offer()?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400 - ); + return this.offer?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400; }; public getThroughputTitle = (): string => { @@ -115,15 +121,14 @@ export class ScaleComponent extends React.Component { return this.getLongDelayMessage(); } - const offer = this.props.collection?.offer(); - if (offer?.offerReplacePending) { - const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput; + if (this.offer?.offerReplacePending) { + const throughput = this.offer.manualThroughput || this.offer.autoscaleMaxThroughput; return getThroughputApplyShortDelayMessage( this.props.isAutoPilotSelected, throughput, throughputUnit, - this.props.collection.databaseId, - this.props.collection.id() + this.databaseId, + this.collectionId ); } @@ -135,7 +140,7 @@ export class ScaleComponent extends React.Component { this.canThroughputExceedMaximumValue() && this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million; - if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) { + if (throughputExceedsBackendLimits && !this.props.isFixedContainer) { return updateThroughputBeyondLimitWarningMessage; } @@ -154,8 +159,8 @@ export class ScaleComponent extends React.Component { this.props.wasAutopilotOriginallySet, throughput, throughputUnit, - this.props.collection.databaseId, - this.props.collection.id(), + this.databaseId, + this.collectionId, targetThroughput ); } @@ -165,15 +170,15 @@ export class ScaleComponent extends React.Component { private getThroughputInputComponent = (): JSX.Element => ( { onScaleSaveableChange={this.props.onScaleSaveableChange} onScaleDiscardableChange={this.props.onScaleDiscardableChange} getThroughputWarningMessage={this.getThroughputWarningMessage} - usageSizeInKB={this.props.collection.usageSizeInKB()} + usageSizeInKB={this.props.collection?.usageSizeInKB()} /> ); @@ -230,7 +235,7 @@ export class ScaleComponent extends React.Component { {!this.isAutoScaleEnabled() && ( {this.getThroughputInputComponent()} - {this.getStorageCapacityTitle()} + {!this.props.database && this.getStorageCapacityTitle()} )} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx index 02316edcf..8515ae0ad 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx @@ -40,6 +40,7 @@ import { userContext } from "../../../../../UserContext"; import { SubscriptionType } from "../../../../../Contracts/SubscriptionType"; import { usageInGB, calculateEstimateNumber } from "../../../../../Utils/PricingUtils"; import { Features } from "../../../../../Common/Constants"; +import { minAutoPilotThroughput } from "../../../../../Utils/AutoPilotUtils"; import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor"; import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants"; @@ -541,6 +542,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< step={AutoPilotUtils.autoPilotIncrementStep} value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()} onChange={this.onAutoPilotThroughputChange} + min={minAutoPilotThroughput} /> {!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()} {this.minRUperGBSurvey()} @@ -579,6 +581,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< : this.props.throughput?.toString() } onChange={this.onThroughputChange} + min={this.props.minimum} /> {this.state.exceedFreeTierThroughput && ( 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: - test - , Container: - test - + Database: test, Container: test , Current autoscale throughput: 100 - 1000 RU/s, Target autoscale throughput: 600 - 6000 RU/s diff --git a/src/Explorer/Controls/Settings/SettingsUtils.test.tsx b/src/Explorer/Controls/Settings/SettingsUtils.test.tsx index cd1adbfee..2c040e769 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.test.tsx @@ -46,6 +46,7 @@ describe("SettingsUtils", () => { readSettings: undefined, onSettingsClick: undefined, loadOffer: undefined, + getPendingThroughputSplitNotification: undefined, } as ViewModels.Database; }; newCollection.offer(undefined); diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap index 25cb13989..474866fb9 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap @@ -256,11 +256,7 @@ exports[`SettingsUtils functions render 1`] = ` > A request to increase the throughput is currently in progress. This operation will take some time to complete.
- Database: - sampleDb - , Container: - sampleCollection - + Database: sampleDb, Container: sampleCollection , Current manual throughput: 1000 RU/s 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: - sampleDb - , Container: - sampleCollection - + Database: sampleDb, Container: sampleCollection , Current manual throughput: 1000 RU/s, Target manual throughput: 2000
; - private notification: DataModels.Notification; - private offerRead: ko.Observable; - private currentCollection: ViewModels.Collection; - private options: ViewModels.SettingsTabV2Options; - constructor(options: ViewModels.SettingsTabV2Options) { + constructor(options: ViewModels.TabOptions) { super(options); - this.options = options; - this.tabId = "SettingsV2-" + this.tabId; const props: SettingsComponentProps = { settingsTab: this, }; this.settingsComponentAdapter = new SettingsComponentAdapter(props); - this.currentCollection = this.collection as ViewModels.Collection; + } +} + +export class CollectionSettingsTabV2 extends SettingsTabV2 { + private notificationRead: ko.Observable; + private notification: DataModels.Notification; + private offerRead: ko.Observable; + + constructor(options: ViewModels.TabOptions) { + super(options); + + this.tabId = "SettingsV2-" + this.tabId; this.notificationRead = ko.observable(false); this.offerRead = ko.observable(false); this.settingsComponentAdapter.parameters = ko.computed(() => { @@ -45,49 +48,95 @@ export default class SettingsTabV2 extends TabsBase { public async onActivate(): Promise { try { this.isExecuting(true); - await this.currentCollection.loadOffer(); - // passed in options and set by parent as "Settings" by default - this.tabTitle(this.currentCollection.offer() ? "Settings" : "Scale & Settings"); - this.options.getPendingNotification.then( - (data: DataModels.Notification) => { - this.notification = data; - this.notificationRead(true); + const collection: ViewModels.Collection = this.collection as ViewModels.Collection; + await collection.loadOffer(); + // passed in options and set by parent as "Settings" by default + this.tabTitle(collection.offer() ? "Settings" : "Scale & Settings"); + + const data: DataModels.Notification = await collection.getPendingThroughputSplitNotification(); + this.notification = data; + this.notificationRead(true); + } catch (error) { + const errorMessage = getErrorMessage(error); + this.notification = undefined; + this.notificationRead(true); + traceFailure( + Action.Tab, + { + databaseAccountName: this.collection.container.databaseAccount().name, + databaseName: this.collection.databaseId, + collectionName: this.collection.id(), + defaultExperience: this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle, + error: errorMessage, + errorStack: getErrorStack(error), }, - (error) => { - const errorMessage = getErrorMessage(error); - this.notification = undefined; - this.notificationRead(true); - traceFailure( - Action.Tab, - { - databaseAccountName: this.options.collection.container.databaseAccount().name, - databaseName: this.options.collection.databaseId, - collectionName: this.options.collection.id(), - defaultExperience: this.options.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle, - error: errorMessage, - errorStack: getErrorStack(error), - }, - this.options.onLoadStartKey - ); - logConsoleError( - `Error while fetching container settings for container ${this.options.collection.id()}: ${errorMessage}` - ); - throw error; - } + this.onLoadStartKey ); + logConsoleError(`Error while fetching container settings for container ${this.collection.id()}: ${errorMessage}`); + throw error; } finally { this.offerRead(true); this.isExecuting(false); } super.onActivate(); - this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.SettingsV2); - } - - public getSettingsTabContainer(): Explorer { - return this.getContainer(); + this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2); + } +} + +export class DatabaseSettingsTabV2 extends SettingsTabV2 { + private notificationRead: ko.Observable; + private notification: DataModels.Notification; + + constructor(options: ViewModels.TabOptions) { + super(options); + this.tabId = "DatabaseSettingsV2-" + this.tabId; + this.notificationRead = ko.observable(false); + this.settingsComponentAdapter.parameters = ko.computed(() => { + if (this.notificationRead()) { + this.pendingNotification(this.notification); + this.notification = undefined; + this.notificationRead(false); + return true; + } + return false; + }); + } + + public async onActivate(): Promise { + try { + this.isExecuting(true); + + const data: DataModels.Notification = await this.database.getPendingThroughputSplitNotification(); + this.notification = data; + this.notificationRead(true); + } catch (error) { + const errorMessage = getErrorMessage(error); + this.notification = undefined; + this.notificationRead(true); + traceFailure( + Action.Tab, + { + databaseAccountName: this.database?.container.databaseAccount().name, + databaseName: this.database.id(), + defaultExperience: this.database?.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle, + error: errorMessage, + errorStack: getErrorStack(error), + }, + this.onLoadStartKey + ); + logConsoleError(`Error while fetching database settings for database ${this.database.id()}: ${errorMessage}`); + throw error; + } finally { + this.isExecuting(false); + } + + super.onActivate(); + this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettingsV2); } } diff --git a/src/Explorer/Tabs/SparkMasterTab.ts b/src/Explorer/Tabs/SparkMasterTab.ts index ca62b5344..2af66f8fd 100644 --- a/src/Explorer/Tabs/SparkMasterTab.ts +++ b/src/Explorer/Tabs/SparkMasterTab.ts @@ -29,7 +29,7 @@ export default class SparkMasterTab extends TabsBase { this.sparkMasterSrc = ko.observable(sparkMasterEndpoint && sparkMasterEndpoint.endpoint); } - protected getContainer() { + public getContainer() { return this._container; } } diff --git a/src/Explorer/Tabs/TabsBase.ts b/src/Explorer/Tabs/TabsBase.ts index 802e4ed51..4147daba4 100644 --- a/src/Explorer/Tabs/TabsBase.ts +++ b/src/Explorer/Tabs/TabsBase.ts @@ -177,7 +177,7 @@ export default class TabsBase extends WaitsForTemplateViewModel { return Q(); } - protected getContainer(): Explorer { + public getContainer(): Explorer { return (this.collection && this.collection.container) || (this.database && this.database.container); } diff --git a/src/Explorer/Tabs/TabsManager.html b/src/Explorer/Tabs/TabsManager.html index 7372fd61e..e94fb390b 100644 --- a/src/Explorer/Tabs/TabsManager.html +++ b/src/Explorer/Tabs/TabsManager.html @@ -143,7 +143,11 @@ - + + + + + diff --git a/src/Explorer/Tabs/TerminalTab.tsx b/src/Explorer/Tabs/TerminalTab.tsx index 6ebcd72cb..7f648959e 100644 --- a/src/Explorer/Tabs/TerminalTab.tsx +++ b/src/Explorer/Tabs/TerminalTab.tsx @@ -56,7 +56,7 @@ export default class TerminalTab extends TabsBase { }); } - protected getContainer(): Explorer { + public getContainer(): Explorer { return this.container; } diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 96108a3b3..a1c4f992f 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -26,7 +26,7 @@ import MongoQueryTab from "../Tabs/MongoQueryTab"; import MongoShellTab from "../Tabs/MongoShellTab"; import QueryTab from "../Tabs/QueryTab"; import QueryTablesTab from "../Tabs/QueryTablesTab"; -import SettingsTabV2 from "../Tabs/SettingsTabV2"; +import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2"; import ConflictId from "./ConflictId"; import DocumentId from "./DocumentId"; import StoredProcedure from "./StoredProcedure"; @@ -544,10 +544,12 @@ export default class Collection implements ViewModels.Collection { }); const tabTitle = !this.offer() ? "Settings" : "Scale & Settings"; - const pendingNotificationsPromise: Promise = this._getPendingThroughputSplitNotification(); - const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.SettingsV2, (tab) => { - return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id(); - }); + const matchingTabs = this.container.tabsManager.getTabs( + ViewModels.CollectionTabKind.CollectionSettingsV2, + (tab) => { + return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id(); + } + ); const traceStartData = { databaseAccountName: this.container.databaseAccount().name, @@ -569,26 +571,20 @@ export default class Collection implements ViewModels.Collection { onUpdateTabsButtons: this.container.onUpdateTabsButtons, }; - let settingsTabV2 = matchingTabs && (matchingTabs[0] as SettingsTabV2); - this.launchSettingsTabV2(settingsTabV2, traceStartData, settingsTabOptions, pendingNotificationsPromise); + let settingsTabV2 = matchingTabs && (matchingTabs[0] as CollectionSettingsTabV2); + this.launchSettingsTabV2(settingsTabV2, traceStartData, settingsTabOptions); }; private launchSettingsTabV2 = ( - settingsTabV2: SettingsTabV2, + settingsTabV2: CollectionSettingsTabV2, traceStartData: any, - settingsTabOptions: ViewModels.TabOptions, - getPendingNotification: Promise + settingsTabOptions: ViewModels.TabOptions ): void => { - const settingsTabV2Options: ViewModels.SettingsTabV2Options = { - ...settingsTabOptions, - getPendingNotification: getPendingNotification, - }; - if (!settingsTabV2) { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, traceStartData); - settingsTabV2Options.onLoadStartKey = startKey; - settingsTabV2Options.tabKind = ViewModels.CollectionTabKind.SettingsV2; - settingsTabV2 = new SettingsTabV2(settingsTabV2Options); + settingsTabOptions.onLoadStartKey = startKey; + settingsTabOptions.tabKind = ViewModels.CollectionTabKind.CollectionSettingsV2; + settingsTabV2 = new CollectionSettingsTabV2(settingsTabOptions); this.container.tabsManager.activateNewTab(settingsTabV2); } else { this.container.tabsManager.activateTab(settingsTabV2); @@ -1040,6 +1036,41 @@ export default class Collection implements ViewModels.Collection { }); }; + public async getPendingThroughputSplitNotification(): Promise { + if (!this.container) { + return undefined; + } + + try { + const notifications: DataModels.Notification[] = await fetchPortalNotifications(); + if (!notifications || notifications.length === 0) { + return undefined; + } + + return _.find(notifications, (notification: DataModels.Notification) => { + const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress"); + return ( + notification.kind === "message" && + notification.collectionName === this.id() && + notification.description && + throughputUpdateRegExp.test(notification.description) + ); + }); + } catch (error) { + Logger.logError( + JSON.stringify({ + error: getErrorMessage(error), + accountName: this.container && this.container.databaseAccount(), + databaseName: this.databaseId, + collectionName: this.id(), + }), + "Settings tree node" + ); + + return undefined; + } + } + private async _uploadFilesCors(files: FileList): Promise { const data = await Promise.all(Array.from(files).map((file) => this._uploadFile(file))); @@ -1100,37 +1131,6 @@ export default class Collection implements ViewModels.Collection { } } - private async _getPendingThroughputSplitNotification(): Promise { - if (!this.container) { - return undefined; - } - - const throughputUpdateRegExp = new RegExp("Throughput update (.*) in progress"); - try { - const notifications = await fetchPortalNotifications(); - if (!notifications) { - return undefined; - } - - return notifications.find( - ({ kind, collectionName, description = "" }) => - kind === "message" && collectionName === this.id() && throughputUpdateRegExp.test(description) - ); - } catch (error) { - Logger.logError( - JSON.stringify({ - error: getErrorMessage(error), - accountName: this.container && this.container.databaseAccount(), - databaseName: this.databaseId, - collectionName: this.id(), - }), - "Settings tree node" - ); - } - - return undefined; - } - private _logUploadDetailsInConsole(uploadDetails: UploadDetails): void { const uploadDetailsRecords: UploadDetailsRecord[] = uploadDetails.data; const numFiles: number = uploadDetailsRecords.length; diff --git a/src/Explorer/Tree/Database.ts b/src/Explorer/Tree/Database.ts index 271cb3ab8..431dc89fe 100644 --- a/src/Explorer/Tree/Database.ts +++ b/src/Explorer/Tree/Database.ts @@ -1,11 +1,11 @@ import * as _ from "underscore"; import * as ko from "knockout"; -import Q from "q"; import * as ViewModels from "../../Contracts/ViewModels"; import * as Constants from "../../Common/Constants"; import * as DataModels from "../../Contracts/DataModels"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import DatabaseSettingsTab from "../Tabs/DatabaseSettingsTab"; +import { DatabaseSettingsTabV2 } from "../Tabs/SettingsTabV2"; import Collection from "./Collection"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; @@ -16,7 +16,6 @@ import { readCollections } from "../../Common/dataAccess/readCollections"; import { JunoClient, IJunoResponse } from "../../Juno/JunoClient"; import { userContext } from "../../UserContext"; import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer"; -import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { fetchPortalNotifications } from "../../Common/PortalNotifications"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; @@ -59,12 +58,17 @@ export default class Database implements ViewModels.Database { dataExplorerArea: Constants.Areas.ResourceTree, }); - const pendingNotificationsPromise: Q.Promise = this._getPendingThroughputSplitNotification(); - const matchingTabs = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.DatabaseSettings, - (tab) => tab.node?.id() === this.id() + const pendingNotificationsPromise: Promise = this.getPendingThroughputSplitNotification(); + const useDatabaseSettingsTabV1: boolean = this.container.isFeatureEnabled( + Constants.Features.enableDatabaseSettingsTabV1 ); - let settingsTab: DatabaseSettingsTab = matchingTabs && (matchingTabs[0] as DatabaseSettingsTab); + const tabKind: ViewModels.CollectionTabKind = useDatabaseSettingsTabV1 + ? ViewModels.CollectionTabKind.DatabaseSettings + : ViewModels.CollectionTabKind.DatabaseSettingsV2; + const matchingTabs = this.container.tabsManager.getTabs(tabKind, (tab) => tab.node?.id() === this.id()); + let settingsTab: DatabaseSettingsTab | DatabaseSettingsTabV2 = useDatabaseSettingsTabV1 + ? (matchingTabs?.[0] as DatabaseSettingsTab) + : (matchingTabs?.[0] as DatabaseSettingsTabV2); if (!settingsTab) { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { databaseAccountName: this.container.databaseAccount().name, @@ -75,9 +79,11 @@ export default class Database implements ViewModels.Database { }); pendingNotificationsPromise.then( (data: any) => { - const pendingNotification: DataModels.Notification = data && data[0]; - settingsTab = new DatabaseSettingsTab({ - tabKind: ViewModels.CollectionTabKind.DatabaseSettings, + const pendingNotification: DataModels.Notification = data?.[0]; + const tabOptions: ViewModels.TabOptions = { + tabKind: useDatabaseSettingsTabV1 + ? ViewModels.CollectionTabKind.DatabaseSettings + : ViewModels.CollectionTabKind.DatabaseSettingsV2, title: "Scale", tabPath: "", node: this, @@ -87,8 +93,10 @@ export default class Database implements ViewModels.Database { isActive: ko.observable(false), onLoadStartKey: startKey, onUpdateTabsButtons: this.container.onUpdateTabsButtons, - }); - + }; + settingsTab = useDatabaseSettingsTabV1 + ? new DatabaseSettingsTab(tabOptions) + : new DatabaseSettingsTabV2(tabOptions); settingsTab.pendingNotification(pendingNotification); this.container.tabsManager.activateNewTab(settingsTab); }, @@ -221,47 +229,40 @@ export default class Database implements ViewModels.Database { } } - private _getPendingThroughputSplitNotification(): Q.Promise { + public async getPendingThroughputSplitNotification(): Promise { if (!this.container) { - return Q.resolve(undefined); + return undefined; } - const deferred: Q.Deferred = Q.defer(); - fetchPortalNotifications().then( - (notifications) => { - if (!notifications || notifications.length === 0) { - deferred.resolve(undefined); - return; - } - - const pendingNotification = _.find(notifications, (notification: DataModels.Notification) => { - const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress"); - return ( - notification.kind === "message" && - !notification.collectionName && - notification.databaseName === this.id() && - notification.description && - throughputUpdateRegExp.test(notification.description) - ); - }); - - deferred.resolve(pendingNotification); - }, - (error: any) => { - Logger.logError( - JSON.stringify({ - error: getErrorMessage(error), - accountName: this.container && this.container.databaseAccount(), - databaseName: this.id(), - collectionName: this.id(), - }), - "Settings tree node" - ); - deferred.resolve(undefined); + try { + const notifications: DataModels.Notification[] = await fetchPortalNotifications(); + if (!notifications || notifications.length === 0) { + return undefined; } - ); - return deferred.promise; + return _.find(notifications, (notification: DataModels.Notification) => { + const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress"); + return ( + notification.kind === "message" && + !notification.collectionName && + notification.databaseName === this.id() && + notification.description && + throughputUpdateRegExp.test(notification.description) + ); + }); + } catch (error) { + Logger.logError( + JSON.stringify({ + error: getErrorMessage(error), + accountName: this.container && this.container.databaseAccount(), + databaseName: this.id(), + collectionName: this.id(), + }), + "Settings tree node" + ); + + return undefined; + } } private getDeltaCollections(