diff --git a/less/documentDB.less b/less/documentDB.less index 191b32271..e944f2a74 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -3023,3 +3023,7 @@ settings-pane { .infoBoxContent a { color: @AccentMediumHigh } + +.collapsibleSection :hover{ + cursor: pointer; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7db1edf76..9c1974874 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,7 +76,6 @@ "version": "7.9.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", - "dev": true, "requires": { "@babel/code-frame": "^7.8.3", "@babel/generator": "^7.9.0", @@ -99,8 +98,7 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" } } }, @@ -7203,11 +7201,10 @@ } }, "create-react-class": { - "version": "15.6.3", - "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz", - "integrity": "sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==", + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.7.0.tgz", + "integrity": "sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==", "requires": { - "fbjs": "^0.8.9", "loose-envify": "^1.3.1", "object-assign": "^4.1.1" } @@ -8504,24 +8501,6 @@ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", "dev": true }, - "encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "requires": { - "iconv-lite": "^0.6.2" - }, - "dependencies": { - "iconv-lite": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", - "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - } - } - }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -9667,27 +9646,6 @@ "bser": "2.1.1" } }, - "fbjs": { - "version": "0.8.17", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", - "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", - "requires": { - "core-js": "^1.0.0", - "isomorphic-fetch": "^2.1.1", - "loose-envify": "^1.0.0", - "object-assign": "^4.1.0", - "promise": "^7.1.1", - "setimmediate": "^1.0.5", - "ua-parser-js": "^0.7.18" - }, - "dependencies": { - "core-js": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", - "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" - } - } - }, "fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -11888,26 +11846,6 @@ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, - "isomorphic-fetch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", - "requires": { - "node-fetch": "^1.0.1", - "whatwg-fetch": ">=0.10.0" - }, - "dependencies": { - "node-fetch": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", - "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" - } - } - } - }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -13406,36 +13344,6 @@ "micromatch": "^3.1.10", "pretty-format": "^24.9.0", "realpath-native": "^1.1.0" - }, - "dependencies": { - "@babel/core": { - "version": "7.11.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.6.tgz", - "integrity": "sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==", - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.11.6", - "@babel/helper-module-transforms": "^7.11.0", - "@babel/helpers": "^7.10.4", - "@babel/parser": "^7.11.5", - "@babel/template": "^7.10.4", - "@babel/traverse": "^7.11.5", - "@babel/types": "^7.11.5", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.1", - "json5": "^2.1.2", - "lodash": "^4.17.19", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } } }, "jest-dev-server": { @@ -16619,6 +16527,8 @@ "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "optional": true, "requires": { "asap": "~2.0.3" } @@ -18246,7 +18156,8 @@ "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true }, "setprototypeof": { "version": "1.1.1", @@ -20006,11 +19917,6 @@ "free-style": "3.1.0" } }, - "ua-parser-js": { - "version": "0.7.22", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.22.tgz", - "integrity": "sha512-YUxzMjJ5T71w6a8WWVcMGM6YWOTX27rCoIQgLXiWaxqXSx9D7DNjiGWn1aJIRSQ5qr0xuhra77bSIh6voR/46Q==" - }, "uglify-js": { "version": "3.4.10", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 2ac739e8a..2e810394a 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -133,6 +133,7 @@ export class Features { // flight names returned from the portal are always lowercase export class Flights { public static readonly SettingsV2 = "settingsv2"; + public static readonly MongoIndexEditor = "mongoindexeditor"; } export class AfecFeatures { diff --git a/src/Common/PortalNotifications.ts b/src/Common/PortalNotifications.ts index b4094dfb1..e97b19891 100644 --- a/src/Common/PortalNotifications.ts +++ b/src/Common/PortalNotifications.ts @@ -1,41 +1,41 @@ -import * as DataModels from "../Contracts/DataModels"; -import * as ViewModels from "../Contracts/ViewModels"; -import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; -import { userContext } from "../UserContext"; -import { configContext, Platform } from "../ConfigContext"; - -const notificationsPath = () => { - switch (configContext.platform) { - case Platform.Hosted: - return "/api/guest/notifications"; - case Platform.Portal: - return "/api/notifications"; - default: - throw new Error(`Unknown platform: ${configContext.platform}`); - } -}; - -export const fetchPortalNotifications = async (): Promise => { - if (configContext.platform === Platform.Emulator) { - return []; - } - - const databaseAccount = userContext.databaseAccount; - const subscriptionId = userContext.subscriptionId; - const resourceGroup = userContext.resourceGroup; - const url = `${configContext.BACKEND_ENDPOINT}${notificationsPath()}?accountName=${ - databaseAccount.name - }&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`; - const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader(); - const headers = { [authorizationHeader.header]: authorizationHeader.token }; - - const response = await window.fetch(url, { - headers - }); - - if (!response.ok) { - throw new Error(await response.text()); - } - - return (await response.json()) as DataModels.Notification[]; -}; +import * as DataModels from "../Contracts/DataModels"; +import * as ViewModels from "../Contracts/ViewModels"; +import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; +import { userContext } from "../UserContext"; +import { configContext, Platform } from "../ConfigContext"; + +const notificationsPath = () => { + switch (configContext.platform) { + case Platform.Hosted: + return "/api/guest/notifications"; + case Platform.Portal: + return "/api/notifications"; + default: + throw new Error(`Unknown platform: ${configContext.platform}`); + } +}; + +export const fetchPortalNotifications = async (): Promise => { + if (configContext.platform === Platform.Emulator) { + return []; + } + + const databaseAccount = userContext.databaseAccount; + const subscriptionId = userContext.subscriptionId; + const resourceGroup = userContext.resourceGroup; + const url = `${configContext.BACKEND_ENDPOINT}${notificationsPath()}?accountName=${ + databaseAccount.name + }&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`; + const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader(); + const headers = { [authorizationHeader.header]: authorizationHeader.token }; + + const response = await window.fetch(url, { + headers + }); + + if (!response.ok) { + throw new Error(await response.text()); + } + + return (await response.json()) as DataModels.Notification[]; +}; diff --git a/src/Common/dataAccess/readMongoDBCollection.tsx b/src/Common/dataAccess/readMongoDBCollection.tsx new file mode 100644 index 000000000..9d7123bda --- /dev/null +++ b/src/Common/dataAccess/readMongoDBCollection.tsx @@ -0,0 +1,58 @@ +import { userContext } from "../../UserContext"; +import { getMongoDBCollection } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; +import { MongoDBCollectionResource } from "../../Utils/arm/generatedClients/2020-04-01/types"; +import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; +import * as Constants from "../Constants"; +import { client } from "../CosmosClient"; +import { handleError } from "../ErrorHandlingUtils"; +import { AuthType } from "../../AuthType"; + +export async function readMongoDBCollectionThroughRP( + databaseId: string, + collectionId: string +): Promise { + if (window.authType !== AuthType.AAD) { + return undefined; + } + let collection: MongoDBCollectionResource; + const subscriptionId = userContext.subscriptionId; + const resourceGroup = userContext.resourceGroup; + const accountName = userContext.databaseAccount.name; + + const clearMessage = logConsoleProgress(`Reading container ${collectionId}`); + try { + const response = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId); + collection = response.properties.resource; + } catch (error) { + handleError(error, `Error while reading container ${collectionId}`, "ReadMongoDBCollection"); + throw error; + } + clearMessage(); + return collection; +} + +export async function getMongoDBCollectionIndexTransformationProgress( + databaseId: string, + collectionId: string +): Promise { + if (window.authType !== AuthType.AAD) { + return undefined; + } + let indexTransformationPercentage: number; + const clearMessage = logConsoleProgress(`Reading container ${collectionId}`); + try { + const response = await client() + .database(databaseId) + .container(collectionId) + .read({ populateQuotaInfo: true }); + + indexTransformationPercentage = parseInt( + response.headers[Constants.HttpHeaders.collectionIndexTransformationProgress] as string + ); + } catch (error) { + handleError(error, `Error while reading container ${collectionId}`, "ReadMongoDBCollection"); + throw error; + } + clearMessage(); + return indexTransformationPercentage; +} diff --git a/src/Common/dataAccess/updateCollection.ts b/src/Common/dataAccess/updateCollection.ts index 17d537870..9f1004e07 100644 --- a/src/Common/dataAccess/updateCollection.ts +++ b/src/Common/dataAccess/updateCollection.ts @@ -3,7 +3,10 @@ import { Collection } from "../../Contracts/DataModels"; import { ContainerDefinition } from "@azure/cosmos"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { + CreateUpdateOptions, ExtendedResourceProperties, + MongoDBCollectionCreateUpdateParameters, + MongoDBCollectionResource, SqlContainerCreateUpdateParameters, SqlContainerResource } from "../../Utils/arm/generatedClients/2020-04-01/types"; @@ -50,6 +53,7 @@ export async function updateCollection( .database(databaseId) .container(collectionId) .replace(newCollection as ContainerDefinition, options); + collection = sdkResponse.resource as Collection; } @@ -77,15 +81,6 @@ async function updateCollectionWithARM( switch (defaultExperience) { case DefaultAccountExperienceType.DocumentDB: return updateSqlContainer(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection); - case DefaultAccountExperienceType.MongoDB: - return updateMongoDBCollection( - databaseId, - collectionId, - subscriptionId, - resourceGroup, - accountName, - newCollection - ); case DefaultAccountExperienceType.Cassandra: return updateCassandraTable(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection); case DefaultAccountExperienceType.Graph: @@ -122,26 +117,35 @@ async function updateSqlContainer( throw new Error(`Sql container to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`); } -async function updateMongoDBCollection( +export async function updateMongoDBCollectionThroughRP( databaseId: string, collectionId: string, - subscriptionId: string, - resourceGroup: string, - accountName: string, - newCollection: Collection -): Promise { + newCollection: MongoDBCollectionResource, + updateOptions?: CreateUpdateOptions +): Promise { + const subscriptionId = userContext.subscriptionId; + const resourceGroup = userContext.resourceGroup; + const accountName = userContext.databaseAccount.name; + const getResponse = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId); if (getResponse && getResponse.properties && getResponse.properties.resource) { - getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties; + const updateParams: MongoDBCollectionCreateUpdateParameters = { + properties: { + resource: newCollection, + options: updateOptions + } + }; + const updateResponse = await createUpdateMongoDBCollection( subscriptionId, resourceGroup, accountName, databaseId, collectionId, - getResponse as SqlContainerCreateUpdateParameters + updateParams ); - return updateResponse && (updateResponse.properties.resource as Collection); + + return updateResponse && (updateResponse.properties.resource as MongoDBCollectionResource); } throw new Error( diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 6245e1f5f..ea27df1f3 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -289,6 +289,10 @@ export interface DocumentsTabOptions extends TabOptions { resourceTokenPartitionKey?: string; } +export interface SettingsTabV2Options extends TabOptions { + getPendingNotification: Q.Promise; +} + export interface ConflictsTabOptions extends TabOptions { partitionKey: DataModels.PartitionKey; conflictIds: ko.ObservableArray; diff --git a/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.test.tsx b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.test.tsx new file mode 100644 index 000000000..5b9eb0fe5 --- /dev/null +++ b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.test.tsx @@ -0,0 +1,14 @@ +import { shallow } from "enzyme"; +import React from "react"; +import { CollapsibleSectionComponent, CollapsibleSectionProps } from "./CollapsibleSectionComponent"; + +describe("CollapsibleSectionComponent", () => { + it("renders", () => { + const props: CollapsibleSectionProps = { + title: "Sample title" + }; + + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx new file mode 100644 index 000000000..b7dea8c36 --- /dev/null +++ b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx @@ -0,0 +1,36 @@ +import { Icon, Label, Stack } from "office-ui-fabric-react"; +import * as React from "react"; +import { accordionIconStyles, accordionStackTokens } from "../Settings/SettingsRenderUtils"; + +export interface CollapsibleSectionProps { + title: string; +} + +export interface CollapsibleSectionState { + isExpanded: boolean; +} + +export class CollapsibleSectionComponent extends React.Component { + constructor(props: CollapsibleSectionProps) { + super(props); + this.state = { + isExpanded: true + }; + } + + private toggleCollapsed = (): void => { + this.setState({ isExpanded: !this.state.isExpanded }); + }; + + public render(): JSX.Element { + return ( + <> + + + + + {this.state.isExpanded && this.props.children} + + ); + } +} diff --git a/src/Explorer/Controls/CollapsiblePanel/__snapshots__/CollapsibleSectionComponent.test.tsx.snap b/src/Explorer/Controls/CollapsiblePanel/__snapshots__/CollapsibleSectionComponent.test.tsx.snap new file mode 100644 index 000000000..f9df3c40e --- /dev/null +++ b/src/Explorer/Controls/CollapsiblePanel/__snapshots__/CollapsibleSectionComponent.test.tsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CollapsibleSectionComponent renders 1`] = ` + + + + + Sample title + + + +`; diff --git a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx index 01bd89d84..9d862864c 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx @@ -8,7 +8,10 @@ import * as DataModels from "../../../Contracts/DataModels"; import ko from "knockout"; import { TtlType, isDirty } from "./SettingsUtils"; import Explorer from "../../Explorer"; -import { updateCollection } from "../../../Common/dataAccess/updateCollection"; +jest.mock("../../../Common/dataAccess/readMongoDBCollection", () => ({ + getMongoDBCollectionIndexTransformationProgress: jest.fn().mockReturnValue(undefined) +})); +import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection"; jest.mock("../../../Common/dataAccess/updateCollection", () => ({ updateCollection: jest.fn().mockReturnValue({ id: undefined, @@ -18,9 +21,17 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({ changeFeedPolicy: undefined, analyticalStorageTtl: undefined, geospatialConfig: undefined - } as DataModels.Collection) + } as DataModels.Collection), + updateMongoDBCollectionThroughRP: jest.fn().mockReturnValue({ + id: undefined, + shardKey: undefined, + indexes: [], + analyticalStorageTtl: undefined + } as MongoDBCollectionResource) })); import { updateOffer } from "../../../Common/dataAccess/updateOffer"; +import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types"; +import Q from "q"; jest.mock("../../../Common/dataAccess/updateOffer", () => ({ updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer) })); @@ -35,7 +46,10 @@ describe("SettingsComponent", () => { node: undefined, hashLocation: "settings", isActive: ko.observable(false), - onUpdateTabsButtons: undefined + onUpdateTabsButtons: undefined, + getPendingNotification: Q.Promise(() => { + return; + }) }) }; @@ -188,13 +202,17 @@ describe("SettingsComponent", () => { expect(settingsComponentInstance.isOfferReplacePending()).toEqual(true); }); - it("save calls updateCollection and updateOffer", async () => { + it("save calls updateCollection, updateMongoDBCollectionThroughRP and updateOffer", async () => { const wrapper = shallow(); - wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true }); + wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true, isMongoIndexingPolicySaveable: true }); wrapper.update(); const settingsComponentInstance = wrapper.instance() as SettingsComponent; + settingsComponentInstance.mongoDBCollectionResource = { + id: "id" + }; await settingsComponentInstance.onSaveClick(); expect(updateCollection).toBeCalled(); + expect(updateMongoDBCollectionThroughRP).toBeCalled(); expect(updateOffer).toBeCalled(); }); diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 37fd840fd..ab796d88e 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -11,13 +11,17 @@ import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryCons import { RequestOptions } from "@azure/cosmos/dist-esm"; import Explorer from "../../Explorer"; import { updateOffer } from "../../../Common/dataAccess/updateOffer"; -import { updateCollection } from "../../../Common/dataAccess/updateCollection"; +import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { userContext } from "../../../UserContext"; import { updateOfferThroughputBeyondLimit } from "../../../Common/dataAccess/updateOfferThroughputBeyondLimit"; import SettingsTab from "../../Tabs/SettingsTabV2"; import { throughputUnit } from "./SettingsRenderUtils"; import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent"; +import { + MongoIndexingPolicyComponent, + MongoIndexingPolicyComponentProps +} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent"; import { getMaxRUs, hasDatabaseSharedThroughput, @@ -27,8 +31,11 @@ import { SettingsV2TabTypes, getTabTitle, isDirty, + AddMongoIndexProps, + MongoIndexTypes, parseConflictResolutionMode, - parseConflictResolutionProcedure + parseConflictResolutionProcedure, + getMongoNotification } from "./SettingsUtils"; import { ConflictResolutionComponent, @@ -38,6 +45,11 @@ import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubCo import { Pivot, PivotItem, IPivotProps, IPivotItemProps } from "office-ui-fabric-react"; import "./SettingsComponent.less"; import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent"; +import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types"; +import { + getMongoDBCollectionIndexTransformationProgress, + readMongoDBCollectionThroughRP +} from "../../../Common/dataAccess/readMongoDBCollection"; interface SettingsV2TabInfo { tab: SettingsV2TabTypes; @@ -84,6 +96,13 @@ export interface SettingsComponentState { shouldDiscardIndexingPolicy: boolean; isIndexingPolicyDirty: boolean; + isMongoIndexingPolicySaveable: boolean; + isMongoIndexingPolicyDiscardable: boolean; + currentMongoIndexes: MongoIndex[]; + indexesToDrop: number[]; + indexesToAdd: AddMongoIndexProps[]; + indexTransformationProgress: number; + conflictResolutionPolicyMode: DataModels.ConflictResolutionMode; conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode; conflictResolutionPolicyPath: string; @@ -98,17 +117,17 @@ export interface SettingsComponentState { export class SettingsComponent extends React.Component { private static readonly sixMonthsInSeconds = 15768000; + private saveSettingsButton: ButtonV2; + private discardSettingsChangesButton: ButtonV2; - public saveSettingsButton: ButtonV2; - public discardSettingsChangesButton: ButtonV2; - - public isAnalyticalStorageEnabled: boolean; + private isAnalyticalStorageEnabled: boolean; private collection: ViewModels.Collection; private container: Explorer; private changeFeedPolicyVisible: boolean; private isFixedContainer: boolean; private autoPilotTiersList: ViewModels.DropdownOption[]; private shouldShowIndexingPolicyEditor: boolean; + public mongoDBCollectionResource: MongoDBCollectionResource; constructor(props: SettingsComponentProps) { super(props); @@ -158,6 +177,13 @@ export class SettingsComponent extends React.Component => { + if ( + this.container.isMongoIndexEditorEnabled() && + this.container.isPreferredApiMongoDB() && + this.container.databaseAccount() + ) { + await this.refreshIndexTransformationProgress(); + + this.mongoDBCollectionResource = await readMongoDBCollectionThroughRP( + this.collection.databaseId, + this.collection.id() + ); + + if (this.mongoDBCollectionResource) { + this.setState({ + currentMongoIndexes: [...this.mongoDBCollectionResource.indexes] + }); + } + } + }; + + public refreshIndexTransformationProgress = async (): Promise => { + const currentProgress = await getMongoDBCollectionIndexTransformationProgress( + this.collection.databaseId, + this.collection.id() + ); + this.setState({ indexTransformationProgress: currentProgress }); + }; + public isSaveSettingsButtonEnabled = (): boolean => { if (this.isOfferReplacePending()) { return false; @@ -208,7 +264,8 @@ export class SettingsComponent extends React.Component { + let finalIndexes: MongoIndex[] = []; + this.state.currentMongoIndexes?.map((mongoIndex: MongoIndex, index: number) => { + if (!this.state.indexesToDrop.includes(index)) { + finalIndexes.push(mongoIndex); + } + }); + finalIndexes = finalIndexes.concat(this.state.indexesToAdd.map((m: AddMongoIndexProps) => m.mongoIndex)); + return finalIndexes; + }; + private onScaleSaveableChange = (isScaleSaveable: boolean): void => this.setState({ isScaleSaveable: isScaleSaveable }); @@ -529,6 +623,36 @@ export class SettingsComponent extends React.Component this.setState({ indexesToDrop: [...this.state.indexesToDrop, index] }); + + private onRevertIndexDrop = (index: number): void => { + const indexesToDrop = [...this.state.indexesToDrop]; + indexesToDrop.splice(index, 1); + this.setState({ indexesToDrop }); + }; + + private onRevertIndexAdd = (index: number): void => { + const indexesToAdd = [...this.state.indexesToAdd]; + indexesToAdd.splice(index, 1); + this.setState({ indexesToAdd }); + }; + + private onIndexAddOrChange = (index: number, description: string, type: MongoIndexTypes): void => { + const newIndexesToAdd = [...this.state.indexesToAdd]; + const notification = getMongoNotification(description, type); + const newMongoIndexWithType: AddMongoIndexProps = { + mongoIndex: { key: { keys: [description] } } as MongoIndex, + type: type, + notification: notification + }; + if (index === newIndexesToAdd.length) { + newIndexesToAdd.push(newMongoIndexWithType); + } else { + newIndexesToAdd[index] = newMongoIndexWithType; + } + this.setState({ indexesToAdd: newIndexesToAdd }); + }; + private onConflictResolutionPolicyModeChange = (newMode: DataModels.ConflictResolutionMode): void => this.setState({ conflictResolutionPolicyMode: newMode }); @@ -567,6 +691,12 @@ export class SettingsComponent extends React.Component this.setState({ isIndexingPolicyDirty: isIndexingPolicyDirty }); + private onMongoIndexingPolicySaveableChange = (isMongoIndexingPolicySaveable: boolean): void => + this.setState({ isMongoIndexingPolicySaveable }); + + private onMongoIndexingPolicyDiscardableChange = (isMongoIndexingPolicyDiscardable: boolean): void => + this.setState({ isMongoIndexingPolicyDiscardable }); + public getAnalyticalStorageTtl = (): number => { if (this.isAnalyticalStorageEnabled) { if (this.state.analyticalStorageTtlSelection === TtlType.On) { @@ -789,6 +919,20 @@ export class SettingsComponent extends React.Component @@ -822,6 +966,15 @@ export class SettingsComponent extends React.Component }); + } else if ( + this.container.isMongoIndexEditorEnabled() && + this.container.isPreferredApiMongoDB() && + this.container.isEnableMongoCapabilityPresent() + ) { + tabs.push({ + tab: SettingsV2TabTypes.IndexingPolicyTab, + content: + }); } if (this.hasConflictResolution()) { diff --git a/src/Explorer/Controls/Settings/SettingsComponentAdapter.tsx b/src/Explorer/Controls/Settings/SettingsComponentAdapter.tsx index 674e25d3c..6fd2cff07 100644 --- a/src/Explorer/Controls/Settings/SettingsComponentAdapter.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponentAdapter.tsx @@ -4,17 +4,11 @@ import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; import { SettingsComponent, SettingsComponentProps } from "./SettingsComponent"; export class SettingsComponentAdapter implements ReactAdapter { - public parameters: ko.Observable; + public parameters: ko.Computed; - constructor(private props: SettingsComponentProps) { - this.parameters = ko.observable(Date.now()); - } + constructor(private props: SettingsComponentProps) {} public renderComponent(): JSX.Element { - return ; - } - - public triggerRender(): void { - window.requestAnimationFrame(() => this.parameters(Date.now())); + return this.parameters() ? : <>; } } diff --git a/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx b/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx index c081acfe0..361cf8c31 100644 --- a/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx @@ -15,7 +15,11 @@ import { getToolTipContainer, conflictResolutionCustomToolTip, changeFeedPolicyToolTip, - conflictResolutionLwwTooltip + conflictResolutionLwwTooltip, + mongoIndexingPolicyDisclaimer, + mongoIndexingPolicyAADError, + mongoIndexTransformationRefreshingMessage, + renderMongoIndexTransformationRefreshMessage } from "./SettingsRenderUtils"; class SettingsRenderUtilsTestComponent extends React.Component { @@ -45,6 +49,16 @@ class SettingsRenderUtilsTestComponent extends React.Component { {conflictResolutionLwwTooltip} {conflictResolutionCustomToolTip} {changeFeedPolicyToolTip} + + {mongoIndexingPolicyDisclaimer} + {mongoIndexingPolicyAADError} + {mongoIndexTransformationRefreshingMessage} + {renderMongoIndexTransformationRefreshMessage(0, () => { + return; + })} + {renderMongoIndexTransformationRefreshMessage(90, () => { + return; + })} ); } diff --git a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx index bb814bf09..dc2dfb64b 100644 --- a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx @@ -21,13 +21,24 @@ import { Link, Text, IMessageBarStyles, - ITextStyles + ITextStyles, + IDetailsRowStyles, + IStackStyles, + IIconStyles, + IDetailsListStyles, + IDropdownStyles, + ISeparatorStyles, + MessageBar, + MessageBarType, + Stack, + Spinner, + SpinnerSize } from "office-ui-fabric-react"; import { isDirtyTypes, isDirty } from "./SettingsUtils"; const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } }; -export const spendAckCheckBoxStyle: ICheckboxStyles = { +export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = { label: { margin: 0, padding: "2 0 2 0" @@ -45,6 +56,20 @@ export const titleAndInputStackProps: Partial = { tokens: { childrenGap: 5 } }; +export const mongoWarningStackProps: Partial = { + tokens: { childrenGap: 5 } +}; + +export const mongoErrorMessageStyles: Partial = { root: { marginLeft: 10 } }; + +export const createAndAddMongoIndexStackProps: Partial = { + tokens: { childrenGap: 5 } +}; + +export const addMongoIndexStackProps: Partial = { + tokens: { childrenGap: 10 } +}; + export const checkBoxAndInputStackProps: Partial = { tokens: { childrenGap: 10 } }; @@ -53,6 +78,54 @@ export const toolTipLabelStackTokens: IStackTokens = { childrenGap: 6 }; +export const accordionStackTokens: IStackTokens = { + childrenGap: 10 +}; + +export const addMongoIndexSubElementsTokens: IStackTokens = { + childrenGap: 20 +}; + +export const accordionIconStyles: IIconStyles = { root: { paddingTop: 7 } }; + +export const mediumWidthStackStyles: IStackStyles = { root: { width: 600 } }; + +export const shortWidthTextFieldStyles: Partial = { root: { paddingLeft: 10, width: 210 } }; + +export const shortWidthDropDownStyles: Partial = { dropdown: { paddingleft: 10, width: 202 } }; + +export const transparentDetailsRowStyles: Partial = { + root: { + selectors: { + ":hover": { + background: "transparent" + } + } + } +}; + +export const customDetailsListStyles: Partial = { + root: { + selectors: { + ".ms-FocusZone": { + paddingTop: 0 + } + } + } +}; + +export const separatorStyles: Partial = { + root: [ + { + selectors: { + "::before": { + background: StyleConstants.BaseMedium + } + } + } + ] +}; + export const messageBarStyles: Partial = { root: { marginTop: "5px" } }; export const throughputUnit = "RU/s"; @@ -313,6 +386,56 @@ export const changeFeedPolicyToolTip: JSX.Element = ( ); +export const mongoIndexingPolicyDisclaimer: JSX.Element = ( + + For queries that filter on multiple properties, create multiple single field indexes instead of a compound index. + + {` Compound indexes `} + + are only used for sorting query results. If you need to add a compound index, you can create one using the Mongo + shell. + +); + +export const mongoIndexingPolicyAADError: JSX.Element = ( + + + To use the indexing policy editor, please login to the + + {"azure portal."} + + + +); + +export const mongoIndexTransformationRefreshingMessage: JSX.Element = ( + + Refreshing index transformation progress + + +); + +export const renderMongoIndexTransformationRefreshMessage = ( + progress: number, + performRefresh: () => void +): JSX.Element => { + if (progress === 0) { + return ( + + {"You can make more indexing changes once the current index transformation is complete. "} + {"Refresh to check if it has completed."} + + ); + } else { + return ( + + {`You can make more indexing changes once the current index transformation has completed. It is ${progress}% complete. `} + {"Refresh to check the progress."} + + ); + } +}; + export const getTextFieldStyles = (current: isDirtyTypes, baseline: isDirtyTypes): Partial => ({ fieldGroup: { height: 25, diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/AddMongoIndexComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/AddMongoIndexComponent.test.tsx new file mode 100644 index 000000000..749825bad --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/AddMongoIndexComponent.test.tsx @@ -0,0 +1,24 @@ +import { shallow } from "enzyme"; +import React from "react"; +import { MongoIndexTypes, MongoNotificationType } from "../../SettingsUtils"; +import { AddMongoIndexComponent, AddMongoIndexComponentProps } from "./AddMongoIndexComponent"; + +describe("AddMongoIndexComponent", () => { + it("renders", () => { + const props: AddMongoIndexComponentProps = { + position: 1, + description: "sample_key", + type: MongoIndexTypes.Single, + notification: { type: MongoNotificationType.Error, message: "sample error" }, + onIndexAddOrChange: () => { + return; + }, + onDiscard: () => { + return; + } + }; + + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/AddMongoIndexComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/AddMongoIndexComponent.tsx new file mode 100644 index 000000000..23d094477 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/AddMongoIndexComponent.tsx @@ -0,0 +1,103 @@ +import * as React from "react"; +import { + MessageBar, + MessageBarType, + Stack, + IconButton, + TextField, + Dropdown, + IDropdownOption, + ITextField +} from "office-ui-fabric-react"; +import { + addMongoIndexSubElementsTokens, + mongoErrorMessageStyles, + mongoWarningStackProps, + shortWidthDropDownStyles, + shortWidthTextFieldStyles +} from "../../SettingsRenderUtils"; +import { + getMongoIndexTypeText, + MongoIndexTypes, + MongoNotificationMessage, + MongoNotificationType, + MongoWildcardPlaceHolder +} from "../../SettingsUtils"; + +export interface AddMongoIndexComponentProps { + position: number; + description: string; + type: MongoIndexTypes; + notification: MongoNotificationMessage; + onIndexAddOrChange: (description: string, type: MongoIndexTypes) => void; + onDiscard: () => void; + disabled?: boolean; +} + +export class AddMongoIndexComponent extends React.Component { + private descriptionTextField: ITextField; + private indexTypes: IDropdownOption[] = [MongoIndexTypes.Single, MongoIndexTypes.Wildcard].map( + (value: MongoIndexTypes) => ({ + text: getMongoIndexTypeText(value), + key: value + }) + ); + + private onDescriptionChange = ( + event: React.FormEvent, + newValue?: string + ): void => { + this.props.onIndexAddOrChange(newValue, this.props.type); + }; + + private onTypeChange = (event: React.FormEvent, option?: IDropdownOption): void => { + const newType = MongoIndexTypes[option.key as keyof typeof MongoIndexTypes]; + this.props.onIndexAddOrChange(this.props.description, newType); + }; + + private setRef = (textField: ITextField) => (this.descriptionTextField = textField); + + public focus = (): void => { + this.descriptionTextField.focus(); + }; + + public render(): JSX.Element { + return ( + + + + + + + this.props.onDiscard()} + /> + + {this.props.notification?.type === MongoNotificationType.Error && ( + + {this.props.notification.message} + + )} + + ); + } +} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.test.tsx new file mode 100644 index 000000000..246bc8186 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.test.tsx @@ -0,0 +1,122 @@ +import { shallow } from "enzyme"; +import React from "react"; +import { MongoIndexTypes, MongoNotificationMessage, MongoNotificationType } from "../../SettingsUtils"; +import { MongoIndexingPolicyComponent, MongoIndexingPolicyComponentProps } from "./MongoIndexingPolicyComponent"; + +describe("MongoIndexingPolicyComponent", () => { + const baseProps: MongoIndexingPolicyComponentProps = { + mongoIndexes: [], + onIndexDrop: () => { + return; + }, + indexesToDrop: [], + onRevertIndexDrop: () => { + return; + }, + indexesToAdd: [], + onRevertIndexAdd: () => { + return; + }, + onIndexAddOrChange: () => { + return; + }, + indexTransformationProgress: undefined, + refreshIndexTransformationProgress: () => + new Promise(() => { + return; + }), + onMongoIndexingPolicySaveableChange: () => { + return; + }, + onMongoIndexingPolicyDiscardableChange: () => { + return; + } + }; + + it("renders", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it("isIndexingTransforming", () => { + const wrapper = shallow(); + const mongoIndexingPolicyComponent = wrapper.instance() as MongoIndexingPolicyComponent; + expect(mongoIndexingPolicyComponent.isIndexingTransforming()).toEqual(false); + wrapper.setProps({ indexTransformationProgress: 50 }); + expect(mongoIndexingPolicyComponent.isIndexingTransforming()).toEqual(true); + wrapper.setProps({ indexTransformationProgress: 100 }); + expect(mongoIndexingPolicyComponent.isIndexingTransforming()).toEqual(false); + }); + + describe("AddMongoIndexProps test", () => { + const wrapper = shallow(); + const mongoIndexingPolicyComponent = wrapper.instance() as MongoIndexingPolicyComponent; + + it("defaults", () => { + expect(mongoIndexingPolicyComponent.isMongoIndexingPolicySaveable()).toEqual(false); + expect(mongoIndexingPolicyComponent.isMongoIndexingPolicyDiscardable()).toEqual(false); + expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toEqual(undefined); + }); + + const sampleWarning = "sampleWarning"; + const sampleError = "sampleError"; + + const cases = [ + [ + { type: MongoNotificationType.Warning, message: sampleWarning } as MongoNotificationMessage, + false, + false, + true, + sampleWarning + ], + [ + { type: MongoNotificationType.Error, message: sampleError } as MongoNotificationMessage, + false, + false, + true, + undefined + ], + [ + { type: MongoNotificationType.Error, message: sampleError } as MongoNotificationMessage, + true, + false, + true, + undefined + ], + [undefined, false, true, true, undefined], + [undefined, true, true, true, undefined] + ]; + + test.each(cases)( + "", + ( + notification: MongoNotificationMessage, + indexToDropIsPresent: boolean, + isMongoIndexingPolicySaveable: boolean, + isMongoIndexingPolicyDiscardable: boolean, + mongoWarningNotificationMessage: string + ) => { + const addMongoIndexProps = { + mongoIndex: { key: { keys: ["sampleKey"] } }, + type: MongoIndexTypes.Single, + notification: notification + }; + + let indexesToDrop: number[] = []; + if (indexToDropIsPresent) { + indexesToDrop = [0]; + } + wrapper.setProps({ indexesToAdd: [addMongoIndexProps], indexesToDrop: indexesToDrop }); + wrapper.update(); + + expect(mongoIndexingPolicyComponent.isMongoIndexingPolicySaveable()).toEqual(isMongoIndexingPolicySaveable); + expect(mongoIndexingPolicyComponent.isMongoIndexingPolicyDiscardable()).toEqual( + isMongoIndexingPolicyDiscardable + ); + expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toEqual( + mongoWarningNotificationMessage + ); + } + ); + }); +}); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx new file mode 100644 index 000000000..cc9c25794 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx @@ -0,0 +1,369 @@ +import * as React from "react"; +import { + DetailsList, + DetailsListLayoutMode, + Stack, + IconButton, + Text, + SelectionMode, + IDetailsRowProps, + DetailsRow, + IColumn, + MessageBar, + MessageBarType, + Spinner, + SpinnerSize, + Separator +} from "office-ui-fabric-react"; +import { + addMongoIndexStackProps, + customDetailsListStyles, + mongoIndexingPolicyDisclaimer, + mediumWidthStackStyles, + subComponentStackProps, + transparentDetailsRowStyles, + createAndAddMongoIndexStackProps, + separatorStyles, + mongoIndexingPolicyAADError, + mongoIndexTransformationRefreshingMessage, + renderMongoIndexTransformationRefreshMessage +} from "../../SettingsRenderUtils"; +import { MongoIndex } from "../../../../../Utils/arm/generatedClients/2020-04-01/types"; +import { + MongoIndexTypes, + AddMongoIndexProps, + MongoIndexIdField, + MongoNotificationType, + getMongoIndexType, + getMongoIndexTypeText +} from "../../SettingsUtils"; +import { AddMongoIndexComponent } from "./AddMongoIndexComponent"; +import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent"; +import { handleError } from "../../../../../Common/ErrorHandlingUtils"; +import { AuthType } from "../../../../../AuthType"; + +export interface MongoIndexingPolicyComponentProps { + mongoIndexes: MongoIndex[]; + onIndexDrop: (index: number) => void; + indexesToDrop: number[]; + onRevertIndexDrop: (index: number) => void; + indexesToAdd: AddMongoIndexProps[]; + onRevertIndexAdd: (index: number) => void; + onIndexAddOrChange: (index: number, description: string, type: MongoIndexTypes) => void; + indexTransformationProgress: number; + refreshIndexTransformationProgress: () => Promise; + onMongoIndexingPolicySaveableChange: (isMongoIndexingPolicySaveable: boolean) => void; + onMongoIndexingPolicyDiscardableChange: (isMongoIndexingPolicyDiscardable: boolean) => void; +} + +interface MongoIndexingPolicyComponentState { + isRefreshingIndexTransformationProgress: boolean; +} + +interface MongoIndexDisplayProps { + definition: JSX.Element; + type: JSX.Element; + actionButton: JSX.Element; +} + +export class MongoIndexingPolicyComponent extends React.Component< + MongoIndexingPolicyComponentProps, + MongoIndexingPolicyComponentState +> { + private shouldCheckComponentIsDirty = true; + private addMongoIndexComponentRefs: React.RefObject[] = []; + private initialIndexesColumns: IColumn[] = [ + { key: "definition", name: "Definition", fieldName: "definition", minWidth: 100, maxWidth: 200, isResizable: true }, + { key: "type", name: "Type", fieldName: "type", minWidth: 100, maxWidth: 200, isResizable: true }, + { + key: "actionButton", + name: "Drop Index", + fieldName: "actionButton", + minWidth: 100, + maxWidth: 200, + isResizable: true + } + ]; + + private indexesToBeDroppedColumns: IColumn[] = [ + { key: "definition", name: "Definition", fieldName: "definition", minWidth: 100, maxWidth: 200, isResizable: true }, + { key: "type", name: "Type", fieldName: "type", minWidth: 100, maxWidth: 200, isResizable: true }, + { + key: "actionButton", + name: "Add index back", + fieldName: "actionButton", + minWidth: 100, + maxWidth: 200, + isResizable: true + } + ]; + + constructor(props: MongoIndexingPolicyComponentProps) { + super(props); + this.state = { + isRefreshingIndexTransformationProgress: false + }; + } + + componentDidUpdate(prevProps: MongoIndexingPolicyComponentProps): void { + if (this.props.indexesToAdd.length > prevProps.indexesToAdd.length) { + this.addMongoIndexComponentRefs[prevProps.indexesToAdd.length]?.current?.focus(); + } + this.onComponentUpdate(); + } + + componentDidMount(): void { + this.onComponentUpdate(); + } + + private onComponentUpdate = (): void => { + if (!this.shouldCheckComponentIsDirty) { + this.shouldCheckComponentIsDirty = true; + return; + } + this.props.onMongoIndexingPolicySaveableChange(this.isMongoIndexingPolicySaveable()); + this.props.onMongoIndexingPolicyDiscardableChange(this.isMongoIndexingPolicyDiscardable()); + this.shouldCheckComponentIsDirty = false; + }; + + public isMongoIndexingPolicySaveable = (): boolean => { + if (this.props.indexesToAdd.length === 0 && this.props.indexesToDrop.length === 0) { + return false; + } + + const addErrorsExist = !!this.props.indexesToAdd.find(addMongoIndexProps => addMongoIndexProps.notification); + + if (addErrorsExist) { + return false; + } + + return true; + }; + + public isMongoIndexingPolicyDiscardable = (): boolean => { + return this.props.indexesToAdd.length > 0 || this.props.indexesToDrop.length > 0; + }; + + public getMongoWarningNotificationMessage = (): string => { + return this.props.indexesToAdd.find( + addMongoIndexProps => addMongoIndexProps.notification?.type === MongoNotificationType.Warning + )?.notification.message; + }; + + private onRenderRow = (props: IDetailsRowProps): JSX.Element => { + return ; + }; + + private getActionButton = (arrayPosition: number, isCurrentIndex: boolean): JSX.Element => { + return isCurrentIndex ? ( + { + this.props.onIndexDrop(arrayPosition); + }} + /> + ) : ( + { + this.props.onRevertIndexDrop(arrayPosition); + }} + /> + ); + }; + + private getMongoIndexDisplayProps = ( + mongoIndex: MongoIndex, + arrayPosition: number, + isCurrentIndex: boolean + ): MongoIndexDisplayProps => { + const keys = mongoIndex?.key?.keys; + const type = getMongoIndexType(keys); + const definition = keys?.join(); + let mongoIndexDisplayProps: MongoIndexDisplayProps; + if (type) { + mongoIndexDisplayProps = { + definition: {definition}, + type: {getMongoIndexTypeText(type)}, + actionButton: definition === MongoIndexIdField ? <> : this.getActionButton(arrayPosition, isCurrentIndex) + }; + } + return mongoIndexDisplayProps; + }; + + private renderIndexesToBeAdded = (): JSX.Element => { + const indexesToAddLength = this.props.indexesToAdd.length; + for (let i = 0; i < indexesToAddLength; i++) { + const existingIndexToAddRef = React.createRef(); + this.addMongoIndexComponentRefs[i] = existingIndexToAddRef; + } + const newIndexToAddRef = React.createRef(); + this.addMongoIndexComponentRefs[indexesToAddLength] = newIndexToAddRef; + + return ( + + {this.props.indexesToAdd.map((mongoIndexWithType, arrayPosition) => { + const keys = mongoIndexWithType.mongoIndex.key.keys; + const type = mongoIndexWithType.type; + const notification = mongoIndexWithType.notification; + return ( + + this.props.onIndexAddOrChange(arrayPosition, description, type) + } + onDiscard={() => { + this.addMongoIndexComponentRefs.splice(arrayPosition, 1); + this.props.onRevertIndexAdd(arrayPosition); + }} + /> + ); + })} + + + this.props.onIndexAddOrChange(indexesToAddLength, description, type) + } + onDiscard={() => { + this.props.onRevertIndexAdd(indexesToAddLength); + }} + /> + + ); + }; + + private renderInitialIndexes = (): JSX.Element => { + const initialIndexes = this.props.mongoIndexes + .map((mongoIndex, arrayPosition) => this.getMongoIndexDisplayProps(mongoIndex, arrayPosition, true)) + .filter((value, arrayPosition) => !!value && !this.props.indexesToDrop.includes(arrayPosition)); + + return ( + + + { + <> + + {this.renderIndexesToBeAdded()} + + } + + + ); + }; + + private renderIndexesToBeDropped = (): JSX.Element => { + const indexesToBeDropped = this.props.indexesToDrop.map((dropIndex, arrayPosition) => + this.getMongoIndexDisplayProps(this.props.mongoIndexes[dropIndex], arrayPosition, false) + ); + + return ( + + + {indexesToBeDropped.length > 0 && ( + + )} + + + ); + }; + + private refreshIndexTransformationProgress = async () => { + this.setState({ isRefreshingIndexTransformationProgress: true }); + try { + await this.props.refreshIndexTransformationProgress(); + } catch (error) { + handleError(error, "Refreshing index transformation progress failed.", "RefreshIndexTransformationProgress"); + } finally { + this.setState({ isRefreshingIndexTransformationProgress: false }); + } + }; + + public isIndexingTransforming = (): boolean => + // index transformation progress can be 0 + this.props.indexTransformationProgress !== undefined && this.props.indexTransformationProgress !== 100; + + private onClickRefreshIndexingTransformationLink = async () => await this.refreshIndexTransformationProgress(); + + private renderIndexTransformationWarning = (): JSX.Element => { + if (this.state.isRefreshingIndexTransformationProgress) { + return mongoIndexTransformationRefreshingMessage; + } else if (this.isIndexingTransforming()) { + return renderMongoIndexTransformationRefreshMessage( + this.props.indexTransformationProgress, + this.onClickRefreshIndexingTransformationLink + ); + } + return undefined; + }; + + private renderWarningMessage = (): JSX.Element => { + let warningMessage: string; + if (this.getMongoWarningNotificationMessage()) { + warningMessage = this.getMongoWarningNotificationMessage(); + } else if (this.isMongoIndexingPolicySaveable()) { + warningMessage = + "You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes."; + } + + return ( + <> + {this.renderIndexTransformationWarning() && ( + {this.renderIndexTransformationWarning()} + )} + + {warningMessage && ( + + {warningMessage} + + )} + + ); + }; + + public render(): JSX.Element { + if (this.props.mongoIndexes) { + return ( + + {this.renderWarningMessage()} + {mongoIndexingPolicyDisclaimer} + {this.renderInitialIndexes()} + + {this.renderIndexesToBeDropped()} + + ); + } else { + return window.authType !== AuthType.AAD ? mongoIndexingPolicyAADError : ; + } + } +} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/__snapshots__/AddMongoIndexComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/__snapshots__/AddMongoIndexComponent.test.tsx.snap new file mode 100644 index 000000000..b5f16f933 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/__snapshots__/AddMongoIndexComponent.test.tsx.snap @@ -0,0 +1,83 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AddMongoIndexComponent renders 1`] = ` + + + + + + + + sample error + + +`; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/__snapshots__/MongoIndexingPolicyComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/__snapshots__/MongoIndexingPolicyComponent.test.tsx.snap new file mode 100644 index 000000000..ab19cbb45 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/__snapshots__/MongoIndexingPolicyComponent.test.tsx.snap @@ -0,0 +1,137 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MongoIndexingPolicyComponent renders 1`] = ` + + + For queries that filter on multiple properties, create multiple single field indexes instead of a compound index. + + Compound indexes + + are only used for sorting query results. If you need to add a compound index, you can create one using the Mongo shell. + + + + + + + + + + + + + + +`; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx index c410bcad0..3dce4dddd 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx @@ -3,7 +3,7 @@ import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils"; import { getTextFieldStyles, getToolTipContainer, - spendAckCheckBoxStyle, + noLeftPaddingCheckBoxStyle, titleAndInputStackProps, checkBoxAndInputStackProps, getChoiceGroupStyles, @@ -278,7 +278,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< {this.props.spendAckVisible && ( { expect(getSanitizedInputValue("999", max)).toEqual(100); expect(getSanitizedInputValue("10", max)).toEqual(10); }); + + it("getMongoIndexType", () => { + expect(getMongoIndexType(["Single"])).toEqual(MongoIndexTypes.Single); + expect(getMongoIndexType(["Wildcard.$**"])).toEqual(MongoIndexTypes.Wildcard); + expect(getMongoIndexType(["Key1", "Key2"])).toEqual(undefined); + }); + + it("getMongoIndexTypeText", () => { + expect(getMongoIndexTypeText(MongoIndexTypes.Single)).toEqual(SingleFieldText); + expect(getMongoIndexTypeText(MongoIndexTypes.Wildcard)).toEqual(WildcardText); + }); + + it("getMongoNotification", () => { + const singleIndexDescription = "sampleKey"; + const wildcardIndexDescription = "sampleKey.$**"; + + let notification = getMongoNotification(singleIndexDescription, undefined); + expect(notification.message).toEqual("Please select a type for each index."); + expect(notification.type).toEqual(MongoNotificationType.Warning); + + notification = getMongoNotification(singleIndexDescription, MongoIndexTypes.Single); + expect(notification).toEqual(undefined); + + notification = getMongoNotification(wildcardIndexDescription, MongoIndexTypes.Wildcard); + expect(notification).toEqual(undefined); + + notification = getMongoNotification("", MongoIndexTypes.Single); + expect(notification.message).toEqual("Please enter a field name."); + expect(notification.type).toEqual(MongoNotificationType.Error); + + notification = getMongoNotification(singleIndexDescription, MongoIndexTypes.Wildcard); + expect(notification.message).toEqual( + "Wildcard path is not present in the field name. Use a pattern like " + MongoWildcardPlaceHolder + ); + expect(notification.type).toEqual(MongoNotificationType.Error); + }); }); diff --git a/src/Explorer/Controls/Settings/SettingsUtils.tsx b/src/Explorer/Controls/Settings/SettingsUtils.tsx index 8dac79c28..556df274f 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.tsx @@ -5,12 +5,17 @@ import * as SharedConstants from "../../../Shared/Constants"; import * as PricingUtils from "../../../Utils/PricingUtils"; import Explorer from "../../Explorer"; +import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types"; const zeroValue = 0; export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy; export const TtlOff = "off"; export const TtlOn = "on"; export const TtlOnNoDefault = "on-nodefault"; +export const MongoIndexIdField = "_id"; +export const MongoWildcardPlaceHolder = "properties.$**"; +export const SingleFieldText = "Single Field"; +export const WildcardText = "Wildcard"; export enum ChangeFeedPolicyState { Off = "Off", @@ -28,6 +33,17 @@ export enum GeospatialConfigType { Geometry = "Geometry" } +export enum MongoIndexTypes { + Single = "Single", + Wildcard = "Wildcard" +} + +export interface AddMongoIndexProps { + mongoIndex: MongoIndex; + type: MongoIndexTypes; + notification: MongoNotificationMessage; +} + export enum SettingsV2TabTypes { ScaleTab, ConflictResolutionTab, @@ -40,6 +56,16 @@ export interface IsComponentDirtyResult { isDiscardable: boolean; } +export enum MongoNotificationType { + Warning = "Warning", + Error = "Error" +} + +export interface MongoNotificationMessage { + type: MongoNotificationType; + message: string; +} + export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection): boolean => { const database: ViewModels.Database = collection.getDatabase(); return database?.isDatabaseShared() && !collection.offer(); @@ -54,7 +80,7 @@ export const getMaxRUs = (collection: ViewModels.Collection, container: Explorer const numPartitionsFromOffer: number = collection?.offer && collection.offer()?.content?.collectionThroughputInfo?.numPhysicalPartitions; - const numPartitionsFromQuotaInfo: number = collection?.quotaInfo().numPartitions; + const numPartitionsFromQuotaInfo: number = collection?.quotaInfo()?.numPartitions; const numPartitions = numPartitionsFromOffer ?? numPartitionsFromQuotaInfo ?? 1; @@ -79,7 +105,7 @@ export const getMinRUs = (collection: ViewModels.Collection, container: Explorer return collectionThroughputInfo.minimumRUForCollection; } - const numPartitions = collectionThroughputInfo?.numPhysicalPartitions ?? collection.quotaInfo().numPartitions; + const numPartitions = collectionThroughputInfo?.numPhysicalPartitions ?? collection.quotaInfo()?.numPartitions; if (!numPartitions || numPartitions === 1) { return SharedConstants.CollectionCreation.DefaultCollectionRUs400; @@ -179,3 +205,48 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => { throw new Error(`Unknown tab ${tab}`); } }; + +export const getMongoNotification = (description: string, type: MongoIndexTypes): MongoNotificationMessage => { + if (description && !type) { + return { + type: MongoNotificationType.Warning, + message: "Please select a type for each index." + }; + } + + if (type && (!description || description.trim().length === 0)) { + return { + type: MongoNotificationType.Error, + message: "Please enter a field name." + }; + } else if (type === MongoIndexTypes.Wildcard && description?.indexOf("$**") === -1) { + return { + type: MongoNotificationType.Error, + message: "Wildcard path is not present in the field name. Use a pattern like " + MongoWildcardPlaceHolder + }; + } + + return undefined; +}; + +export const getMongoIndexType = (keys: string[]): MongoIndexTypes => { + const length = keys?.length; + let type: MongoIndexTypes; + + if (length === 1) { + if (keys[0].indexOf("$**") !== -1) { + type = MongoIndexTypes.Wildcard; + } else { + type = MongoIndexTypes.Single; + } + } + + return type; +}; + +export const getMongoIndexTypeText = (index: MongoIndexTypes): string => { + if (index === MongoIndexTypes.Single) { + return SingleFieldText; + } + return WildcardText; +}; diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index be1d95326..c07de9c02 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -983,6 +983,7 @@ exports[`SettingsComponent renders 1`] = ` "isHostedDataExplorerEnabled": [Function], "isLeftPaneExpanded": [Function], "isLinkInjectionEnabled": [Function], + "isMongoIndexEditorEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], "isNotificationConsoleExpanded": [Function], @@ -2289,6 +2290,7 @@ exports[`SettingsComponent renders 1`] = ` "isHostedDataExplorerEnabled": [Function], "isLeftPaneExpanded": [Function], "isLinkInjectionEnabled": [Function], + "isMongoIndexEditorEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], "isNotificationConsoleExpanded": [Function], @@ -3608,6 +3610,7 @@ exports[`SettingsComponent renders 1`] = ` "isHostedDataExplorerEnabled": [Function], "isLeftPaneExpanded": [Function], "isLinkInjectionEnabled": [Function], + "isMongoIndexEditorEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], "isNotificationConsoleExpanded": [Function], @@ -4914,6 +4917,7 @@ exports[`SettingsComponent renders 1`] = ` "isHostedDataExplorerEnabled": [Function], "isLeftPaneExpanded": [Function], "isLinkInjectionEnabled": [Function], + "isMongoIndexEditorEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], "isNotificationConsoleExpanded": [Function], diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap index 40cb20ece..77b4fc444 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap @@ -310,5 +310,59 @@ exports[`SettingsUtils functions render 1`] = ` > Enable change feed log retention policy to retain last 10 minutes of history for items in the container by default. To support this, the request unit (RU) charge for this container will be multiplied by a factor of two for writes. Reads are unaffected. + + For queries that filter on multiple properties, create multiple single field indexes instead of a compound index. + + Compound indexes + + are only used for sorting query results. If you need to add a compound index, you can create one using the Mongo shell. + + + + To use the indexing policy editor, please login to the + + azure portal. + + + + + + Refreshing index transformation progress + + + + + You can make more indexing changes once the current index transformation is complete. + + Refresh to check if it has completed. + + + + You can make more indexing changes once the current index transformation has completed. It is 90% complete. + + Refresh to check the progress. + + `; diff --git a/src/Explorer/Explorer.ts b/src/Explorer/Explorer.ts index 517bc7551..7e6501afb 100644 --- a/src/Explorer/Explorer.ts +++ b/src/Explorer/Explorer.ts @@ -206,6 +206,7 @@ export default class Explorer { public isCodeOfConductEnabled: ko.Computed; public isLinkInjectionEnabled: ko.Computed; public isSettingsV2Enabled: ko.Observable; + public isMongoIndexEditorEnabled: ko.Observable; public isGitHubPaneEnabled: ko.Observable; public isPublishNotebookPaneEnabled: ko.Observable; public isCopyNotebookPaneEnabled: ko.Observable; @@ -412,8 +413,8 @@ export default class Explorer { this.isLinkInjectionEnabled = ko.computed(() => this.isFeatureEnabled(Constants.Features.enableLinkInjection) ); - //this.isSettingsV2Enabled = ko.computed(() => this.isFeatureEnabled(Constants.Features.enableSettingsV2)); this.isSettingsV2Enabled = ko.observable(false); + this.isMongoIndexEditorEnabled = ko.observable(false); this.isGitHubPaneEnabled = ko.observable(false); this.isPublishNotebookPaneEnabled = ko.observable(false); this.isCopyNotebookPaneEnabled = ko.observable(false); @@ -1948,6 +1949,10 @@ export default class Explorer { if (flights.indexOf(Constants.Flights.SettingsV2) !== -1) { this.isSettingsV2Enabled(true); } + + if (flights.indexOf(Constants.Flights.MongoIndexEditor) !== -1) { + this.isMongoIndexEditorEnabled(true); + } } public findSelectedCollection(): ViewModels.Collection { diff --git a/src/Explorer/Tabs/SettingsTabV2.tsx b/src/Explorer/Tabs/SettingsTabV2.tsx index 594365e6e..149ccdd6d 100644 --- a/src/Explorer/Tabs/SettingsTabV2.tsx +++ b/src/Explorer/Tabs/SettingsTabV2.tsx @@ -1,22 +1,91 @@ import * as ViewModels from "../../Contracts/ViewModels"; +import * as DataModels from "../../Contracts/DataModels"; import TabsBase from "./TabsBase"; import { SettingsComponentAdapter } from "../Controls/Settings/SettingsComponentAdapter"; import { SettingsComponentProps } from "../Controls/Settings/SettingsComponent"; import Explorer from "../Explorer"; +import { traceFailure } from "../../Shared/Telemetry/TelemetryProcessor"; +import ko from "knockout"; +import * as Constants from "../../Common/Constants"; +import { Action } from "../../Shared/Telemetry/TelemetryConstants"; +import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; export default class SettingsTabV2 extends TabsBase { public settingsComponentAdapter: SettingsComponentAdapter; + private notificationRead: ko.Observable; + private notification: DataModels.Notification; + private offerRead: ko.Observable; + private currentCollection: ViewModels.Collection; + private options: ViewModels.SettingsTabV2Options; - constructor(options: ViewModels.TabOptions) { + constructor(options: ViewModels.SettingsTabV2Options) { 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; + this.notificationRead = ko.observable(false); + this.offerRead = ko.observable(false); + this.settingsComponentAdapter.parameters = ko.computed(() => { + if (this.notificationRead() && this.offerRead()) { + this.pendingNotification(this.notification); + this.notification = undefined; + this.offerRead(false); + this.notificationRead(false); + return true; + } + return false; + }); } public onActivate(): Q.Promise { + this.isExecuting(true); + this.currentCollection.loadOffer().then( + () => { + // passed in options and set by parent as "Settings" by default + this.tabTitle("Scale & Settings"); + this.offerRead(true); + this.options.getPendingNotification.then( + (data: DataModels.Notification) => { + this.notification = data; + this.notificationRead(true); + this.isExecuting(false); + }, + error => { + this.notification = undefined; + this.notificationRead(true); + this.isExecuting(false); + 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: error + }, + this.options.onLoadStartKey + ); + logConsoleError( + `Error while fetching container settings for container ${this.options.collection.id()}: ${JSON.stringify( + error + )}` + ); + throw error; + } + ); + }, + () => { + this.offerRead(true); + this.isExecuting(false); + } + ); + return super.onActivate().then(() => { this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.SettingsV2); }); diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 04f99bde1..14192ae2b 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -551,7 +551,11 @@ export default class Collection implements ViewModels.Collection { dataExplorerArea: Constants.Areas.ResourceTree }); - await this.loadOffer(); + const isSettingsV2Enabled = this.container.isSettingsV2Enabled(); + if (!isSettingsV2Enabled) { + 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 => { @@ -578,28 +582,30 @@ export default class Collection implements ViewModels.Collection { onUpdateTabsButtons: this.container.onUpdateTabsButtons }; - const isSettingsV2Enabled = this.container.isSettingsV2Enabled(); - var settingsTab: TabsBase; if (isSettingsV2Enabled) { - settingsTab = matchingTabs && (matchingTabs[0] as SettingsTabV2); + let settingsTabV2 = matchingTabs && (matchingTabs[0] as SettingsTabV2); + this.launchSettingsTabV2(settingsTabV2, traceStartData, settingsTabOptions, pendingNotificationsPromise); } else { - settingsTab = matchingTabs && (matchingTabs[0] as SettingsTab); + let settingsTab = matchingTabs && (matchingTabs[0] as SettingsTab); + this.launchSettingsTabV1(settingsTab, traceStartData, settingsTabOptions, pendingNotificationsPromise); } + }; + private launchSettingsTabV1 = ( + settingsTab: SettingsTab, + traceStartData: any, + settingsTabOptions: ViewModels.TabOptions, + getPendingNotification: Q.Promise + ): void => { if (!settingsTab) { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, traceStartData); settingsTabOptions.onLoadStartKey = startKey; - pendingNotificationsPromise.then( + getPendingNotification.then( (data: any) => { const pendingNotification: DataModels.Notification = data && data[0]; - if (isSettingsV2Enabled) { - settingsTabOptions.tabKind = ViewModels.CollectionTabKind.SettingsV2; - settingsTab = new SettingsTabV2(settingsTabOptions); - } else { - settingsTabOptions.tabKind = ViewModels.CollectionTabKind.Settings; - settingsTab = new SettingsTab(settingsTabOptions); - } + settingsTabOptions.tabKind = ViewModels.CollectionTabKind.Settings; + settingsTab = new SettingsTab(settingsTabOptions); this.container.tabsManager.activateNewTab(settingsTab); settingsTab.pendingNotification(pendingNotification); }, @@ -612,7 +618,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: tabTitle, + tabTitle: settingsTabOptions.title, error: error }, startKey @@ -625,7 +631,7 @@ export default class Collection implements ViewModels.Collection { } ); } else { - pendingNotificationsPromise.then( + getPendingNotification.then( (pendingNotification: DataModels.Notification) => { settingsTab.pendingNotification(pendingNotification); this.container.tabsManager.activateTab(settingsTab); @@ -638,6 +644,28 @@ export default class Collection implements ViewModels.Collection { } }; + private launchSettingsTabV2 = ( + settingsTabV2: SettingsTabV2, + traceStartData: any, + settingsTabOptions: ViewModels.TabOptions, + getPendingNotification: Q.Promise + ): 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); + this.container.tabsManager.activateNewTab(settingsTabV2); + } else { + this.container.tabsManager.activateTab(settingsTabV2); + } + }; + private async loadCollectionQuotaInfo(): Promise { // TODO: Use the collection entity cache to get quota info const quotaInfoWithUniqueKeyPolicy = await readCollectionQuotaInfo(this);