From 28899f63d78e0d5ecd3f7f43892f73d6b1777183 Mon Sep 17 00:00:00 2001 From: Srinath Narayanan Date: Tue, 24 Nov 2020 10:32:18 -0800 Subject: [PATCH 01/21] Fixed bug in fetching 'index transformation progress' header (#330) * bug fix * fixed formatting errors --- src/Common/dataAccess/getIndexTransformationProgress.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/dataAccess/getIndexTransformationProgress.ts b/src/Common/dataAccess/getIndexTransformationProgress.ts index 94dcf9fde..fa0298fbc 100644 --- a/src/Common/dataAccess/getIndexTransformationProgress.ts +++ b/src/Common/dataAccess/getIndexTransformationProgress.ts @@ -14,7 +14,7 @@ export async function getIndexTransformationProgress(databaseId: string, collect const response = await client() .database(databaseId) .container(collectionId) - .read(); + .read({ populateQuotaInfo: true }); indexTransformationPercentage = parseInt( response.headers[Constants.HttpHeaders.collectionIndexTransformationProgress] as string From b784ac0f86cbcb7273b6a3053566d2030ee0c56b Mon Sep 17 00:00:00 2001 From: Chris-MS-896 <64865559+Chris-MS-896@users.noreply.github.com> Date: Mon, 30 Nov 2020 14:33:18 -0600 Subject: [PATCH 02/21] =?UTF-8?q?[967093][Screen=20Readers-=20CosmosDB=20?= =?UTF-8?q?=E2=80=93=20Notification]=20Screen=20reader=20does=20not=20pass?= =?UTF-8?q?=20the=20combo-box=20list=20information=20(#329)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ‘Bug fix: Screen reader does not pass the combo-box list information under notification field.’ * ‘update for comments’ * ‘load path refator’ --- .../NotificationConsole.less | 16 ++++++ .../NotificationConsoleComponent.tsx | 34 +++++------ ...NotificationConsoleComponent.test.tsx.snap | 57 ++++++++----------- 3 files changed, 57 insertions(+), 50 deletions(-) diff --git a/src/Explorer/Menus/NotificationConsole/NotificationConsole.less b/src/Explorer/Menus/NotificationConsole/NotificationConsole.less index 570243fc4..2e9413020 100644 --- a/src/Explorer/Menus/NotificationConsole/NotificationConsole.less +++ b/src/Explorer/Menus/NotificationConsole/NotificationConsole.less @@ -99,7 +99,22 @@ .notificationConsoleControls { padding: @MediumSpace; margin-left:@DefaultSpace; + display: flex; + align-items: center; + .ms-Dropdown-container { + display: flex; + .ms-Dropdown-title { + height: 25px; + line-height: 25px; + } + .ms-Dropdown { + min-width: 110px; + margin-left: 10px; + height: 25px; + line-height: 25px; + } + } #consoleFilterLabel { padding: 4px; } @@ -107,6 +122,7 @@ .consoleSplitter { border-left: 1px solid @BaseMedium; margin: @MediumSpace; + height: 20px; } .clearNotificationsButton { diff --git a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx index de411f2c4..6f5916e89 100644 --- a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx +++ b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx @@ -5,7 +5,7 @@ import * as React from "react"; import { ClientDefaults, KeyCodes } from "../../../Common/Constants"; import AnimateHeight from "react-animate-height"; - +import { Dropdown, IDropdownOption } from "office-ui-fabric-react"; import LoadingIcon from "../../../../images/loading.svg"; import ErrorBlackIcon from "../../../../images/error_black.svg"; import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg"; @@ -53,7 +53,12 @@ export class NotificationConsoleComponent extends React.Component< NotificationConsoleComponentState > { private static readonly transitionDurationMs = 200; - private static readonly FilterOptions = ["All", "In Progress", "Info", "Error"]; + private static readonly FilterOptions = [ + { key: "All", text: "All" }, + { key: "In Progress", text: "In progress" }, + { key: "Info", text: "Info" }, + { key: "Error", text: "Error" } + ]; private headerTimeoutId: number; private prevHeaderStatus: string; private consoleHeaderElement: HTMLElement; @@ -62,7 +67,7 @@ export class NotificationConsoleComponent extends React.Component< super(props); this.state = { headerStatus: "", - selectedFilter: NotificationConsoleComponent.FilterOptions[0], + selectedFilter: NotificationConsoleComponent.FilterOptions[0].key || "", isExpanded: props.isConsoleExpanded }; this.prevHeaderStatus = null; @@ -150,20 +155,15 @@ export class NotificationConsoleComponent extends React.Component< >
- - + aria-labelledby="consoleFilterLabel" + aria-label={this.state.selectedFilter} + /> ): void { - this.setState({ selectedFilter: event.target.value }); + private onFilterSelected(event: React.ChangeEvent, option: IDropdownOption): void { + this.setState({ selectedFilter: String(option.key) }); } private getFilteredConsoleData(): ConsoleData[] { diff --git a/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap b/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap index 51d6908eb..08d3750a3 100644 --- a/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap +++ b/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap @@ -110,43 +110,34 @@ exports[`NotificationConsoleComponent renders the console (expanded) 1`] = `
- - + selectedKey="All" + /> From 04ab1f3918dc37dfbf1a7dc412b3da94654b2376 Mon Sep 17 00:00:00 2001 From: Chris-MS-896 <64865559+Chris-MS-896@users.noreply.github.com> Date: Mon, 30 Nov 2020 15:32:28 -0600 Subject: [PATCH 03/21] '[Visual Requirement-Data Explorer (iframe)] On the Data Explorer page, luminosity contrast ratio of the borderline button is less than 3.:1.' (#331) --- src/Explorer/SplashScreen/SplashScreenComponent.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Explorer/SplashScreen/SplashScreenComponent.less b/src/Explorer/SplashScreen/SplashScreenComponent.less index 38ee7fd0b..d30a52be3 100644 --- a/src/Explorer/SplashScreen/SplashScreenComponent.less +++ b/src/Explorer/SplashScreen/SplashScreenComponent.less @@ -47,7 +47,7 @@ padding: 32px 16px; display: flex; background-color: @BaseLight; - border: 1px solid #E5E5E5; + border: 1px solid #949494; box-sizing: border-box; box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); border-radius: 4px; From fd60c9c15ebbb028fd5a1f67d62ebf5eace07bba Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Mon, 30 Nov 2020 23:06:38 -0800 Subject: [PATCH 04/21] Remove RUPM (#328) Remove all RUPM code --- sampleData/gremlinSampleData.json | 3 +- src/Common/Constants.ts | 6 -- src/Contracts/DataModels.ts | 1 - .../FeaturePanel/FeaturePanelComponent.tsx | 1 - .../FeaturePanelComponent.test.tsx.snap | 6 -- .../Settings/SettingsRenderUtils.test.tsx | 2 +- .../Controls/Settings/SettingsRenderUtils.tsx | 5 +- .../ThroughputInputAutoPilotV3Component.tsx | 3 +- .../SettingsComponent.test.tsx.snap | 16 ----- .../SettingsRenderUtils.test.tsx.snap | 6 +- src/Explorer/Panes/AddCollectionPane.html | 32 --------- src/Explorer/Panes/AddCollectionPane.ts | 55 +-------------- src/Explorer/Panes/AddDatabasePane.ts | 10 +-- .../Panes/CassandraAddCollectionPane.ts | 26 +------ src/Explorer/Tabs/DatabaseSettingsTab.ts | 3 +- src/Shared/Constants.ts | 3 - src/Shared/PriceEstimateCalculator.ts | 14 ++-- src/Utils/PricingUtils.test.ts | 68 +++++++------------ src/Utils/PricingUtils.ts | 23 ++----- 19 files changed, 49 insertions(+), 234 deletions(-) diff --git a/sampleData/gremlinSampleData.json b/sampleData/gremlinSampleData.json index 485db5d1a..82fed5817 100644 --- a/sampleData/gremlinSampleData.json +++ b/sampleData/gremlinSampleData.json @@ -3,7 +3,6 @@ "offerThroughput": 400, "databaseLevelThroughput": false, "collectionId": "Persons", - "rupmEnabled": false, "partitionKey": { "kind": "Hash", "paths": ["/name"] }, "data": [ "g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)", @@ -13,4 +12,4 @@ "g.V('1').addE('knows').to(g.V('2')).outV().addE('knows').to(g.V('3'))", "g.V('3').addE('knows').to(g.V('4'))" ] -} \ No newline at end of file +} diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 2142a530e..89a7fafac 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -108,7 +108,6 @@ export class CapabilityNames { export class Features { public static readonly cosmosdb = "cosmosdb"; public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy"; - public static readonly enableRupm = "enablerupm"; public static readonly executeSproc = "dataexplorerexecutesproc"; public static readonly hostedDataExplorer = "hosteddataexplorerenabled"; public static readonly enableTtl = "enablettl"; @@ -181,11 +180,6 @@ export class CassandraBackend { public static readonly guestSchemaApi: string = "api/guest/cassandra/schema"; } -export class RUPMStates { - public static on: string = "on"; - public static off: string = "off"; -} - export class Queries { public static CustomPageOption: string = "custom"; public static UnlimitedPageOption: string = "unlimited"; diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 5ec15c572..8df58ba18 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -248,7 +248,6 @@ export interface CreateDatabaseAndCollectionRequest { collectionId: string; offerThroughput: number; databaseLevelThroughput: boolean; - rupmEnabled?: boolean; partitionKey?: PartitionKey; indexingPolicy?: IndexingPolicy; uniqueKeyPolicy?: UniqueKeyPolicy; diff --git a/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx b/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx index b0f1733f7..8fe45f2f6 100644 --- a/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx +++ b/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx @@ -44,7 +44,6 @@ export const FeaturePanelComponent: React.FunctionComponent = () => { onChange?: (_?: React.FormEvent, checked?: boolean) => void; }[] = [ { key: "feature.enablechangefeedpolicy", label: "Enable change feed policy", value: "true" }, - { key: "feature.enablerupm", label: "Enable RUPM", value: "true" }, { key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" }, { key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" }, { key: "feature.enablettl", label: "Enable TTL", value: "true" }, diff --git a/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap b/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap index 7f4a39014..7d8d57936 100644 --- a/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap +++ b/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap @@ -131,12 +131,6 @@ exports[`Feature panel renders all flags 1`] = ` label="Enable change feed policy" onChange={[Function]} /> - { - const hourlyPrice: number = computeRUUsagePriceHourly(serverId, rupmEnabled, throughput, regions, multimaster); + const hourlyPrice: number = computeRUUsagePriceHourly(serverId, throughput, regions, multimaster); const dailyPrice: number = hourlyPrice * 24; const monthlyPrice: number = hourlyPrice * hoursInAMonth; const currency: string = getPriceCurrency(serverId); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx index f3efba887..cc23b0ec9 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx @@ -179,8 +179,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : offerThroughput, serverId, regions, - multimaster, - false + multimaster ); } else { estimatedSpend = getEstimatedAutoscaleSpendElement( diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index c4923e5d0..9d3cf530a 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -133,8 +133,6 @@ exports[`SettingsComponent renders 1`] = ` "partitionKeyVisible": [Function], "requestUnitsUsageCost": [Function], "ruToolTipText": [Function], - "rupm": [Function], - "rupmVisible": [Function], "sharedAutoPilotThroughput": [Function], "sharedThroughputRangeText": [Function], "shouldCreateMongoWildcardIndex": [Function], @@ -622,8 +620,6 @@ exports[`SettingsComponent renders 1`] = ` "partitionKeyVisible": [Function], "requestUnitsUsageCost": [Function], "ruToolTipText": [Function], - "rupm": [Function], - "rupmVisible": [Function], "sharedAutoPilotThroughput": [Function], "sharedThroughputRangeText": [Function], "shouldCreateMongoWildcardIndex": [Function], @@ -1412,8 +1408,6 @@ exports[`SettingsComponent renders 1`] = ` "partitionKeyVisible": [Function], "requestUnitsUsageCost": [Function], "ruToolTipText": [Function], - "rupm": [Function], - "rupmVisible": [Function], "sharedAutoPilotThroughput": [Function], "sharedThroughputRangeText": [Function], "shouldCreateMongoWildcardIndex": [Function], @@ -1901,8 +1895,6 @@ exports[`SettingsComponent renders 1`] = ` "partitionKeyVisible": [Function], "requestUnitsUsageCost": [Function], "ruToolTipText": [Function], - "rupm": [Function], - "rupmVisible": [Function], "sharedAutoPilotThroughput": [Function], "sharedThroughputRangeText": [Function], "shouldCreateMongoWildcardIndex": [Function], @@ -2704,8 +2696,6 @@ exports[`SettingsComponent renders 1`] = ` "partitionKeyVisible": [Function], "requestUnitsUsageCost": [Function], "ruToolTipText": [Function], - "rupm": [Function], - "rupmVisible": [Function], "sharedAutoPilotThroughput": [Function], "sharedThroughputRangeText": [Function], "shouldCreateMongoWildcardIndex": [Function], @@ -3193,8 +3183,6 @@ exports[`SettingsComponent renders 1`] = ` "partitionKeyVisible": [Function], "requestUnitsUsageCost": [Function], "ruToolTipText": [Function], - "rupm": [Function], - "rupmVisible": [Function], "sharedAutoPilotThroughput": [Function], "sharedThroughputRangeText": [Function], "shouldCreateMongoWildcardIndex": [Function], @@ -3983,8 +3971,6 @@ exports[`SettingsComponent renders 1`] = ` "partitionKeyVisible": [Function], "requestUnitsUsageCost": [Function], "ruToolTipText": [Function], - "rupm": [Function], - "rupmVisible": [Function], "sharedAutoPilotThroughput": [Function], "sharedThroughputRangeText": [Function], "shouldCreateMongoWildcardIndex": [Function], @@ -4472,8 +4458,6 @@ exports[`SettingsComponent renders 1`] = ` "partitionKeyVisible": [Function], "requestUnitsUsageCost": [Function], "ruToolTipText": [Function], - "rupm": [Function], - "rupmVisible": [Function], "sharedAutoPilotThroughput": [Function], "sharedThroughputRangeText": [Function], "shouldCreateMongoWildcardIndex": [Function], diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap index 5d0a46560..a13590e36 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap @@ -69,15 +69,15 @@ exports[`SettingsUtils functions render 1`] = ` ¥ - 1.29 + 1.02 hourly / ¥ - 31.06 + 24.48 daily / ¥ - 944.60 + 744.60 monthly diff --git a/src/Explorer/Panes/AddCollectionPane.html b/src/Explorer/Panes/AddCollectionPane.html index 9167bcf93..af007c63b 100644 --- a/src/Explorer/Panes/AddCollectionPane.html +++ b/src/Explorer/Panes/AddCollectionPane.html @@ -243,38 +243,6 @@
-
-
-

- * - RU/m - - More information - - For each 100 Request Units per second (RU/s) provisioned, 1,000 Request Units - per - minute - (RU/m) can be provisioned. E.g.: for a container with 5,000 RU/s provisioned - with - RU/m - enabled, the RU/m budget will be 50,000 RU/m. - - -

-
-
- - -
-
- - -
-
-
-

* diff --git a/src/Explorer/Panes/AddCollectionPane.ts b/src/Explorer/Panes/AddCollectionPane.ts index 62a700d54..cf37da3ca 100644 --- a/src/Explorer/Panes/AddCollectionPane.ts +++ b/src/Explorer/Panes/AddCollectionPane.ts @@ -42,8 +42,6 @@ export default class AddCollectionPane extends ContextualPaneBase { public partitionKeyVisible: ko.Computed; public partitionKeyPattern: ko.Computed; public partitionKeyTitle: ko.Computed; - public rupm: ko.Observable; - public rupmVisible: ko.Observable; public storage: ko.Observable; public throughputSinglePartition: ViewModels.Editable; public throughputMultiPartition: ViewModels.Editable; @@ -143,12 +141,6 @@ export default class AddCollectionPane extends ContextualPaneBase { } return ""; }); - this.rupm = ko.observable(Constants.RUPMStates.off); - this.rupmVisible = ko.observable(false); - const featureSubcription = this.container.features.subscribe(() => { - this.rupmVisible(this.container.isFeatureEnabled(Constants.Features.enableRupm)); - featureSubcription.dispose(); - }); this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue()); @@ -201,7 +193,6 @@ export default class AddCollectionPane extends ContextualPaneBase { account.properties.readLocations.length) || 1; const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false; - const rupmEnabled: boolean = this.rupm() === Constants.RUPMStates.on; let throughputSpendAckText: string; let estimatedSpend: string; @@ -211,23 +202,15 @@ export default class AddCollectionPane extends ContextualPaneBase { serverId, regions, multimaster, - rupmEnabled, this.isSharedAutoPilotSelected() ); - estimatedSpend = PricingUtils.getEstimatedSpendHtml( - offerThroughput, - serverId, - regions, - multimaster, - rupmEnabled - ); + estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster); } else { throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString( this.sharedAutoPilotThroughput(), serverId, regions, multimaster, - rupmEnabled, this.isSharedAutoPilotSelected() ); estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( @@ -264,7 +247,6 @@ export default class AddCollectionPane extends ContextualPaneBase { account.properties.readLocations.length) || 1; const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false; - const rupmEnabled: boolean = this.rupm() === Constants.RUPMStates.on; let throughputSpendAckText: string; let estimatedSpend: string; @@ -274,15 +256,13 @@ export default class AddCollectionPane extends ContextualPaneBase { serverId, regions, multimaster, - rupmEnabled, this.isAutoPilotSelected() ); estimatedSpend = PricingUtils.getEstimatedSpendHtml( this.throughputMultiPartition(), serverId, regions, - multimaster, - rupmEnabled + multimaster ); } else { throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString( @@ -290,7 +270,6 @@ export default class AddCollectionPane extends ContextualPaneBase { serverId, regions, multimaster, - rupmEnabled, this.isAutoPilotSelected() ); estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( @@ -686,8 +665,7 @@ export default class AddCollectionPane extends ContextualPaneBase { storage: this.storage(), offerThroughput: this._getThroughput(), partitionKey: this.partitionKey(), - databaseId: this.databaseId(), - rupm: this.rupm() + databaseId: this.databaseId() }), subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionQuotaId: this.container.quotaId(), @@ -788,7 +766,6 @@ export default class AddCollectionPane extends ContextualPaneBase { id: this.collectionId(), storage: this.storage(), partitionKey, - rupm: this.rupm(), uniqueKeyPolicy, collectionWithThroughputInShared: this.collectionWithThroughputInShared() }), @@ -863,7 +840,6 @@ export default class AddCollectionPane extends ContextualPaneBase { id: this.collectionId(), storage: this.storage(), partitionKey, - rupm: this.rupm(), uniqueKeyPolicy, collectionWithThroughputInShared: this.collectionWithThroughputInShared() }), @@ -898,7 +874,6 @@ export default class AddCollectionPane extends ContextualPaneBase { id: this.collectionId(), storage: this.storage(), partitionKey, - rupm: this.rupm(), uniqueKeyPolicy, collectionWithThroughputInShared: this.collectionWithThroughputInShared() }, @@ -981,20 +956,6 @@ export default class AddCollectionPane extends ContextualPaneBase { return true; } - public onRupmOptionsKeyDown(source: any, event: KeyboardEvent): boolean { - if (event.key === "ArrowRight") { - this.rupm("off"); - return false; - } - - if (event.key === "ArrowLeft") { - this.rupm("on"); - return false; - } - - return true; - } - public onEnableSynapseLinkButtonClicked() { this.container.openEnableSynapseLinkDialog(); } @@ -1018,16 +979,6 @@ export default class AddCollectionPane extends ContextualPaneBase { } const throughput = this._getThroughput(); - const maxThroughputWithRUPM = - SharedConstants.CollectionCreation.MaxRUPMPerPartition * this._calculateNumberOfPartitions(); - - if (this.rupm() === Constants.RUPMStates.on && throughput > maxThroughputWithRUPM) { - this.formErrors( - `The maximum supported provisioned throughput with RU/m enabled is ${maxThroughputWithRUPM} RU/s. Please turn off RU/m to incease thoughput above ${maxThroughputWithRUPM} RU/s.` - ); - return false; - } - if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) { this.formErrors(`Please acknowledge the estimated daily spend.`); return false; diff --git a/src/Explorer/Panes/AddDatabasePane.ts b/src/Explorer/Panes/AddDatabasePane.ts index 8081c7cc8..5f198d9a2 100644 --- a/src/Explorer/Panes/AddDatabasePane.ts +++ b/src/Explorer/Panes/AddDatabasePane.ts @@ -133,19 +133,12 @@ export default class AddDatabasePane extends ContextualPaneBase { let estimatedSpendAcknowledge: string; let estimatedSpend: string; if (!this.isAutoPilotSelected()) { - estimatedSpend = PricingUtils.getEstimatedSpendHtml( - offerThroughput, - serverId, - regions, - multimaster, - false /*rupmEnabled*/ - ); + estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster); estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( offerThroughput, serverId, regions, multimaster, - false /*rupmEnabled*/, this.isAutoPilotSelected() ); } else { @@ -160,7 +153,6 @@ export default class AddDatabasePane extends ContextualPaneBase { serverId, regions, multimaster, - false /*rupmEnabled*/, this.isAutoPilotSelected() ); } diff --git a/src/Explorer/Panes/CassandraAddCollectionPane.ts b/src/Explorer/Panes/CassandraAddCollectionPane.ts index 864f4c386..b72dec1ea 100644 --- a/src/Explorer/Panes/CassandraAddCollectionPane.ts +++ b/src/Explorer/Panes/CassandraAddCollectionPane.ts @@ -138,19 +138,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { let estimatedSpend: string; let estimatedDedicatedSpendAcknowledge: string; if (!this.isAutoPilotSelected()) { - estimatedSpend = PricingUtils.getEstimatedSpendHtml( - offerThroughput, - serverId, - regions, - multimaster, - false /*rupmEnabled*/ - ); + estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster); estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( offerThroughput, serverId, regions, multimaster, - false /*rupmEnabled*/, this.isAutoPilotSelected() ); } else { @@ -165,7 +158,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { serverId, regions, multimaster, - false /*rupmEnabled*/, this.isAutoPilotSelected() ); } @@ -190,19 +182,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { let estimatedSpend: string; let estimatedSharedSpendAcknowledge: string; if (!this.isSharedAutoPilotSelected()) { - estimatedSpend = PricingUtils.getEstimatedSpendHtml( - this.keyspaceThroughput(), - serverId, - regions, - multimaster, - false /*rupmEnabled*/ - ); + estimatedSpend = PricingUtils.getEstimatedSpendHtml(this.keyspaceThroughput(), serverId, regions, multimaster); estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( this.keyspaceThroughput(), serverId, regions, multimaster, - false /*rupmEnabled*/, this.isSharedAutoPilotSelected() ); } else { @@ -217,7 +202,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { serverId, regions, multimaster, - false /*rupmEnabled*/, this.isSharedAutoPilotSelected() ); } @@ -312,8 +296,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { storage: Constants.BackendDefaults.multiPartitionStorageInGb, offerThroughput: this.throughput(), partitionKey: "", - databaseId: this.keyspaceId(), - rupm: false + databaseId: this.keyspaceId() }), subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionQuotaId: this.container.quotaId(), @@ -366,7 +349,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { offerThroughput: this.throughput(), partitionKey: "", databaseId: this.keyspaceId(), - rupm: false, hasDedicatedThroughput: this.dedicateTableThroughput() }), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), @@ -413,7 +395,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { offerThroughput: this.throughput(), partitionKey: "", databaseId: this.keyspaceId(), - rupm: false, hasDedicatedThroughput: this.dedicateTableThroughput() }), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), @@ -444,7 +425,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { offerThroughput: this.throughput(), partitionKey: "", databaseId: this.keyspaceId(), - rupm: false, hasDedicatedThroughput: this.dedicateTableThroughput() }, keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), diff --git a/src/Explorer/Tabs/DatabaseSettingsTab.ts b/src/Explorer/Tabs/DatabaseSettingsTab.ts index 26c125124..1ae0e6e2a 100644 --- a/src/Explorer/Tabs/DatabaseSettingsTab.ts +++ b/src/Explorer/Tabs/DatabaseSettingsTab.ts @@ -155,8 +155,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. this.overrideWithAutoPilotSettings() ? this.autoPilotThroughput() : this.throughput(), serverId, regions, - multimaster, - false /*rupmEnabled*/ + multimaster ); } else { estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( diff --git a/src/Shared/Constants.ts b/src/Shared/Constants.ts index 303217a59..212ff41ca 100644 --- a/src/Shared/Constants.ts +++ b/src/Shared/Constants.ts @@ -126,7 +126,6 @@ export class OfferPricing { Standard: { StartingPrice: 24 / hoursInAMonth, // per hour PricePerRU: 0.00008, - PricePerRUPM: (10 * 2) / 1000 / hoursInAMonth, // preview price: $2 per 1000 RU/m per month -> 100 RU/s PricePerGB: 0.25 / hoursInAMonth } }, @@ -139,7 +138,6 @@ export class OfferPricing { Standard: { StartingPrice: OfferPricing.MonthlyPricing.mooncake.Standard.StartingPrice / hoursInAMonth, // per hour PricePerRU: 0.00051, - PricePerRUPM: (10 * 20) / 1000 / hoursInAMonth, // preview price: 20rmb per 1000 RU/m per month -> 100 RU/s PricePerGB: OfferPricing.MonthlyPricing.mooncake.Standard.PricePerGB / hoursInAMonth } } @@ -156,7 +154,6 @@ export class CollectionCreation { public static readonly MinRU7PartitionsTo25Partitions: number = 2500; public static readonly MinRUPerPartitionAbove25Partitions: number = 100; public static readonly MaxRUPerPartition: number = 10000; - public static readonly MaxRUPMPerPartition: number = 5000; public static readonly MinPartitionedCollectionRUs: number = 2500; public static readonly NumberOfPartitionsInFixedCollection: number = 1; diff --git a/src/Shared/PriceEstimateCalculator.ts b/src/Shared/PriceEstimateCalculator.ts index c9a134b76..d7c30d695 100644 --- a/src/Shared/PriceEstimateCalculator.ts +++ b/src/Shared/PriceEstimateCalculator.ts @@ -1,17 +1,13 @@ import * as Constants from "./Constants"; -export function computeRUUsagePrice(serverId: string, rupmEnabled: boolean, requestUnits: number): string { +export function computeRUUsagePrice(serverId: string, requestUnits: number): string { if (serverId === "mooncake") { - let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU, - rupmCharge = rupmEnabled ? requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRUPM : 0; - return ( - calculateEstimateNumber(ruCharge + rupmCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency - ); + let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU; + return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency; } - let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU, - rupmCharge = rupmEnabled ? requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRUPM : 0; - return calculateEstimateNumber(ruCharge + rupmCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency; + let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU; + return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency; } export function computeStorageUsagePrice(serverId: string, storageUsedRoundUpToGB: number): string { diff --git a/src/Utils/PricingUtils.test.ts b/src/Utils/PricingUtils.test.ts index af1cedf66..fe9fe65c4 100644 --- a/src/Utils/PricingUtils.test.ts +++ b/src/Utils/PricingUtils.test.ts @@ -25,37 +25,37 @@ describe("PricingUtils Tests", () => { describe("computeRUUsagePriceHourly()", () => { it("should return 0 for NaN regions default cloud", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, null, false); + const value = PricingUtils.computeRUUsagePriceHourly("default", 1, null, false); expect(value).toBe(0); }); it("should return 0 for -1 regions", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, -1, false); + const value = PricingUtils.computeRUUsagePriceHourly("default", 1, -1, false); expect(value).toBe(0); }); - it("should return 0.00008 for default cloud, rupm disabled, 1RU, 1 region, multimaster disabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, 1, false); + it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster disabled", () => { + const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 1, false); expect(value).toBe(0.00008); }); - it("should return 0.00051 for Mooncake cloud, rupm disabled, 1RU, 1 region, multimaster disabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("mooncake", false, 1, 1, false); + it("should return 0.00051 for Mooncake cloud, 1RU, 1 region, multimaster disabled", () => { + const value = PricingUtils.computeRUUsagePriceHourly("mooncake", 1, 1, false); expect(value).toBe(0.00051); }); - it("should return 0.00016 for default cloud, rupm disabled, 1RU, 2 regions, multimaster disabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, 2, false); + it("should return 0.00016 for default cloud, 1RU, 2 regions, multimaster disabled", () => { + const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 2, false); expect(value).toBe(0.00016); }); - it("should return 0.00008 for default cloud, rupm disabled, 1RU, 1 region, multimaster enabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, 1, true); + it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster enabled", () => { + const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 1, true); expect(value).toBe(0.00008); }); - it("should return 0.00048 for default cloud, rupm disabled, 1RU, 2 region, multimaster enabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, 2, true); + it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled", () => { + const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 2, true); expect(value).toBe(0.00048); }); }); @@ -150,18 +150,6 @@ describe("PricingUtils Tests", () => { }); }); - describe("getPricePerRuPm()", () => { - it("should return 0.000027397260273972603 for default clouds", () => { - const value = PricingUtils.getPricePerRuPm("default"); - expect(value).toBe(0.000027397260273972603); - }); - - it("should return 0.00027397260273972606 for mooncake", () => { - const value = PricingUtils.getPricePerRuPm("mooncake"); - expect(value).toBe(0.00027397260273972606); - }); - }); - describe("getRegionMultiplier()", () => { describe("without multimaster", () => { it("should return 0 for null", () => { @@ -254,52 +242,48 @@ describe("PricingUtils Tests", () => { }); describe("getEstimatedSpendHtml()", () => { - it("should return 'Estimated cost (USD): $0.000080 hourly / $0.0019 daily / $0.058 monthly (1 region, 1RU/s, $0.00008/RU)' for 1RU/s on default cloud, 1 region, with multimaster, and no rupm", () => { + it("should return 'Estimated cost (USD): $0.000080 hourly / $0.0019 daily / $0.058 monthly (1 region, 1RU/s, $0.00008/RU)' for 1RU/s on default cloud, 1 region, with multimaster", () => { const value = PricingUtils.getEstimatedSpendHtml( 1 /*RU/s*/, "default" /* cloud */, 1 /* region */, - true /* multimaster */, - false /* rupm */ + true /* multimaster */ ); expect(value).toBe( "Estimated cost (USD): $0.000080 hourly / $0.0019 daily / $0.058 monthly (1 region, 1RU/s, $0.00008/RU)" ); }); - it("should return 'Estimated cost (RMB): ¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly (1 region, 1RU/s, ¥0.00051/RU)' for 1RU/s on mooncake, 1 region, with multimaster, and no rupm", () => { + it("should return 'Estimated cost (RMB): ¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly (1 region, 1RU/s, ¥0.00051/RU)' for 1RU/s on mooncake, 1 region, with multimaster", () => { const value = PricingUtils.getEstimatedSpendHtml( 1 /*RU/s*/, "mooncake" /* cloud */, 1 /* region */, - true /* multimaster */, - false /* rupm */ + true /* multimaster */ ); expect(value).toBe( "Estimated cost (RMB): ¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly (1 region, 1RU/s, ¥0.00051/RU)" ); }); - it("should return 'Estimated cost (USD): $0.13 hourly / $3.07 daily / $140.16 monthly (2 regions, 400RU/s, $0.00016/RU)' for 400RU/s on default cloud, 2 region, with multimaster, and no rupm", () => { + it("should return 'Estimated cost (USD): $0.13 hourly / $3.07 daily / $140.16 monthly (2 regions, 400RU/s, $0.00016/RU)' for 400RU/s on default cloud, 2 region, with multimaster", () => { const value = PricingUtils.getEstimatedSpendHtml( 400 /*RU/s*/, "default" /* cloud */, 2 /* region */, - true /* multimaster */, - false /* rupm */ + true /* multimaster */ ); expect(value).toBe( "Estimated cost (USD): $0.19 hourly / $4.61 daily / $140.16 monthly (2 regions, 400RU/s, $0.00016/RU)" ); }); - it("should return 'Estimated cost (USD): $0.064 hourly / $1.54 daily / $46.72 monthly (2 regions, 400RU/s, $0.00008/RU)' for 400RU/s on default cloud, 2 region, without multimaster, and no rupm", () => { + it("should return 'Estimated cost (USD): $0.064 hourly / $1.54 daily / $46.72 monthly (2 regions, 400RU/s, $0.00008/RU)' for 400RU/s on default cloud, 2 region, without multimaster", () => { const value = PricingUtils.getEstimatedSpendHtml( 400 /*RU/s*/, "default" /* cloud */, 2 /* region */, - false /* multimaster */, - false /* rupm */ + false /* multimaster */ ); expect(value).toBe( "Estimated cost (USD): $0.064 hourly / $1.54 daily / $46.72 monthly (2 regions, 400RU/s, $0.00008/RU)" @@ -308,49 +292,45 @@ describe("PricingUtils Tests", () => { }); describe("getEstimatedSpendAcknowledgeString()", () => { - it("should return 'I acknowledge the estimated $0.0019 daily cost for the throughput above.' for 1RU/s on default cloud, 1 region, with multimaster, and no rupm", () => { + it("should return 'I acknowledge the estimated $0.0019 daily cost for the throughput above.' for 1RU/s on default cloud, 1 region, with multimaster", () => { const value = PricingUtils.getEstimatedSpendAcknowledgeString( 1 /*RU/s*/, "default" /* cloud */, 1 /* region */, true /* multimaster */, - false /* rupm */, false ); expect(value).toBe("I acknowledge the estimated $0.0019 daily cost for the throughput above."); }); - it("should return 'I acknowledge the estimated ¥0.012 daily cost for the throughput above.' for 1RU/s on mooncake, 1 region, with multimaster, and no rupm", () => { + it("should return 'I acknowledge the estimated ¥0.012 daily cost for the throughput above.' for 1RU/s on mooncake, 1 region, with multimaster", () => { const value = PricingUtils.getEstimatedSpendAcknowledgeString( 1 /*RU/s*/, "mooncake" /* cloud */, 1 /* region */, true /* multimaster */, - false /* rupm */, false ); expect(value).toBe("I acknowledge the estimated ¥0.012 daily cost for the throughput above."); }); - it("should return 'I acknowledge the estimated $3.07 daily cost for the throughput above.' for 400RU/s on default cloud, 2 region, with multimaster, and no rupm", () => { + it("should return 'I acknowledge the estimated $3.07 daily cost for the throughput above.' for 400RU/s on default cloud, 2 region, with multimaster", () => { const value = PricingUtils.getEstimatedSpendAcknowledgeString( 400 /*RU/s*/, "default" /* cloud */, 2 /* region */, true /* multimaster */, - false /* rupm */, false ); expect(value).toBe("I acknowledge the estimated $4.61 daily cost for the throughput above."); }); - it("should return 'I acknowledge the estimated $1.54 daily cost for the throughput above.' for 400RU/s on default cloud, 2 region, without multimaster, and no rupm", () => { + it("should return 'I acknowledge the estimated $1.54 daily cost for the throughput above.' for 400RU/s on default cloud, 2 region, without multimaster", () => { const value = PricingUtils.getEstimatedSpendAcknowledgeString( 400 /*RU/s*/, "default" /* cloud */, 2 /* region */, false /* multimaster */, - false /* rupm */, false ); expect(value).toBe("I acknowledge the estimated $1.54 daily cost for the throughput above."); diff --git a/src/Utils/PricingUtils.ts b/src/Utils/PricingUtils.ts index bb063d63e..d0515e5ec 100644 --- a/src/Utils/PricingUtils.ts +++ b/src/Utils/PricingUtils.ts @@ -49,21 +49,16 @@ export function getMultimasterMultiplier(numberOfRegions: number, multimasterEna export function computeRUUsagePriceHourly( serverId: string, - rupmEnabled: boolean, requestUnits: number, numberOfRegions: number, multimasterEnabled: boolean ): number { const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled); const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled); - const pricePerRu = getPricePerRu(serverId); - const pricePerRuPm = getPricePerRuPm(serverId); - const ruCharge = requestUnits * pricePerRu * multimasterMultiplier * regionMultiplier; - const rupmCharge = rupmEnabled ? requestUnits * pricePerRuPm : 0; - return Number((ruCharge + rupmCharge).toFixed(5)); + return Number(ruCharge.toFixed(5)); } export function getPriceCurrency(serverId: string): string { @@ -149,14 +144,6 @@ export function getPricePerRu(serverId: string): number { return Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU; } -export function getPricePerRuPm(serverId: string): number { - if (serverId === "mooncake") { - return Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRUPM; - } - - return Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRUPM; -} - export function getAutoPilotV3SpendHtml(maxAutoPilotThroughputSet: number, isDatabaseThroughput: boolean): string { if (!maxAutoPilotThroughputSet) { return ""; @@ -214,10 +201,9 @@ export function getEstimatedSpendHtml( throughput: number, serverId: string, regions: number, - multimaster: boolean, - rupmEnabled: boolean + multimaster: boolean ): string { - const hourlyPrice: number = computeRUUsagePriceHourly(serverId, rupmEnabled, throughput, regions, multimaster); + const hourlyPrice: number = computeRUUsagePriceHourly(serverId, throughput, regions, multimaster); const dailyPrice: number = hourlyPrice * 24; const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth; const currency: string = getPriceCurrency(serverId); @@ -238,12 +224,11 @@ export function getEstimatedSpendAcknowledgeString( serverId: string, regions: number, multimaster: boolean, - rupmEnabled: boolean, isAutoscale: boolean ): string { const hourlyPrice: number = isAutoscale ? computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster) - : computeRUUsagePriceHourly(serverId, rupmEnabled, throughput, regions, multimaster); + : computeRUUsagePriceHourly(serverId, throughput, regions, multimaster); const dailyPrice: number = hourlyPrice * 24; const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth; const currencySign: string = getCurrencySign(serverId); From 0532ed26a2fdc6d05c5d735d9473a8d4a6eb5efd Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Tue, 1 Dec 2020 10:23:18 -0600 Subject: [PATCH 05/21] Remove runner workflow that is no longer functioning (#332) --- .github/workflows/runners.yml | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 .github/workflows/runners.yml diff --git a/.github/workflows/runners.yml b/.github/workflows/runners.yml deleted file mode 100644 index 46513584b..000000000 --- a/.github/workflows/runners.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Runners -on: - schedule: - - cron: "0 * 1 * *" -jobs: - sqlcreatecollection: - runs-on: ubuntu-latest - name: "SQL | Create Collection" - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - - run: npm ci - - run: npm run test:e2e - env: - PORTAL_RUNNER_APP_INSIGHTS_KEY: ${{ secrets.PORTAL_RUNNER_APP_INSIGHTS_KEY }} - PORTAL_RUNNER_USERNAME: ${{ secrets.PORTAL_RUNNER_USERNAME }} - PORTAL_RUNNER_PASSWORD: ${{ secrets.PORTAL_RUNNER_PASSWORD }} - PORTAL_RUNNER_SUBSCRIPTION: 69e02f2d-f059-4409-9eac-97e8a276ae2c - PORTAL_RUNNER_RESOURCE_GROUP: runners - PORTAL_RUNNER_DATABASE_ACCOUNT: portal-sql-runner - - uses: actions/upload-artifact@v2 - if: failure() - with: - name: screenshots - path: failure.png From e133df18dde0f17e717ce9ad058bcecc3f82ea7b Mon Sep 17 00:00:00 2001 From: Tanuj Mittal Date: Thu, 10 Dec 2020 11:54:21 -0800 Subject: [PATCH 06/21] Record baseUrl for OpenTerminal success/failure telemetry (#335) This is useful to know which terminal is opening. --- src/Terminal/index.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Terminal/index.ts b/src/Terminal/index.ts index 79c8a8809..7b49dde8b 100644 --- a/src/Terminal/index.ts +++ b/src/Terminal/index.ts @@ -59,9 +59,8 @@ const main = async (): Promise => { const serverSettings = createServerSettings(urlVars); - const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, { - baseUrl: serverSettings.baseUrl - }); + const data = { baseUrl: serverSettings.baseUrl }; + const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data); try { if (urlVars.hasOwnProperty(TerminalQueryParams.Terminal)) { @@ -70,9 +69,9 @@ const main = async (): Promise => { throw new Error("Only terminal is supported"); } - TelemetryProcessor.traceSuccess(Action.OpenTerminal, startTime); + TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime); } catch (error) { - TelemetryProcessor.traceFailure(Action.OpenTerminal, startTime); + TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime); } }; From 40491ec9c5b732f5044118dfba9133505c6c0870 Mon Sep 17 00:00:00 2001 From: Tanuj Mittal Date: Thu, 10 Dec 2020 13:09:18 -0800 Subject: [PATCH 07/21] Gallery related fixes (#312) * AVERT fixes * Remove enableCodeOfConduct feature flag * Fix reporting abuse * Add empty screen for Liked and Published tabs in Gallery * fix build * Remove unused code * Fix standalone public gallery --- src/Common/Constants.ts | 1 - .../FeaturePanel/FeaturePanelComponent.tsx | 1 - .../FeaturePanelComponent.test.tsx.snap | 14 ++--- .../GalleryViewerComponent.tsx | 54 +++++++++++++++---- .../SettingsComponent.test.tsx.snap | 4 -- src/Explorer/Explorer.ts | 5 -- src/Explorer/Notebook/NotebookManager.ts | 10 +--- .../Panes/PublishNotebookPaneAdapter.tsx | 29 +++++----- src/Juno/JunoClient.ts | 5 +- src/Utils/GalleryUtils.ts | 3 +- 10 files changed, 65 insertions(+), 61 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 89a7fafac..1f6fb7ce1 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -113,7 +113,6 @@ export class Features { public static readonly enableTtl = "enablettl"; public static readonly enableNotebooks = "enablenotebooks"; public static readonly enableGalleryPublish = "enablegallerypublish"; - public static readonly enableCodeOfConduct = "enablecodeofconduct"; public static readonly enableLinkInjection = "enablelinkinjection"; public static readonly enableSpark = "enablespark"; public static readonly livyEndpoint = "livyendpoint"; diff --git a/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx b/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx index 8fe45f2f6..cb41340cc 100644 --- a/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx +++ b/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx @@ -48,7 +48,6 @@ export const FeaturePanelComponent: React.FunctionComponent = () => { { key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" }, { key: "feature.enablettl", label: "Enable TTL", value: "true" }, { key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" }, - { key: "feature.enablecodeofconduct", label: "Enable Code Of Conduct Acknowledgement", value: "true" }, { key: "feature.enableLinkInjection", label: "Enable Injecting Notebook Viewer Link into the first cell", diff --git a/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap b/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap index 7d8d57936..3a47b12eb 100644 --- a/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap +++ b/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap @@ -157,14 +157,14 @@ exports[`Feature panel renders all flags 1`] = ` /> @@ -172,12 +172,6 @@ exports[`Feature panel renders all flags 1`] = ` className="checkboxRow" horizontalAlign="space-between" > - { + return !data || data.length === 0; + }; + + private createEmptyTabContent = (iconName: string, line1: string, line2: string): JSX.Element => { + return ( + + + {line1} + {line2} + + ); + }; + + private createSamplesTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => { + return { + tab, + content: this.createSearchBarHeader(this.createCardsTabContent(data)) + }; + }; + private createPublicGalleryTab( tab: GalleryTab, data: IGalleryItem[], @@ -194,17 +216,29 @@ export class GalleryViewerComponent extends React.Component { return { tab, - content: this.createPublishedNotebooksTabContent(data) + content: this.isEmptyData(data) + ? this.createEmptyTabContent( + "Contact", + "You have not published anything", + "Publish your sample notebooks to share your published work with others" + ) + : this.createPublishedNotebooksTabContent(data) }; }; @@ -364,9 +398,9 @@ export class GalleryViewerComponent extends React.Component { if (!offline) { try { - let response: IJunoResponse | IJunoResponse; - if (this.props.container.isCodeOfConductEnabled()) { - response = await this.props.junoClient.fetchPublicNotebooks(); + let response: IJunoResponse | IJunoResponse; + if (this.props.container) { + response = await this.props.junoClient.getPublicGalleryData(); this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct; this.publicNotebooks = response.data?.notebooksData; } else { @@ -568,7 +602,7 @@ export class GalleryViewerComponent extends React.Component => { GalleryUtils.deleteItem(this.props.container, this.props.junoClient, data, item => { - this.publishedNotebooks = this.publishedNotebooks.filter(notebook => item.id !== notebook.id); + this.publishedNotebooks = this.publishedNotebooks?.filter(notebook => item.id !== notebook.id); this.refreshSelectedTab(item); }); }; diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 9d3cf530a..bc0e13735 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -943,7 +943,6 @@ exports[`SettingsComponent renders 1`] = ` "hasWriteAccess": [Function], "isAccountReady": [Function], "isAuthWithResourceToken": [Function], - "isCodeOfConductEnabled": [Function], "isCopyNotebookPaneEnabled": [Function], "isEnableMongoCapabilityPresent": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], @@ -2218,7 +2217,6 @@ exports[`SettingsComponent renders 1`] = ` "hasWriteAccess": [Function], "isAccountReady": [Function], "isAuthWithResourceToken": [Function], - "isCodeOfConductEnabled": [Function], "isCopyNotebookPaneEnabled": [Function], "isEnableMongoCapabilityPresent": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], @@ -3506,7 +3504,6 @@ exports[`SettingsComponent renders 1`] = ` "hasWriteAccess": [Function], "isAccountReady": [Function], "isAuthWithResourceToken": [Function], - "isCodeOfConductEnabled": [Function], "isCopyNotebookPaneEnabled": [Function], "isEnableMongoCapabilityPresent": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], @@ -4781,7 +4778,6 @@ exports[`SettingsComponent renders 1`] = ` "hasWriteAccess": [Function], "isAccountReady": [Function], "isAuthWithResourceToken": [Function], - "isCodeOfConductEnabled": [Function], "isCopyNotebookPaneEnabled": [Function], "isEnableMongoCapabilityPresent": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], diff --git a/src/Explorer/Explorer.ts b/src/Explorer/Explorer.ts index 0d2e1b64a..0c9f91661 100644 --- a/src/Explorer/Explorer.ts +++ b/src/Explorer/Explorer.ts @@ -204,7 +204,6 @@ export default class Explorer { // features public isGalleryPublishEnabled: ko.Computed; - public isCodeOfConductEnabled: ko.Computed; public isLinkInjectionEnabled: ko.Computed; public isGitHubPaneEnabled: ko.Observable; public isPublishNotebookPaneEnabled: ko.Observable; @@ -404,9 +403,6 @@ export default class Explorer { this.isGalleryPublishEnabled = ko.computed(() => this.isFeatureEnabled(Constants.Features.enableGalleryPublish) ); - this.isCodeOfConductEnabled = ko.computed(() => - this.isFeatureEnabled(Constants.Features.enableCodeOfConduct) - ); this.isLinkInjectionEnabled = ko.computed(() => this.isFeatureEnabled(Constants.Features.enableLinkInjection) ); @@ -2276,7 +2272,6 @@ export default class Explorer { name, content, parentDomElement, - this.isCodeOfConductEnabled(), this.isLinkInjectionEnabled() ); this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter; diff --git a/src/Explorer/Notebook/NotebookManager.ts b/src/Explorer/Notebook/NotebookManager.ts index 932d80e13..6442394a0 100644 --- a/src/Explorer/Notebook/NotebookManager.ts +++ b/src/Explorer/Notebook/NotebookManager.ts @@ -128,17 +128,9 @@ export default class NotebookManager { name: string, content: string | ImmutableNotebook, parentDomElement: HTMLElement, - isCodeOfConductEnabled: boolean, isLinkInjectionEnabled: boolean ): Promise { - await this.publishNotebookPaneAdapter.open( - name, - getFullName(), - content, - parentDomElement, - isCodeOfConductEnabled, - isLinkInjectionEnabled - ); + await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement, isLinkInjectionEnabled); } public openCopyNotebookPane(name: string, content: string): void { diff --git a/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx b/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx index 967355251..f849b7e21 100644 --- a/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx +++ b/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx @@ -98,26 +98,21 @@ export class PublishNotebookPaneAdapter implements ReactAdapter { author: string, notebookContent: string | ImmutableNotebook, parentDomElement: HTMLElement, - isCodeOfConductEnabled: boolean, isLinkInjectionEnabled: boolean ): Promise { - if (isCodeOfConductEnabled) { - try { - const response = await this.junoClient.isCodeOfConductAccepted(); - if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { - throw new Error(`Received HTTP ${response.status} when accepting code of conduct`); - } - - this.isCodeOfConductAccepted = response.data; - } catch (error) { - handleError( - error, - "PublishNotebookPaneAdapter/isCodeOfConductAccepted", - "Failed to check if code of conduct was accepted" - ); + try { + const response = await this.junoClient.isCodeOfConductAccepted(); + if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { + throw new Error(`Received HTTP ${response.status} when accepting code of conduct`); } - } else { - this.isCodeOfConductAccepted = true; + + this.isCodeOfConductAccepted = response.data; + } catch (error) { + handleError( + error, + "PublishNotebookPaneAdapter/isCodeOfConductAccepted", + "Failed to check if code of conduct was accepted" + ); } this.name = name; diff --git a/src/Juno/JunoClient.ts b/src/Juno/JunoClient.ts index b4724002d..bbc1954cc 100644 --- a/src/Juno/JunoClient.ts +++ b/src/Juno/JunoClient.ts @@ -178,8 +178,7 @@ export class JunoClient { return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`); } - // will be renamed once feature.enableCodeOfConduct flag is removed - public async fetchPublicNotebooks(): Promise> { + public async getPublicGalleryData(): Promise> { const url = `${this.getNotebooksAccountUrl()}/gallery/public`; const response = await window.fetch(url, { method: "PATCH", @@ -405,7 +404,7 @@ export class JunoClient { } public async reportAbuse(notebookId: string, abuseCategory: string, notes: string): Promise> { - const response = await window.fetch(`${this.getNotebooksUrl()}/avert/reportAbuse`, { + const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/reportAbuse`, { method: "POST", body: JSON.stringify({ notebookId, diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index 99dee0f07..426e235b0 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -10,6 +10,7 @@ import Explorer from "../Explorer/Explorer"; import { IChoiceGroupOption, IChoiceGroupProps } from "office-ui-fabric-react"; import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent"; import { handleError } from "../Common/ErrorHandlingUtils"; +import { HttpStatusCodes } from "../Common/Constants"; const defaultSelectedAbuseCategory = "Other"; const abuseCategories: IChoiceGroupOption[] = [ @@ -113,7 +114,7 @@ export function reportAbuse( try { const response = await junoClient.reportAbuse(notebookId, abuseCategory, additionalDetails); - if (!response.data) { + if (response.status !== HttpStatusCodes.Accepted) { throw new Error(`Received HTTP ${response.status} when submitting report for ${data.name}`); } From 31e4b49f1168d70f44688e799688a829df581e92 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Thu, 10 Dec 2020 14:13:08 -0800 Subject: [PATCH 08/21] Only call getCollectionDataUsageSize for AAD users (#337) --- src/Common/dataAccess/getCollectionDataUsageSize.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Common/dataAccess/getCollectionDataUsageSize.ts b/src/Common/dataAccess/getCollectionDataUsageSize.ts index a59eab21a..bdbca9e04 100644 --- a/src/Common/dataAccess/getCollectionDataUsageSize.ts +++ b/src/Common/dataAccess/getCollectionDataUsageSize.ts @@ -1,3 +1,4 @@ +import { AuthType } from "../../AuthType"; import { armRequest } from "../../Utils/arm/request"; import { configContext } from "../../ConfigContext"; import { handleError } from "../ErrorHandlingUtils"; @@ -40,6 +41,10 @@ interface MetricsResponse { } export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise => { + if (window.authType !== AuthType.AAD) { + return undefined; + } + const subscriptionId = userContext.subscriptionId; const resourceGroup = userContext.resourceGroup; const accountName = userContext.databaseAccount.name; From c21f42159fbf98954fa7f862de45a1f3d8cfcec5 Mon Sep 17 00:00:00 2001 From: vchske Date: Fri, 11 Dec 2020 10:06:43 -0800 Subject: [PATCH 09/21] Updated cost messaging for new db/container pane (#333) * Adds information text further explaining estimated cost. * Minor tweak to cost messaging * Updated unit tests * Added text and link for capacity planner when choosing manual RUs --- .../ThroughputInputComponentAutoscaleV3.html | 6 ++++++ src/Utils/PricingUtils.test.ts | 16 ++++++++-------- src/Utils/PricingUtils.ts | 6 ++++-- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoscaleV3.html b/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoscaleV3.html index bda2ec9b7..1057cac75 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoscaleV3.html +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoscaleV3.html @@ -126,6 +126,12 @@

+

+ Estimate your required throughput with + capacity calculator +

{ }); describe("getEstimatedSpendHtml()", () => { - it("should return 'Estimated cost (USD): $0.000080 hourly / $0.0019 daily / $0.058 monthly (1 region, 1RU/s, $0.00008/RU)' for 1RU/s on default cloud, 1 region, with multimaster", () => { + it("should return 'Cost (USD): $0.000080 hourly / $0.0019 daily / $0.058 monthly (1 region, 1RU/s, $0.00008/RU)

*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account

' for 1RU/s on default cloud, 1 region, with multimaster", () => { const value = PricingUtils.getEstimatedSpendHtml( 1 /*RU/s*/, "default" /* cloud */, @@ -250,11 +250,11 @@ describe("PricingUtils Tests", () => { true /* multimaster */ ); expect(value).toBe( - "Estimated cost (USD): $0.000080 hourly / $0.0019 daily / $0.058 monthly (1 region, 1RU/s, $0.00008/RU)" + "Cost (USD): $0.000080 hourly / $0.0019 daily / $0.058 monthly (1 region, 1RU/s, $0.00008/RU)

*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account

" ); }); - it("should return 'Estimated cost (RMB): ¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly (1 region, 1RU/s, ¥0.00051/RU)' for 1RU/s on mooncake, 1 region, with multimaster", () => { + it("should return 'Cost (RMB): ¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly (1 region, 1RU/s, ¥0.00051/RU)

*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account

' for 1RU/s on mooncake, 1 region, with multimaster", () => { const value = PricingUtils.getEstimatedSpendHtml( 1 /*RU/s*/, "mooncake" /* cloud */, @@ -262,11 +262,11 @@ describe("PricingUtils Tests", () => { true /* multimaster */ ); expect(value).toBe( - "Estimated cost (RMB): ¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly (1 region, 1RU/s, ¥0.00051/RU)" + "Cost (RMB): ¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly (1 region, 1RU/s, ¥0.00051/RU)

*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account

" ); }); - it("should return 'Estimated cost (USD): $0.13 hourly / $3.07 daily / $140.16 monthly (2 regions, 400RU/s, $0.00016/RU)' for 400RU/s on default cloud, 2 region, with multimaster", () => { + it("should return 'Cost (USD): $0.13 hourly / $3.07 daily / $140.16 monthly (2 regions, 400RU/s, $0.00016/RU)

*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account

' for 400RU/s on default cloud, 2 region, with multimaster", () => { const value = PricingUtils.getEstimatedSpendHtml( 400 /*RU/s*/, "default" /* cloud */, @@ -274,11 +274,11 @@ describe("PricingUtils Tests", () => { true /* multimaster */ ); expect(value).toBe( - "Estimated cost (USD): $0.19 hourly / $4.61 daily / $140.16 monthly (2 regions, 400RU/s, $0.00016/RU)" + "Cost (USD): $0.19 hourly / $4.61 daily / $140.16 monthly (2 regions, 400RU/s, $0.00016/RU)

*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account

" ); }); - it("should return 'Estimated cost (USD): $0.064 hourly / $1.54 daily / $46.72 monthly (2 regions, 400RU/s, $0.00008/RU)' for 400RU/s on default cloud, 2 region, without multimaster", () => { + it("should return 'Cost (USD): $0.064 hourly / $1.54 daily / $46.72 monthly (2 regions, 400RU/s, $0.00008/RU)

*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account

' for 400RU/s on default cloud, 2 region, without multimaster", () => { const value = PricingUtils.getEstimatedSpendHtml( 400 /*RU/s*/, "default" /* cloud */, @@ -286,7 +286,7 @@ describe("PricingUtils Tests", () => { false /* multimaster */ ); expect(value).toBe( - "Estimated cost (USD): $0.064 hourly / $1.54 daily / $46.72 monthly (2 regions, 400RU/s, $0.00008/RU)" + "Cost (USD): $0.064 hourly / $1.54 daily / $46.72 monthly (2 regions, 400RU/s, $0.00008/RU)

*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account

" ); }); }); diff --git a/src/Utils/PricingUtils.ts b/src/Utils/PricingUtils.ts index d0515e5ec..5b6c4ec0e 100644 --- a/src/Utils/PricingUtils.ts +++ b/src/Utils/PricingUtils.ts @@ -211,11 +211,13 @@ export function getEstimatedSpendHtml( const pricePerRu = getPricePerRu(serverId) * getMultimasterMultiplier(regions, multimaster); return ( - `Estimated cost (${currency}): ` + + `Cost (${currency}): ` + `${currencySign}${calculateEstimateNumber(hourlyPrice)} hourly / ` + `${currencySign}${calculateEstimateNumber(dailyPrice)} daily / ` + `${currencySign}${calculateEstimateNumber(monthlyPrice)} monthly ` + - `(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput}RU/s, ${currencySign}${pricePerRu}/RU)` + `(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput}RU/s, ${currencySign}${pricePerRu}/RU)` + + `

` + + `*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account

` ); } From ea39c1d092a7ee22247b64d15e2760a02de6e349 Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Fri, 11 Dec 2020 13:38:57 -0600 Subject: [PATCH 10/21] Fix offer update notification for AAD users (#338) --- package-lock.json | 10 ++++++++-- src/Common/OfferUtility.test.ts | 6 ++++-- src/Common/OfferUtility.ts | 5 +++-- src/Common/dataAccess/readCollectionOffer.ts | 6 ++++-- src/Common/dataAccess/readDatabaseOffer.ts | 6 ++++-- src/Contracts/DataModels.ts | 2 +- .../Controls/Settings/SettingsComponent.test.tsx | 3 ++- src/Explorer/Controls/Settings/SettingsComponent.tsx | 2 +- .../SettingsSubComponents/ScaleComponent.test.tsx | 2 +- .../Settings/SettingsSubComponents/ScaleComponent.tsx | 2 +- src/Explorer/Controls/Settings/TestUtils.tsx | 3 ++- src/Explorer/Tabs/DatabaseSettingsTab.ts | 6 ++---- 12 files changed, 33 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4635ddf5c..47b42c598 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6359,7 +6359,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -14691,6 +14690,14 @@ "requires": { "nan": "2.14.1", "prebuild-install": "5.3.3" + }, + "dependencies": { + "nan": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", + "optional": true + } } }, "killable": { @@ -20134,7 +20141,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "dev": true, "requires": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", diff --git a/src/Common/OfferUtility.test.ts b/src/Common/OfferUtility.test.ts index 5b8a39a69..d24310758 100644 --- a/src/Common/OfferUtility.test.ts +++ b/src/Common/OfferUtility.test.ts @@ -24,7 +24,8 @@ describe("parseSDKOfferResponse", () => { autoscaleMaxThroughput: undefined, minimumThroughput: 400, id: "test", - offerDefinition: mockOfferDefinition + offerDefinition: mockOfferDefinition, + offerReplacePending: false }; expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult); @@ -54,7 +55,8 @@ describe("parseSDKOfferResponse", () => { autoscaleMaxThroughput: 5000, minimumThroughput: 400, id: "test", - offerDefinition: mockOfferDefinition + offerDefinition: mockOfferDefinition, + offerReplacePending: false }; expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult); diff --git a/src/Common/OfferUtility.ts b/src/Common/OfferUtility.ts index a71dc7c84..a6e1377fe 100644 --- a/src/Common/OfferUtility.ts +++ b/src/Common/OfferUtility.ts @@ -1,5 +1,6 @@ import { Offer, SDKOfferDefinition } from "../Contracts/DataModels"; import { OfferResponse } from "@azure/cosmos"; +import { HttpHeaders } from "./Constants"; export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => { const offerDefinition: SDKOfferDefinition = offerResponse?.resource; @@ -18,7 +19,7 @@ export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => { manualThroughput: undefined, minimumThroughput, offerDefinition, - headers: offerResponse.headers + offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true" }; } @@ -28,6 +29,6 @@ export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => { manualThroughput: offerContent.offerThroughput, minimumThroughput, offerDefinition, - headers: offerResponse.headers + offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true" }; }; diff --git a/src/Common/dataAccess/readCollectionOffer.ts b/src/Common/dataAccess/readCollectionOffer.ts index f7d97a4b0..9d0ed5fc3 100644 --- a/src/Common/dataAccess/readCollectionOffer.ts +++ b/src/Common/dataAccess/readCollectionOffer.ts @@ -105,7 +105,8 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri id: offerId, autoscaleMaxThroughput: autoscaleSettings.maxThroughput, manualThroughput: undefined, - minimumThroughput + minimumThroughput, + offerReplacePending: resource.offerReplacePending === "true" }; } @@ -113,7 +114,8 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri id: offerId, autoscaleMaxThroughput: undefined, manualThroughput: resource.throughput, - minimumThroughput + minimumThroughput, + offerReplacePending: resource.offerReplacePending === "true" }; } diff --git a/src/Common/dataAccess/readDatabaseOffer.ts b/src/Common/dataAccess/readDatabaseOffer.ts index 1f4a67acd..9e99745c7 100644 --- a/src/Common/dataAccess/readDatabaseOffer.ts +++ b/src/Common/dataAccess/readDatabaseOffer.ts @@ -77,7 +77,8 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise => { id: offerId, autoscaleMaxThroughput: autoscaleSettings.maxThroughput, manualThroughput: undefined, - minimumThroughput + minimumThroughput, + offerReplacePending: resource.offerReplacePending === "true" }; } @@ -85,7 +86,8 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise => { id: offerId, autoscaleMaxThroughput: undefined, manualThroughput: resource.throughput, - minimumThroughput + minimumThroughput, + offerReplacePending: resource.offerReplacePending === "true" }; } diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 8df58ba18..505090c18 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -214,7 +214,7 @@ export interface Offer { manualThroughput: number; minimumThroughput: number; offerDefinition?: SDKOfferDefinition; - headers?: any; + offerReplacePending: boolean; } export interface SDKOfferDefinition extends Resource { diff --git a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx index c230753e4..c8ea4f390 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx @@ -92,7 +92,8 @@ describe("SettingsComponent", () => { autoscaleMaxThroughput: 10000, manualThroughput: undefined, minimumThroughput: 400, - id: "test" + id: "test", + offerReplacePending: false }); const props = { ...baseProps }; diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index fc9d88e05..704bebb24 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -295,7 +295,7 @@ export class SettingsComponent extends React.Component { - return !!this.collection?.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending]; + return this.collection?.offer()?.offerReplacePending; }; public onSaveClick = async (): Promise => { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx index 628ac46ea..cab9803b7 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx @@ -59,7 +59,7 @@ describe("ScaleComponent", () => { autoscaleMaxThroughput: maxThroughput, minimumThroughput: 400, id: "offer", - headers: { "x-ms-offer-replace-pending": true } + offerReplacePending: true }); const newProps = { ...baseProps, diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx index c8f193b44..10aaa0cbf 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx @@ -116,7 +116,7 @@ export class ScaleComponent extends React.Component { } const offer = this.props.collection?.offer(); - if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) { + if (offer?.offerReplacePending) { const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput; return getThroughputApplyShortDelayMessage( this.props.isAutoPilotSelected, diff --git a/src/Explorer/Controls/Settings/TestUtils.tsx b/src/Explorer/Controls/Settings/TestUtils.tsx index 020d1e2e5..6a318bf80 100644 --- a/src/Explorer/Controls/Settings/TestUtils.tsx +++ b/src/Explorer/Controls/Settings/TestUtils.tsx @@ -23,7 +23,8 @@ export const collection = ({ autoscaleMaxThroughput: undefined, manualThroughput: 10000, minimumThroughput: 6000, - id: "offer" + id: "offer", + offerReplacePending: false }), conflictResolutionPolicy: ko.observable( {} as DataModels.ConflictResolutionPolicy diff --git a/src/Explorer/Tabs/DatabaseSettingsTab.ts b/src/Explorer/Tabs/DatabaseSettingsTab.ts index 1ae0e6e2a..75f441af4 100644 --- a/src/Explorer/Tabs/DatabaseSettingsTab.ts +++ b/src/Explorer/Tabs/DatabaseSettingsTab.ts @@ -230,9 +230,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. return this.throughputTitle() + this.requestUnitsUsageCost(); }); this.pendingNotification = ko.observable(); - this._offerReplacePending = ko.observable( - !!this.database.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending] - ); + this._offerReplacePending = ko.observable(!!this.database.offer()?.offerReplacePending); this.notificationStatusInfo = ko.observable(""); this.shouldShowNotificationStatusPrompt = ko.computed(() => this.notificationStatusInfo().length > 0); this.warningMessage = ko.computed(() => { @@ -241,7 +239,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. } const offer = this.database.offer(); - if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) { + if (offer?.offerReplacePending) { const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput; return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id()); } From f54e8eb692cb780853915f97f02e55db649c56e3 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Wed, 16 Dec 2020 15:27:17 -0800 Subject: [PATCH 11/21] Move queryDocuments out of DataAccessUtility (#334) --- src/Common/DataAccessUtilityBase.ts | 169 -------- src/Common/DocumentClientUtilityBase.ts | 217 ---------- src/Common/DocumentUtility.ts | 10 + src/Common/QueriesClient.ts | 134 +++--- .../queryDocuments.test.ts.snap} | 0 .../dataAccess/createCollection.test.ts | 1 - src/Common/dataAccess/createDocument.ts | 25 ++ src/Common/dataAccess/deleteConflict.ts | 36 ++ src/Common/dataAccess/deleteDocument.ts | 25 ++ .../dataAccess/executeStoredProcedure.ts | 48 +++ src/Common/dataAccess/queryConflicts.ts | 14 + .../queryDocuments.test.ts} | 26 +- src/Common/dataAccess/queryDocuments.ts | 34 ++ src/Common/dataAccess/queryDocumentsPage.ts | 26 ++ src/Common/dataAccess/readDocument.ts | 27 ++ src/Common/dataAccess/updateDocument.ts | 32 ++ .../ContainerSampleGenerator.test.ts | 4 +- .../DataSamples/ContainerSampleGenerator.ts | 17 +- .../GraphExplorer.test.tsx | 10 +- .../GraphExplorerComponent/GraphExplorer.tsx | 212 +++++----- .../DataTable/TableEntityListViewModel.ts | 88 ++-- src/Explorer/Tables/TableDataClient.ts | 399 ++++++++---------- src/Explorer/Tabs/ConflictsTab.ts | 339 +++++++-------- src/Explorer/Tabs/DatabaseSettingsTab.ts | 9 +- src/Explorer/Tabs/DocumentsTab.html | 2 +- src/Explorer/Tabs/DocumentsTab.ts | 155 +++---- src/Explorer/Tabs/GraphTab.ts | 7 +- src/Explorer/Tabs/MongoDocumentsTab.html | 2 +- src/Explorer/Tabs/MongoDocumentsTab.ts | 23 +- src/Explorer/Tabs/MongoShellTab.ts | 7 +- src/Explorer/Tabs/QueryTab.ts | 192 ++++----- src/Explorer/Tabs/QueryTablesTab.ts | 21 +- src/Explorer/Tabs/ScriptTabBase.ts | 11 +- src/Explorer/Tabs/SettingsTabV2.tsx | 89 ++-- src/Explorer/Tabs/TabsBase.ts | 6 +- src/Explorer/Tree/Collection.ts | 43 +- src/Explorer/Tree/ConflictId.ts | 53 +-- src/Explorer/Tree/DocumentId.ts | 4 +- src/Explorer/Tree/StoredProcedure.ts | 2 +- src/Utils/QueryUtils.ts | 86 ++-- 40 files changed, 1163 insertions(+), 1442 deletions(-) delete mode 100644 src/Common/DataAccessUtilityBase.ts delete mode 100644 src/Common/DocumentClientUtilityBase.ts create mode 100644 src/Common/DocumentUtility.ts rename src/Common/{__snapshots__/DataAccessUtilityBase.test.ts.snap => dataAccess/__snapshots__/queryDocuments.test.ts.snap} (100%) create mode 100644 src/Common/dataAccess/createDocument.ts create mode 100644 src/Common/dataAccess/deleteConflict.ts create mode 100644 src/Common/dataAccess/deleteDocument.ts create mode 100644 src/Common/dataAccess/executeStoredProcedure.ts create mode 100644 src/Common/dataAccess/queryConflicts.ts rename src/Common/{DataAccessUtilityBase.test.ts => dataAccess/queryDocuments.test.ts} (73%) create mode 100644 src/Common/dataAccess/queryDocuments.ts create mode 100644 src/Common/dataAccess/queryDocumentsPage.ts create mode 100644 src/Common/dataAccess/readDocument.ts create mode 100644 src/Common/dataAccess/updateDocument.ts diff --git a/src/Common/DataAccessUtilityBase.ts b/src/Common/DataAccessUtilityBase.ts deleted file mode 100644 index e835829d4..000000000 --- a/src/Common/DataAccessUtilityBase.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { ConflictDefinition, FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; -import Q from "q"; -import * as DataModels from "../Contracts/DataModels"; -import * as ViewModels from "../Contracts/ViewModels"; -import ConflictId from "../Explorer/Tree/ConflictId"; -import DocumentId from "../Explorer/Tree/DocumentId"; -import StoredProcedure from "../Explorer/Tree/StoredProcedure"; -import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; -import * as Constants from "./Constants"; -import { client } from "./CosmosClient"; - -export function getCommonQueryOptions(options: FeedOptions): any { - const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage); - options = options || {}; - options.populateQueryMetrics = true; - options.enableScanInQuery = options.enableScanInQuery || true; - if (!options.partitionKey) { - options.forceQueryPlan = true; - } - options.maxItemCount = - options.maxItemCount || - (storedItemPerPageSetting !== undefined && storedItemPerPageSetting) || - Constants.Queries.itemsPerPage; - options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism); - - return options; -} - -export function queryDocuments( - databaseId: string, - containerId: string, - query: string, - options: any -): Q.Promise> { - options = getCommonQueryOptions(options); - const documentsIterator = client() - .database(databaseId) - .container(containerId) - .items.query(query, options); - return Q(documentsIterator); -} - -export function getPartitionKeyHeaderForConflict(conflictId: ConflictId): Object { - const partitionKeyDefinition: DataModels.PartitionKey = conflictId.partitionKey; - const partitionKeyValue: any = conflictId.partitionKeyValue; - - return getPartitionKeyHeader(partitionKeyDefinition, partitionKeyValue); -} - -export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.PartitionKey, partitionKeyValue: any): Object { - if (!partitionKeyDefinition) { - return undefined; - } - - if (partitionKeyValue === undefined) { - return [{}]; - } - - return [partitionKeyValue]; -} - -export function updateDocument( - collection: ViewModels.CollectionBase, - documentId: DocumentId, - newDocument: any -): Q.Promise { - const partitionKey = documentId.partitionKeyValue; - - return Q( - client() - .database(collection.databaseId) - .container(collection.id()) - .item(documentId.id(), partitionKey) - .replace(newDocument) - .then(response => response.resource) - ); -} - -export function executeStoredProcedure( - collection: ViewModels.Collection, - storedProcedure: StoredProcedure, - partitionKeyValue: any, - params: any[] -): Q.Promise { - // TODO remove this deferred. Kept it because of timeout code at bottom of function - const deferred = Q.defer(); - - client() - .database(collection.databaseId) - .container(collection.id()) - .scripts.storedProcedure(storedProcedure.id()) - .execute(partitionKeyValue, params, { enableScriptLogging: true }) - .then(response => - deferred.resolve({ - result: response.resource, - scriptLogs: response.headers[Constants.HttpHeaders.scriptLogResults] - }) - ) - .catch(error => deferred.reject(error)); - - return deferred.promise.timeout( - Constants.ClientDefaults.requestTimeoutMs, - `Request timed out while executing stored procedure ${storedProcedure.id()}` - ); -} - -export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise { - return Q( - client() - .database(collection.databaseId) - .container(collection.id()) - .items.create(newDocument) - .then(response => response.resource) - ); -} - -export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise { - const partitionKey = documentId.partitionKeyValue; - - return Q( - client() - .database(collection.databaseId) - .container(collection.id()) - .item(documentId.id(), partitionKey) - .read() - .then(response => response.resource) - ); -} - -export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise { - const partitionKey = documentId.partitionKeyValue; - - return Q( - client() - .database(collection.databaseId) - .container(collection.id()) - .item(documentId.id(), partitionKey) - .delete() - ); -} - -export function deleteConflict( - collection: ViewModels.CollectionBase, - conflictId: ConflictId, - options: any = {} -): Q.Promise { - options.partitionKey = options.partitionKey || getPartitionKeyHeaderForConflict(conflictId); - - return Q( - client() - .database(collection.databaseId) - .container(collection.id()) - .conflict(conflictId.id()) - .delete(options) - ); -} - -export function queryConflicts( - databaseId: string, - containerId: string, - query: string, - options: any -): Q.Promise> { - const documentsIterator = client() - .database(databaseId) - .container(containerId) - .conflicts.query(query, options); - return Q(documentsIterator); -} diff --git a/src/Common/DocumentClientUtilityBase.ts b/src/Common/DocumentClientUtilityBase.ts deleted file mode 100644 index ded90cf05..000000000 --- a/src/Common/DocumentClientUtilityBase.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; -import Q from "q"; -import * as ViewModels from "../Contracts/ViewModels"; -import ConflictId from "../Explorer/Tree/ConflictId"; -import DocumentId from "../Explorer/Tree/DocumentId"; -import StoredProcedure from "../Explorer/Tree/StoredProcedure"; -import { logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; -import * as Constants from "./Constants"; -import * as DataAccessUtilityBase from "./DataAccessUtilityBase"; -import { MinimalQueryIterator, nextPage } from "./IteratorUtilities"; -import { handleError } from "./ErrorHandlingUtils"; - -// TODO: Log all promise resolutions and errors with verbosity levels -export function queryDocuments( - databaseId: string, - containerId: string, - query: string, - options: any -): Q.Promise> { - return DataAccessUtilityBase.queryDocuments(databaseId, containerId, query, options); -} - -export function queryConflicts( - databaseId: string, - containerId: string, - query: string, - options: any -): Q.Promise> { - return DataAccessUtilityBase.queryConflicts(databaseId, containerId, query, options); -} - -export function getEntityName() { - const defaultExperience = - window.dataExplorer && window.dataExplorer.defaultExperience && window.dataExplorer.defaultExperience(); - if (defaultExperience === Constants.DefaultAccountExperience.MongoDB) { - return "document"; - } - return "item"; -} - -export function executeStoredProcedure( - collection: ViewModels.Collection, - storedProcedure: StoredProcedure, - partitionKeyValue: any, - params: any[] -): Q.Promise { - var deferred = Q.defer(); - - const clearMessage = logConsoleProgress(`Executing stored procedure ${storedProcedure.id()}`); - DataAccessUtilityBase.executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params) - .then( - (response: any) => { - deferred.resolve(response); - logConsoleInfo( - `Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}` - ); - }, - (error: any) => { - handleError( - error, - "ExecuteStoredProcedure", - `Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}` - ); - deferred.reject(error); - } - ) - .finally(() => { - clearMessage(); - }); - - return deferred.promise; -} - -export function queryDocumentsPage( - resourceName: string, - documentsIterator: MinimalQueryIterator, - firstItemIndex: number, - options: any -): Q.Promise { - var deferred = Q.defer(); - const entityName = getEntityName(); - const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`); - Q(nextPage(documentsIterator, firstItemIndex)) - .then( - (result: ViewModels.QueryResults) => { - const itemCount = (result.documents && result.documents.length) || 0; - logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`); - deferred.resolve(result); - }, - (error: any) => { - handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`); - deferred.reject(error); - } - ) - .finally(() => { - clearMessage(); - }); - - return deferred.promise; -} - -export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise { - var deferred = Q.defer(); - const entityName = getEntityName(); - const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`); - DataAccessUtilityBase.readDocument(collection, documentId) - .then( - (document: any) => { - deferred.resolve(document); - }, - (error: any) => { - handleError(error, "ReadDocument", `Failed to read ${entityName} ${documentId.id()}`); - deferred.reject(error); - } - ) - .finally(() => { - clearMessage(); - }); - - return deferred.promise; -} - -export function updateDocument( - collection: ViewModels.CollectionBase, - documentId: DocumentId, - newDocument: any -): Q.Promise { - var deferred = Q.defer(); - const entityName = getEntityName(); - const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`); - DataAccessUtilityBase.updateDocument(collection, documentId, newDocument) - .then( - (updatedDocument: any) => { - logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`); - deferred.resolve(updatedDocument); - }, - (error: any) => { - handleError(error, "UpdateDocument", `Failed to update ${entityName} ${documentId.id()}`); - deferred.reject(error); - } - ) - .finally(() => { - clearMessage(); - }); - - return deferred.promise; -} - -export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise { - var deferred = Q.defer(); - const entityName = getEntityName(); - const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`); - DataAccessUtilityBase.createDocument(collection, newDocument) - .then( - (savedDocument: any) => { - logConsoleInfo(`Successfully created new ${entityName} for container ${collection.id()}`); - deferred.resolve(savedDocument); - }, - (error: any) => { - handleError(error, "CreateDocument", `Error while creating new ${entityName} for container ${collection.id()}`); - deferred.reject(error); - } - ) - .finally(() => { - clearMessage(); - }); - - return deferred.promise; -} - -export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise { - var deferred = Q.defer(); - const entityName = getEntityName(); - const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`); - DataAccessUtilityBase.deleteDocument(collection, documentId) - .then( - (response: any) => { - logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`); - deferred.resolve(response); - }, - (error: any) => { - handleError(error, "DeleteDocument", `Error while deleting ${entityName} ${documentId.id()}`); - deferred.reject(error); - } - ) - .finally(() => { - clearMessage(); - }); - - return deferred.promise; -} - -export function deleteConflict( - collection: ViewModels.CollectionBase, - conflictId: ConflictId, - options?: any -): Q.Promise { - var deferred = Q.defer(); - - const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`); - DataAccessUtilityBase.deleteConflict(collection, conflictId, options) - .then( - (response: any) => { - logConsoleInfo(`Successfully deleted conflict ${conflictId.id()}`); - deferred.resolve(response); - }, - (error: any) => { - handleError(error, "DeleteConflict", `Error while deleting conflict ${conflictId.id()}`); - deferred.reject(error); - } - ) - .finally(() => { - clearMessage(); - }); - - return deferred.promise; -} diff --git a/src/Common/DocumentUtility.ts b/src/Common/DocumentUtility.ts new file mode 100644 index 000000000..b552ba495 --- /dev/null +++ b/src/Common/DocumentUtility.ts @@ -0,0 +1,10 @@ +import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType"; +import { userContext } from "../UserContext"; + +export const getEntityName = (): string => { + if (userContext.defaultExperience === DefaultAccountExperienceType.MongoDB) { + return "document"; + } + + return "item"; +}; diff --git a/src/Common/QueriesClient.ts b/src/Common/QueriesClient.ts index a7f28f7b7..d11d36e81 100644 --- a/src/Common/QueriesClient.ts +++ b/src/Common/QueriesClient.ts @@ -3,16 +3,18 @@ import * as _ from "underscore"; import * as DataModels from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; import Explorer from "../Explorer/Explorer"; -import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; import DocumentId from "../Explorer/Tree/DocumentId"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { QueryUtils } from "../Utils/QueryUtils"; import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; import { userContext } from "../UserContext"; -import { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase"; +import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage"; import { createCollection } from "./dataAccess/createCollection"; import { handleError } from "./ErrorHandlingUtils"; +import { createDocument } from "./dataAccess/createDocument"; +import { deleteDocument } from "./dataAccess/deleteDocument"; +import { queryDocuments } from "./dataAccess/queryDocuments"; export class QueriesClient { private static readonly PartitionKey: DataModels.PartitionKey = { @@ -31,10 +33,7 @@ export class QueriesClient { return Promise.resolve(queriesCollection.rawDataModel); } - const id = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - "Setting up account for saving queries" - ); + const clearMessage = NotificationConsoleUtils.logConsoleProgress("Setting up account for saving queries"); return createCollection({ collectionId: SavedQueries.CollectionName, createNewDatabase: true, @@ -45,10 +44,7 @@ export class QueriesClient { }) .then( (collection: DataModels.Collection) => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - "Successfully set up account for saving queries" - ); + NotificationConsoleUtils.logConsoleInfo("Successfully set up account for saving queries"); return Promise.resolve(collection); }, (error: any) => { @@ -56,17 +52,14 @@ export class QueriesClient { return Promise.reject(error); } ) - .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); + .finally(() => clearMessage()); } public async saveQuery(query: DataModels.Query): Promise { const queriesCollection = this.findQueriesCollection(); if (!queriesCollection) { const errorMessage: string = "Account not set up to perform saved query operations"; - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to save query ${query.queryName}: ${errorMessage}` - ); + NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`); return Promise.reject(errorMessage); } @@ -74,25 +67,16 @@ export class QueriesClient { this.validateQuery(query); } catch (error) { const errorMessage: string = "Invalid query specified"; - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to save query ${query.queryName}: ${errorMessage}` - ); + NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`); return Promise.reject(errorMessage); } - const id = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Saving query ${query.queryName}` - ); + const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Saving query ${query.queryName}`); query.id = query.queryName; return createDocument(queriesCollection, query) .then( (savedQuery: DataModels.Query) => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully saved query ${query.queryName}` - ); + NotificationConsoleUtils.logConsoleInfo(`Successfully saved query ${query.queryName}`); return Promise.resolve(); }, (error: any) => { @@ -103,74 +87,65 @@ export class QueriesClient { return Promise.reject(error); } ) - .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); + .finally(() => clearMessage()); } public async getQueries(): Promise { const queriesCollection = this.findQueriesCollection(); if (!queriesCollection) { const errorMessage: string = "Account not set up to perform saved query operations"; - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to fetch saved queries: ${errorMessage}` - ); + NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`); return Promise.reject(errorMessage); } const options: any = { enableCrossPartitionQuery: true }; - const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Fetching saved queries"); - return queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options) + const clearMessage = NotificationConsoleUtils.logConsoleProgress("Fetching saved queries"); + const queryIterator: QueryIterator = queryDocuments( + SavedQueries.DatabaseName, + SavedQueries.CollectionName, + this.fetchQueriesQuery(), + options + ); + const fetchQueries = async (firstItemIndex: number): Promise => + await queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex); + return QueryUtils.queryAllPages(fetchQueries) .then( - (queryIterator: QueryIterator) => { - const fetchQueries = (firstItemIndex: number): Q.Promise => - queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex, options); - return QueryUtils.queryAllPages(fetchQueries).then( - (results: ViewModels.QueryResults) => { - let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => { - if (!document) { - return undefined; - } - const { id, resourceId, query, queryName } = document; - const parsedQuery: DataModels.Query = { - resourceId: resourceId, - queryName: queryName, - query: query, - id: id - }; - try { - this.validateQuery(parsedQuery); - return parsedQuery; - } catch (error) { - return undefined; - } - }); - queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully fetched saved queries"); - return Promise.resolve(queries); - }, - (error: any) => { - handleError(error, "getSavedQueries", "Failed to fetch saved queries"); - return Promise.reject(error); + (results: ViewModels.QueryResults) => { + let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => { + if (!document) { + return undefined; } - ); + const { id, resourceId, query, queryName } = document; + const parsedQuery: DataModels.Query = { + resourceId: resourceId, + queryName: queryName, + query: query, + id: id + }; + try { + this.validateQuery(parsedQuery); + return parsedQuery; + } catch (error) { + return undefined; + } + }); + queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery); + NotificationConsoleUtils.logConsoleInfo("Successfully fetched saved queries"); + return Promise.resolve(queries); }, (error: any) => { - // should never get into this state but we handle this regardless handleError(error, "getSavedQueries", "Failed to fetch saved queries"); return Promise.reject(error); } ) - .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); + .finally(() => clearMessage()); } public async deleteQuery(query: DataModels.Query): Promise { const queriesCollection = this.findQueriesCollection(); if (!queriesCollection) { const errorMessage: string = "Account not set up to perform saved query operations"; - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to fetch saved queries: ${errorMessage}` - ); + NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`); return Promise.reject(errorMessage); } @@ -178,16 +153,10 @@ export class QueriesClient { this.validateQuery(query); } catch (error) { const errorMessage: string = "Invalid query specified"; - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to delete query ${query.queryName}: ${errorMessage}` - ); + NotificationConsoleUtils.logConsoleError(`Failed to delete query ${query.queryName}: ${errorMessage}`); } - const id = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Deleting query ${query.queryName}` - ); + const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting query ${query.queryName}`); query.id = query.queryName; const documentId = new DocumentId( { @@ -201,10 +170,7 @@ export class QueriesClient { return deleteDocument(queriesCollection, documentId) .then( () => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully deleted query ${query.queryName}` - ); + NotificationConsoleUtils.logConsoleInfo(`Successfully deleted query ${query.queryName}`); return Promise.resolve(); }, (error: any) => { @@ -212,7 +178,7 @@ export class QueriesClient { return Promise.reject(error); } ) - .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); + .finally(() => clearMessage()); } public getResourceId(): string { diff --git a/src/Common/__snapshots__/DataAccessUtilityBase.test.ts.snap b/src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap similarity index 100% rename from src/Common/__snapshots__/DataAccessUtilityBase.test.ts.snap rename to src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap diff --git a/src/Common/dataAccess/createCollection.test.ts b/src/Common/dataAccess/createCollection.test.ts index 79902e72f..7d4076f35 100644 --- a/src/Common/dataAccess/createCollection.test.ts +++ b/src/Common/dataAccess/createCollection.test.ts @@ -1,6 +1,5 @@ jest.mock("../../Utils/arm/request"); jest.mock("../CosmosClient"); -jest.mock("../DataAccessUtilityBase"); import { AuthType } from "../../AuthType"; import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; diff --git a/src/Common/dataAccess/createDocument.ts b/src/Common/dataAccess/createDocument.ts new file mode 100644 index 000000000..b64f70ff9 --- /dev/null +++ b/src/Common/dataAccess/createDocument.ts @@ -0,0 +1,25 @@ +import { CollectionBase } from "../../Contracts/ViewModels"; +import { client } from "../CosmosClient"; +import { getEntityName } from "../DocumentUtility"; +import { handleError } from "../ErrorHandlingUtils"; +import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; + +export const createDocument = async (collection: CollectionBase, newDocument: unknown): Promise => { + const entityName = getEntityName(); + const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`); + + try { + const response = await client() + .database(collection.databaseId) + .container(collection.id()) + .items.create(newDocument); + + logConsoleInfo(`Successfully created new ${entityName} for container ${collection.id()}`); + return response?.resource; + } catch (error) { + handleError(error, "CreateDocument", `Error while creating new ${entityName} for container ${collection.id()}`); + throw error; + } finally { + clearMessage(); + } +}; diff --git a/src/Common/dataAccess/deleteConflict.ts b/src/Common/dataAccess/deleteConflict.ts new file mode 100644 index 000000000..746d3577b --- /dev/null +++ b/src/Common/dataAccess/deleteConflict.ts @@ -0,0 +1,36 @@ +import ConflictId from "../../Explorer/Tree/ConflictId"; +import { CollectionBase } from "../../Contracts/ViewModels"; +import { RequestOptions } from "@azure/cosmos"; +import { client } from "../CosmosClient"; +import { handleError } from "../ErrorHandlingUtils"; +import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; + +export const deleteConflict = async (collection: CollectionBase, conflictId: ConflictId): Promise => { + const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`); + + try { + const options = { + partitionKey: getPartitionKeyHeaderForConflict(conflictId) + }; + + await client() + .database(collection.databaseId) + .container(collection.id()) + .conflict(conflictId.id()) + .delete(options as RequestOptions); + logConsoleInfo(`Successfully deleted conflict ${conflictId.id()}`); + } catch (error) { + handleError(error, "DeleteConflict", `Error while deleting conflict ${conflictId.id()}`); + throw error; + } finally { + clearMessage(); + } +}; + +const getPartitionKeyHeaderForConflict = (conflictId: ConflictId): unknown => { + if (!conflictId.partitionKey) { + return undefined; + } + + return conflictId.partitionKeyValue === undefined ? [{}] : [conflictId.partitionKeyValue]; +}; diff --git a/src/Common/dataAccess/deleteDocument.ts b/src/Common/dataAccess/deleteDocument.ts new file mode 100644 index 000000000..0ab2e6999 --- /dev/null +++ b/src/Common/dataAccess/deleteDocument.ts @@ -0,0 +1,25 @@ +import { CollectionBase } from "../../Contracts/ViewModels"; +import { client } from "../CosmosClient"; +import { getEntityName } from "../DocumentUtility"; +import { handleError } from "../ErrorHandlingUtils"; +import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; +import DocumentId from "../../Explorer/Tree/DocumentId"; + +export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise => { + const entityName: string = getEntityName(); + const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`); + + try { + await client() + .database(collection.databaseId) + .container(collection.id()) + .item(documentId.id(), documentId.partitionKeyValue) + .delete(); + logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`); + } catch (error) { + handleError(error, "DeleteDocument", `Error while deleting ${entityName} ${documentId.id()}`); + throw error; + } finally { + clearMessage(); + } +}; diff --git a/src/Common/dataAccess/executeStoredProcedure.ts b/src/Common/dataAccess/executeStoredProcedure.ts new file mode 100644 index 000000000..7459c3a02 --- /dev/null +++ b/src/Common/dataAccess/executeStoredProcedure.ts @@ -0,0 +1,48 @@ +import { Collection } from "../../Contracts/ViewModels"; +import { ClientDefaults, HttpHeaders } from "../Constants"; +import { client } from "../CosmosClient"; +import { handleError } from "../ErrorHandlingUtils"; +import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; +import StoredProcedure from "../../Explorer/Tree/StoredProcedure"; + +export interface ExecuteSprocResult { + result: StoredProcedure; + scriptLogs: string; +} + +export const executeStoredProcedure = async ( + collection: Collection, + storedProcedure: StoredProcedure, + partitionKeyValue: string, + params: string[] +): Promise => { + const clearMessage = logConsoleProgress(`Executing stored procedure ${storedProcedure.id()}`); + const timeout = setTimeout(() => { + throw Error(`Request timed out while executing stored procedure ${storedProcedure.id()}`); + }, ClientDefaults.requestTimeoutMs); + + try { + const response = await client() + .database(collection.databaseId) + .container(collection.id()) + .scripts.storedProcedure(storedProcedure.id()) + .execute(partitionKeyValue, params, { enableScriptLogging: true }); + clearTimeout(timeout); + logConsoleInfo( + `Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}` + ); + return { + result: response.resource, + scriptLogs: response.headers[HttpHeaders.scriptLogResults] as string + }; + } catch (error) { + handleError( + error, + "ExecuteStoredProcedure", + `Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}` + ); + throw error; + } finally { + clearMessage(); + } +}; diff --git a/src/Common/dataAccess/queryConflicts.ts b/src/Common/dataAccess/queryConflicts.ts new file mode 100644 index 000000000..ed3ecaa26 --- /dev/null +++ b/src/Common/dataAccess/queryConflicts.ts @@ -0,0 +1,14 @@ +import { ConflictDefinition, FeedOptions, QueryIterator, Resource } from "@azure/cosmos"; +import { client } from "../CosmosClient"; + +export const queryConflicts = ( + databaseId: string, + containerId: string, + query: string, + options: FeedOptions +): QueryIterator => { + return client() + .database(databaseId) + .container(containerId) + .conflicts.query(query, options); +}; diff --git a/src/Common/DataAccessUtilityBase.test.ts b/src/Common/dataAccess/queryDocuments.test.ts similarity index 73% rename from src/Common/DataAccessUtilityBase.test.ts rename to src/Common/dataAccess/queryDocuments.test.ts index 339d82aca..38565a3df 100644 --- a/src/Common/DataAccessUtilityBase.test.ts +++ b/src/Common/dataAccess/queryDocuments.test.ts @@ -1,13 +1,13 @@ -import { getCommonQueryOptions } from "./DataAccessUtilityBase"; -import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; - -describe("getCommonQueryOptions", () => { - it("builds the correct default options objects", () => { - expect(getCommonQueryOptions({})).toMatchSnapshot(); - }); - it("reads from localStorage", () => { - LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, 37); - LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, 17); - expect(getCommonQueryOptions({})).toMatchSnapshot(); - }); -}); +import { getCommonQueryOptions } from "./queryDocuments"; +import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; + +describe("getCommonQueryOptions", () => { + it("builds the correct default options objects", () => { + expect(getCommonQueryOptions({})).toMatchSnapshot(); + }); + it("reads from localStorage", () => { + LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, 37); + LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, 17); + expect(getCommonQueryOptions({})).toMatchSnapshot(); + }); +}); diff --git a/src/Common/dataAccess/queryDocuments.ts b/src/Common/dataAccess/queryDocuments.ts new file mode 100644 index 000000000..0436b756c --- /dev/null +++ b/src/Common/dataAccess/queryDocuments.ts @@ -0,0 +1,34 @@ +import { Queries } from "../Constants"; +import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; +import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; +import { client } from "../CosmosClient"; + +export const queryDocuments = ( + databaseId: string, + containerId: string, + query: string, + options: FeedOptions +): QueryIterator => { + options = getCommonQueryOptions(options); + return client() + .database(databaseId) + .container(containerId) + .items.query(query, options); +}; + +export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => { + const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage); + options = options || {}; + options.populateQueryMetrics = true; + options.enableScanInQuery = options.enableScanInQuery || true; + if (!options.partitionKey) { + options.forceQueryPlan = true; + } + options.maxItemCount = + options.maxItemCount || + (storedItemPerPageSetting !== undefined && storedItemPerPageSetting) || + Queries.itemsPerPage; + options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism); + + return options; +}; diff --git a/src/Common/dataAccess/queryDocumentsPage.ts b/src/Common/dataAccess/queryDocumentsPage.ts new file mode 100644 index 000000000..064e4126f --- /dev/null +++ b/src/Common/dataAccess/queryDocumentsPage.ts @@ -0,0 +1,26 @@ +import { QueryResults } from "../../Contracts/ViewModels"; +import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; +import { MinimalQueryIterator, nextPage } from "../IteratorUtilities"; +import { handleError } from "../ErrorHandlingUtils"; +import { getEntityName } from "../DocumentUtility"; + +export const queryDocumentsPage = async ( + resourceName: string, + documentsIterator: MinimalQueryIterator, + firstItemIndex: number +): Promise => { + const entityName = getEntityName(); + const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`); + + try { + const result: QueryResults = await nextPage(documentsIterator, firstItemIndex); + const itemCount = (result.documents && result.documents.length) || 0; + logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`); + return result; + } catch (error) { + handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`); + throw error; + } finally { + clearMessage(); + } +}; diff --git a/src/Common/dataAccess/readDocument.ts b/src/Common/dataAccess/readDocument.ts new file mode 100644 index 000000000..d399f25f0 --- /dev/null +++ b/src/Common/dataAccess/readDocument.ts @@ -0,0 +1,27 @@ +import { Item } from "@azure/cosmos"; +import { CollectionBase } from "../../Contracts/ViewModels"; +import { client } from "../CosmosClient"; +import { getEntityName } from "../DocumentUtility"; +import { handleError } from "../ErrorHandlingUtils"; +import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; +import DocumentId from "../../Explorer/Tree/DocumentId"; + +export const readDocument = async (collection: CollectionBase, documentId: DocumentId): Promise => { + const entityName = getEntityName(); + const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`); + + try { + const response = await client() + .database(collection.databaseId) + .container(collection.id()) + .item(documentId.id(), documentId.partitionKeyValue) + .read(); + + return response?.resource; + } catch (error) { + handleError(error, "ReadDocument", `Failed to read ${entityName} ${documentId.id()}`); + throw error; + } finally { + clearMessage(); + } +}; diff --git a/src/Common/dataAccess/updateDocument.ts b/src/Common/dataAccess/updateDocument.ts new file mode 100644 index 000000000..9e1b50fd9 --- /dev/null +++ b/src/Common/dataAccess/updateDocument.ts @@ -0,0 +1,32 @@ +import { CollectionBase } from "../../Contracts/ViewModels"; +import { Item } from "@azure/cosmos"; +import { client } from "../CosmosClient"; +import { getEntityName } from "../DocumentUtility"; +import { handleError } from "../ErrorHandlingUtils"; +import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; +import DocumentId from "../../Explorer/Tree/DocumentId"; + +export const updateDocument = async ( + collection: CollectionBase, + documentId: DocumentId, + newDocument: Item +): Promise => { + const entityName = getEntityName(); + const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`); + + try { + const response = await client() + .database(collection.databaseId) + .container(collection.id()) + .item(documentId.id(), documentId.partitionKeyValue) + .replace(newDocument); + + logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`); + return response?.resource; + } catch (error) { + handleError(error, "UpdateDocument", `Failed to update ${entityName} ${documentId.id()}`); + throw error; + } finally { + clearMessage(); + } +}; diff --git a/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts b/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts index d64d84152..838e829a4 100644 --- a/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts +++ b/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts @@ -1,11 +1,11 @@ -jest.mock("../../Common/DocumentClientUtilityBase"); jest.mock("../Graph/GraphExplorerComponent/GremlinClient"); jest.mock("../../Common/dataAccess/createCollection"); +jest.mock("../../Common/dataAccess/createDocument"); import * as ko from "knockout"; import * as ViewModels from "../../Contracts/ViewModels"; import Q from "q"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; -import { createDocument } from "../../Common/DocumentClientUtilityBase"; +import { createDocument } from "../../Common/dataAccess/createDocument"; import Explorer from "../Explorer"; import { updateUserContext } from "../../UserContext"; diff --git a/src/Explorer/DataSamples/ContainerSampleGenerator.ts b/src/Explorer/DataSamples/ContainerSampleGenerator.ts index 3c3b28732..b115a98b1 100644 --- a/src/Explorer/DataSamples/ContainerSampleGenerator.ts +++ b/src/Explorer/DataSamples/ContainerSampleGenerator.ts @@ -4,8 +4,8 @@ import GraphTab from ".././Tabs/GraphTab"; import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import Explorer from "../Explorer"; -import { createDocument } from "../../Common/DocumentClientUtilityBase"; import { createCollection } from "../../Common/dataAccess/createCollection"; +import { createDocument } from "../../Common/dataAccess/createDocument"; import { userContext } from "../../UserContext"; interface SampleDataFile extends DataModels.CreateCollectionParams { @@ -95,12 +95,15 @@ export class ContainerSampleGenerator { .reduce((previous, current) => previous.then(current), Promise.resolve()); } else { // For SQL all queries are executed at the same time - this.sampleDataFile.data.map(doc => { - const subPromise = createDocument(collection, doc); - subPromise.catch(reason => NotificationConsoleUtils.logConsoleError(reason)); - promises.push(subPromise); - }); - await Promise.all(promises); + await Promise.all( + this.sampleDataFile.data.map(async doc => { + try { + await createDocument(collection, doc); + } catch (error) { + NotificationConsoleUtils.logConsoleError(error); + } + }) + ); } } diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx index 17c5d963c..c381dc8c8 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx @@ -1,4 +1,5 @@ -jest.mock("../../../Common/DocumentClientUtilityBase"); +jest.mock("../../../Common/dataAccess/queryDocuments"); +jest.mock("../../../Common/dataAccess/queryDocumentsPage"); import React from "react"; import * as sinon from "sinon"; import { mount, ReactWrapper } from "enzyme"; @@ -12,7 +13,8 @@ import * as DataModels from "../../../Contracts/DataModels"; import * as StorageUtility from "../../../Shared/StorageUtility"; import GraphTab from "../../Tabs/GraphTab"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; -import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase"; +import { queryDocuments } from "../../../Common/dataAccess/queryDocuments"; +import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage"; describe("Check whether query result is vertex array", () => { it("should reject null as vertex array", () => { @@ -299,12 +301,12 @@ describe("GraphExplorer", () => { ignoreD3Update: boolean ): GraphExplorer => { (queryDocuments as jest.Mock).mockImplementation((container: any, query: string, options: any) => { - return Q.resolve({ + return { _query: query, nextItem: (callback: (error: any, document: DataModels.DocumentId) => void): void => {}, hasMoreResults: () => false, executeNext: (callback: (error: any, documents: DataModels.DocumentId[], headers: any) => void): void => {} - }); + }; }); (queryDocumentsPage as jest.Mock).mockImplementation( (rid: string, iterator: any, firstItemIndex: number, options: any) => { diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx index 974ec3a8f..af2d90059 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx @@ -28,8 +28,10 @@ import * as Constants from "../../../Common/Constants"; import { InputProperty } from "../../../Contracts/ViewModels"; import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos"; import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif"; -import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase"; +import { queryDocuments } from "../../../Common/dataAccess/queryDocuments"; +import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage"; import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; +import { FeedOptions } from "@azure/cosmos"; export interface GraphAccessor { applyFilter: () => void; @@ -725,26 +727,32 @@ export class GraphExplorer extends React.Component { - // TODO maxItemCount: this reduces throttling, but won't cap the # of results - return queryDocuments(this.props.databaseId, this.props.collectionId, query, { - maxItemCount: GraphExplorer.PAGE_ALL, - enableCrossPartitionQuery: - StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.IsCrossPartitionQueryEnabled) === - "true" - }).then( - (iterator: QueryIterator) => { - return iterator.fetchNext().then(response => response.resources); - }, - (reason: any) => { - GraphExplorer.reportToConsole( - ConsoleDataType.Error, - `Failed to execute non-paged query ${query}. Reason:${reason}`, - reason - ); - return null; - } - ); + public async executeNonPagedDocDbQuery(query: string): Promise { + try { + // TODO maxItemCount: this reduces throttling, but won't cap the # of results + const iterator: QueryIterator = queryDocuments( + this.props.databaseId, + this.props.collectionId, + query, + { + maxItemCount: GraphExplorer.PAGE_ALL, + enableCrossPartitionQuery: + StorageUtility.LocalStorageUtility.getEntryString( + StorageUtility.StorageKey.IsCrossPartitionQueryEnabled + ) === "true" + } as FeedOptions + ); + const response = await iterator.fetchNext(); + + return response?.resources; + } catch (error) { + GraphExplorer.reportToConsole( + ConsoleDataType.Error, + `Failed to execute non-paged query ${query}. Reason:${error}`, + error + ); + return null; + } } /** @@ -864,7 +872,7 @@ export class GraphExplorer extends React.Component { // Clear any progress indicator this.executeCounter = 0; this.setState({ @@ -882,24 +890,22 @@ export class GraphExplorer extends React.Component (this.queryTotalRequestCharge = result.requestCharge), - (error: any) => { - const errorMsg = `Failure in submitting query: ${query}: ${getErrorMessage(error)}`; - GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); - this.setState({ - filterQueryError: errorMsg - }); + try { + let result: UserQueryResult; + if (query.toLocaleLowerCase() === "g.V()".toLocaleLowerCase()) { + result = await this.executeDocDbGVQuery(); + } else { + result = await this.executeGremlinQuery(query); } - ); + + this.queryTotalRequestCharge = result.requestCharge; + } catch (error) { + const errorMsg = `Failure in submitting query: ${query}: ${getErrorMessage(error)}`; + GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); + this.setState({ + filterQueryError: errorMsg + }); + } } /** @@ -1390,7 +1396,7 @@ export class GraphExplorer extends React.Component { + private updatePossibleVertices(): Promise { const highlightedNodeId = this.state.highlightedNode ? this.state.highlightedNode.id : null; const q = `SELECT c.id, c["${this.props.graphConfigUiData.nodeCaptionChoice() || @@ -1721,85 +1727,81 @@ export class GraphExplorer extends React.Component { + private async executeDocDbGVQuery(): Promise { let query = "select root.id from root where IS_DEFINED(root._isEdge) = false order by root._ts desc"; if (this.props.collectionPartitionKeyProperty) { query = `select root.id, root.${this.props.collectionPartitionKeyProperty} from root where IS_DEFINED(root._isEdge) = false order by root._ts asc`; } - return queryDocuments(this.props.databaseId, this.props.collectionId, query, { - maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE, - enableCrossPartitionQuery: LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true" - }) - .then( - (iterator: QueryIterator) => { - this.currentDocDBQueryInfo = { - iterator: iterator, - index: 0, - query: query - }; - }, - (reason: any) => { - GraphExplorer.reportToConsole( - ConsoleDataType.Error, - `Failed to execute CosmosDB query: ${query} reason:${reason}` - ); - } - ) - .then(() => this.loadMoreRootNodes()); + try { + const iterator: QueryIterator = queryDocuments( + this.props.databaseId, + this.props.collectionId, + query, + { + maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE, + enableCrossPartitionQuery: + LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true" + } as FeedOptions + ); + this.currentDocDBQueryInfo = { + iterator: iterator, + index: 0, + query: query + }; + return await this.loadMoreRootNodes(); + } catch (error) { + GraphExplorer.reportToConsole( + ConsoleDataType.Error, + `Failed to execute CosmosDB query: ${query} reason:${error}` + ); + throw error; + } } - private loadMoreRootNodes(): Q.Promise { + private async loadMoreRootNodes(): Promise { if (!this.currentDocDBQueryInfo) { - return Q.resolve(null); + return undefined; } - let RU: string = GraphExplorer.REQUEST_CHARGE_UNKNOWN_MSG; + let RU: string = GraphExplorer.REQUEST_CHARGE_UNKNOWN_MSG; const queryInfoStr = `${this.currentDocDBQueryInfo.query} (${this.currentDocDBQueryInfo.index + 1}-${this .currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE})`; const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`); - return queryDocumentsPage( - this.props.collectionId, - this.currentDocDBQueryInfo.iterator, - this.currentDocDBQueryInfo.index, - { - enableCrossPartitionQuery: - LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true" - } - ) - .then((results: ViewModels.QueryResults) => { - GraphExplorer.clearConsoleProgress(id); - this.currentDocDBQueryInfo.index = results.lastItemIndex + 1; - this.setState({ hasMoreRoots: results.hasMoreResults }); - RU = results.requestCharge.toString(); - GraphExplorer.reportToConsole( - ConsoleDataType.Info, - `Executed: ${queryInfoStr} ${GremlinClient.GremlinClient.getRequestChargeString(RU)}` - ); - const documents = results.documents || []; - return documents.map( - (item: DataModels.DocumentId) => { - return GraphExplorer.getPkIdFromDocumentId(item, this.props.collectionPartitionKeyProperty); - }, - (reason: any) => { - // Failure - GraphExplorer.clearConsoleProgress(id); - const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${reason}`; - GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); - this.setState({ - filterQueryError: errorMsg - }); - this.setFilterQueryStatus(FilterQueryStatus.ErrorResult); - throw reason; - } - ); - }) - .then((pkIds: string[]) => { - const arg = pkIds.join(","); - return this.executeGremlinQuery(`g.V(${arg})`); - }) - .then(() => ({ requestCharge: RU })); + try { + const results: ViewModels.QueryResults = await queryDocumentsPage( + this.props.collectionId, + this.currentDocDBQueryInfo.iterator, + this.currentDocDBQueryInfo.index + ); + + GraphExplorer.clearConsoleProgress(id); + this.currentDocDBQueryInfo.index = results.lastItemIndex + 1; + this.setState({ hasMoreRoots: results.hasMoreResults }); + RU = results.requestCharge.toString(); + GraphExplorer.reportToConsole( + ConsoleDataType.Info, + `Executed: ${queryInfoStr} ${GremlinClient.GremlinClient.getRequestChargeString(RU)}` + ); + const pkIds: string[] = (results.documents || []).map((item: DataModels.DocumentId) => + GraphExplorer.getPkIdFromDocumentId(item, this.props.collectionPartitionKeyProperty) + ); + + const arg = pkIds.join(","); + await this.executeGremlinQuery(`g.V(${arg})`); + + return { requestCharge: RU }; + } catch (error) { + GraphExplorer.clearConsoleProgress(id); + const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${getErrorMessage(error)}`; + GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); + this.setState({ + filterQueryError: errorMsg + }); + this.setFilterQueryStatus(FilterQueryStatus.ErrorResult); + throw error; + } } private executeGremlinQuery(query: string): Q.Promise { diff --git a/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts b/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts index 1991d7012..668ec71db 100644 --- a/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts +++ b/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts @@ -421,53 +421,47 @@ export default class TableEntityListViewModel extends DataTableViewModel { * Note that this also means that we can get less entities than the requested download size in a successful call. * See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx */ - private prefetchData( + private async prefetchData( tableQuery: Entities.ITableQuery, downloadSize: number, currentRetry: number = 0 - ): Q.Promise { + ): Promise { if (!this.cache.serverCallInProgress) { this.cache.serverCallInProgress = true; this.allDownloaded = false; this.lastPrefetchTime = new Date().getTime(); - var time = this.lastPrefetchTime; + const time = this.lastPrefetchTime; - var promise: Q.Promise; if (this._documentIterator && this.continuationToken) { // TODO handle Cassandra case + const response = await this._documentIterator.fetchNext(); + const entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(response?.resources); - promise = Q(this._documentIterator.fetchNext().then(response => response.resources)).then( - (documents: any[]) => { - let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents); - let finalEntities: IListTableEntitiesSegmentedResult = { - Results: entities, - ContinuationToken: this._documentIterator.hasMoreResults() - }; - return Q.resolve(finalEntities); - } - ); - } else if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) { - promise = this.queryTablesTab.container.tableDataClient.queryDocuments( - this.queryTablesTab.collection, - this.cqlQuery(), - true, - this.continuationToken - ); - } else { - let query = this.sqlQuery(); - if (this.queryTablesTab.container.isPreferredApiCassandra()) { - query = this.cqlQuery(); - } - promise = this.queryTablesTab.container.tableDataClient.queryDocuments( - this.queryTablesTab.collection, - query, - true - ); + return { + Results: entities, + ContinuationToken: this._documentIterator.hasMoreResults() + }; } - return promise - .then((result: IListTableEntitiesSegmentedResult) => { + + try { + let documents: IListTableEntitiesSegmentedResult; + if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) { + documents = await this.queryTablesTab.container.tableDataClient.queryDocuments( + this.queryTablesTab.collection, + this.cqlQuery(), + true, + this.continuationToken + ); + } else { + const query = this.queryTablesTab.container.isPreferredApiCassandra() ? this.cqlQuery() : this.sqlQuery(); + documents = await this.queryTablesTab.container.tableDataClient.queryDocuments( + this.queryTablesTab.collection, + query, + true + ); + if (!this._documentIterator) { - this._documentIterator = result.iterator; + this._documentIterator = documents.iterator; } var actualDownloadSize: number = 0; @@ -478,11 +472,11 @@ export default class TableEntityListViewModel extends DataTableViewModel { return Q.resolve(null); } - var entities = result.Results; + var entities = documents.Results; actualDownloadSize = entities.length; // Queries can fetch no results and still return a continuation header. See prefetchAndRender() method. - this.continuationToken = this.isCancelled ? null : result.ContinuationToken; + this.continuationToken = this.isCancelled ? null : documents.ContinuationToken; if (!this.continuationToken) { this.allDownloaded = true; @@ -514,20 +508,22 @@ export default class TableEntityListViewModel extends DataTableViewModel { // For #2.1, set prefetch exceeds maximum retry number and end prefetch. // For #2.2, go to next round prefetch. if (this.allDownloaded || nextDownloadSize === 0) { - return Q.resolve(result); + return documents; } if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) { - result.ExceedMaximumRetries = true; - return Q.resolve(result); + documents.ExceedMaximumRetries = true; + return documents; } - return this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1); - }) - .catch((error: Error) => { - this.cache.serverCallInProgress = false; - return Q.reject(error); - }); + + return await this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1); + } + } catch (error) { + this.cache.serverCallInProgress = false; + throw error; + } } - return null; + + return undefined; } } diff --git a/src/Explorer/Tables/TableDataClient.ts b/src/Explorer/Tables/TableDataClient.ts index ba6ae246d..b90073ef9 100644 --- a/src/Explorer/Tables/TableDataClient.ts +++ b/src/Explorer/Tables/TableDataClient.ts @@ -4,6 +4,7 @@ import Q from "q"; import { displayTokenRenewalPromptForStatus, getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; import { AuthType } from "../../AuthType"; import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; +import { FeedOptions } from "@azure/cosmos"; import * as Constants from "../../Common/Constants"; import * as Entities from "./Entities"; import * as HeadersUtility from "../../Common/HeadersUtility"; @@ -12,9 +13,12 @@ import * as TableConstants from "./Constants"; import * as TableEntityProcessor from "./TableEntityProcessor"; import * as ViewModels from "../../Contracts/ViewModels"; import Explorer from "../Explorer"; -import { queryDocuments, deleteDocument, updateDocument, createDocument } from "../../Common/DocumentClientUtilityBase"; import { configContext } from "../../ConfigContext"; import { handleError } from "../../Common/ErrorHandlingUtils"; +import { createDocument } from "../../Common/dataAccess/createDocument"; +import { deleteDocument } from "../../Common/dataAccess/deleteDocument"; +import { queryDocuments } from "../../Common/dataAccess/queryDocuments"; +import { updateDocument } from "../../Common/dataAccess/updateDocument"; export interface CassandraTableKeys { partitionKeys: CassandraTableKey[]; @@ -38,19 +42,19 @@ export abstract class TableDataClient { collection: ViewModels.Collection, originalDocument: any, newEntity: Entities.ITableEntity - ): Q.Promise; + ): Promise; public abstract queryDocuments( collection: ViewModels.Collection, query: string, shouldNotify?: boolean, paginationToken?: string - ): Q.Promise; + ): Promise; public abstract deleteDocuments( collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[] - ): Q.Promise; + ): Promise; } export class TablesAPIDataClient extends TableDataClient { @@ -74,77 +78,63 @@ export class TablesAPIDataClient extends TableDataClient { return deferred.promise; } - public updateDocument( + public async updateDocument( collection: ViewModels.Collection, originalDocument: any, entity: Entities.ITableEntity - ): Q.Promise { - const deferred = Q.defer(); - - updateDocument( - collection, - originalDocument, - TableEntityProcessor.convertEntityToNewDocument(entity) - ).then( - (newDocument: any) => { - const newEntity = TableEntityProcessor.convertDocumentsToEntities([newDocument])[0]; - deferred.resolve(newEntity); - }, - reason => { - deferred.reject(reason); - } - ); - return deferred.promise; + ): Promise { + try { + const newDocument = await updateDocument( + collection, + originalDocument, + TableEntityProcessor.convertEntityToNewDocument(entity) + ); + return TableEntityProcessor.convertDocumentsToEntities([newDocument])[0]; + } catch (error) { + handleError(error, "TablesAPIDataClient/updateDocument"); + throw error; + } } - public queryDocuments( + public async queryDocuments( collection: ViewModels.Collection, query: string - ): Q.Promise { - const deferred = Q.defer(); + ): Promise { + try { + const options = { + enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey() + } as FeedOptions; + const iterator = queryDocuments(collection.databaseId, collection.id(), query, options); + const response = await iterator.fetchNext(); + const documents = response?.resources; + const entities = TableEntityProcessor.convertDocumentsToEntities(documents); - let options: any = {}; - options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); - queryDocuments(collection.databaseId, collection.id(), query, options).then( - iterator => { - iterator - .fetchNext() - .then(response => response.resources) - .then( - (documents: any[] = []) => { - let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents); - let finalEntities: Entities.IListTableEntitiesResult = { - Results: entities, - ContinuationToken: iterator.hasMoreResults(), - iterator: iterator - }; - deferred.resolve(finalEntities); - }, - reason => { - deferred.reject(reason); - } - ); - }, - reason => { - deferred.reject(reason); - } - ); - return deferred.promise; + return { + Results: entities, + ContinuationToken: iterator.hasMoreResults(), + iterator: iterator + }; + } catch (error) { + handleError(error, "TablesAPIDataClient/queryDocuments", "Query documents failed"); + throw error; + } } - public deleteDocuments(collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[]): Q.Promise { - let documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments( + public async deleteDocuments( + collection: ViewModels.Collection, + entitiesToDelete: Entities.ITableEntity[] + ): Promise { + const documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments( entitiesToDelete, collection ); - let promiseArray: Q.Promise[] = []; - documentsToDelete && - documentsToDelete.forEach(document => { + + await Promise.all( + documentsToDelete?.map(async document => { document.id = ko.observable(document.id); - let promise: Q.Promise = deleteDocument(collection, document); - promiseArray.push(promise); - }); - return Q.all(promiseArray); + await deleteDocument(collection, document); + }) + ); } } @@ -180,10 +170,7 @@ export class CassandraAPIDataClient extends TableDataClient { (data: any) => { entity[TableConstants.EntityKeyNames.RowKey] = entity[this.getCassandraPartitionKeyProperty(collection)]; entity[TableConstants.EntityKeyNames.RowKey]._ = entity[TableConstants.EntityKeyNames.RowKey]._.toString(); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully added new row to table ${collection.id()}` - ); + NotificationConsoleUtils.logConsoleInfo(`Successfully added new row to table ${collection.id()}`); deferred.resolve(entity); }, error => { @@ -197,181 +184,149 @@ export class CassandraAPIDataClient extends TableDataClient { return deferred.promise; } - public updateDocument( + public async updateDocument( collection: ViewModels.Collection, originalDocument: any, newEntity: Entities.ITableEntity - ): Q.Promise { - const notificationId = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Updating row ${originalDocument.RowKey._}` - ); - const deferred = Q.defer(); - let promiseArray: Q.Promise[] = []; - let query = `UPDATE ${collection.databaseId}.${collection.id()}`; - let isChange: boolean = false; - for (let property in newEntity) { - if (!originalDocument[property] || newEntity[property]._.toString() !== originalDocument[property]._.toString()) { - if (this.isStringType(newEntity[property].$)) { - query = `${query} SET ${property} = '${newEntity[property]._}',`; - } else { - query = `${query} SET ${property} = ${newEntity[property]._},`; + ): Promise { + const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Updating row ${originalDocument.RowKey._}`); + + try { + let whereSegment = " WHERE"; + let keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat( + collection.cassandraKeys.clusteringKeys + ); + for (let keyIndex in keys) { + const key = keys[keyIndex].property; + const keyType = keys[keyIndex].type; + whereSegment += this.isStringType(keyType) + ? ` ${key} = '${newEntity[key]._}' AND` + : ` ${key} = ${newEntity[key]._} AND`; + } + whereSegment = whereSegment.slice(0, whereSegment.length - 4); + + let updateQuery = `UPDATE ${collection.databaseId}.${collection.id()}`; + let isPropertyUpdated = false; + for (let property in newEntity) { + if ( + !originalDocument[property] || + newEntity[property]._.toString() !== originalDocument[property]._.toString() + ) { + updateQuery += this.isStringType(newEntity[property].$) + ? ` SET ${property} = '${newEntity[property]._}',` + : ` SET ${property} = ${newEntity[property]._},`; + isPropertyUpdated = true; } - isChange = true; } - } - query = query.slice(0, query.length - 1); - let whereSegment = " WHERE"; - let keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat( - collection.cassandraKeys.clusteringKeys - ); - for (let keyIndex in keys) { - const key = keys[keyIndex].property; - const keyType = keys[keyIndex].type; - if (this.isStringType(keyType)) { - whereSegment = `${whereSegment} ${key} = '${newEntity[key]._}' AND`; - } else { - whereSegment = `${whereSegment} ${key} = ${newEntity[key]._} AND`; + + if (isPropertyUpdated) { + updateQuery = updateQuery.slice(0, updateQuery.length - 1); + updateQuery += whereSegment; + await this.queryDocuments(collection, updateQuery); } - } - whereSegment = whereSegment.slice(0, whereSegment.length - 4); - query = query + whereSegment; - if (isChange) { - promiseArray.push(this.queryDocuments(collection, query)); - } - query = `DELETE `; - for (let property in originalDocument) { - if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) { - query = `${query} ${property},`; - } - } - if (query.length > 7) { - query = query.slice(0, query.length - 1); - query = `${query} FROM ${collection.databaseId}.${collection.id()}${whereSegment}`; - promiseArray.push(this.queryDocuments(collection, query)); - } - Q.all(promiseArray) - .then( - (data: any) => { - newEntity[TableConstants.EntityKeyNames.RowKey] = originalDocument[TableConstants.EntityKeyNames.RowKey]; - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully updated row ${newEntity.RowKey._}` - ); - deferred.resolve(newEntity); - }, - error => { - handleError(error, "UpdateRowCassandra", `Failed to update row ${newEntity.RowKey._}`); - deferred.reject(error); + + let deleteQuery = `DELETE `; + let isPropertyDeleted = false; + for (let property in originalDocument) { + if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) { + deleteQuery += ` ${property},`; + isPropertyDeleted = true; } - ) - .finally(() => { - NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); - }); - return deferred.promise; + } + + if (isPropertyDeleted) { + deleteQuery = deleteQuery.slice(0, deleteQuery.length - 1); + deleteQuery += ` FROM ${collection.databaseId}.${collection.id()}${whereSegment}`; + await this.queryDocuments(collection, deleteQuery); + } + + newEntity[TableConstants.EntityKeyNames.RowKey] = originalDocument[TableConstants.EntityKeyNames.RowKey]; + NotificationConsoleUtils.logConsoleInfo(`Successfully updated row ${newEntity.RowKey._}`); + return newEntity; + } catch (error) { + handleError(error, "UpdateRowCassandra", "Failed to update row ${newEntity.RowKey._}"); + throw error; + } finally { + clearMessage(); + } } - public queryDocuments( + public async queryDocuments( collection: ViewModels.Collection, query: string, shouldNotify?: boolean, paginationToken?: string - ): Q.Promise { - let notificationId: string; - if (shouldNotify) { - notificationId = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Querying rows for table ${collection.id()}` - ); - } - const deferred = Q.defer(); - const authType = window.authType; - const apiEndpoint: string = - authType === AuthType.EncryptedToken - ? Constants.CassandraBackend.guestQueryApi - : Constants.CassandraBackend.queryApi; - $.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, { - type: "POST", - data: { - accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name, - cassandraEndpoint: this.trimCassandraEndpoint( - collection.container.databaseAccount().properties.cassandraEndpoint - ), - resourceId: collection.container.databaseAccount().id, - keyspaceId: collection.databaseId, - tableId: collection.id(), - query: query, - paginationToken: paginationToken - }, - beforeSend: this.setAuthorizationHeader, - error: this.handleAjaxError, - cache: false - }) - .then( - (data: any) => { - if (shouldNotify) { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully fetched ${data.result.length} rows for table ${collection.id()}` - ); - } - deferred.resolve({ - Results: data.result, - ContinuationToken: data.paginationToken - }); + ): Promise { + const clearMessage = + shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`); + try { + const authType = window.authType; + const apiEndpoint: string = + authType === AuthType.EncryptedToken + ? Constants.CassandraBackend.guestQueryApi + : Constants.CassandraBackend.queryApi; + const data: any = await $.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, { + type: "POST", + data: { + accountName: + collection && collection.container.databaseAccount && collection.container.databaseAccount().name, + cassandraEndpoint: this.trimCassandraEndpoint( + collection.container.databaseAccount().properties.cassandraEndpoint + ), + resourceId: collection.container.databaseAccount().id, + keyspaceId: collection.databaseId, + tableId: collection.id(), + query, + paginationToken }, - (error: any) => { - if (shouldNotify) { - handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`); - } - deferred.reject(error); - } - ) - .done(() => { - if (shouldNotify) { - NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); - } + beforeSend: this.setAuthorizationHeader, + error: this.handleAjaxError, + cache: false }); - return deferred.promise; + shouldNotify && + NotificationConsoleUtils.logConsoleInfo( + `Successfully fetched ${data.result.length} rows for table ${collection.id()}` + ); + return { + Results: data.result, + ContinuationToken: data.paginationToken + }; + } catch (error) { + shouldNotify && + handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`); + throw error; + } finally { + clearMessage?.(); + } } - public deleteDocuments(collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[]): Q.Promise { + public async deleteDocuments( + collection: ViewModels.Collection, + entitiesToDelete: Entities.ITableEntity[] + ): Promise { const query = `DELETE FROM ${collection.databaseId}.${collection.id()} WHERE `; - let promiseArray: Q.Promise[] = []; - let partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection); - for (let i = 0, len = entitiesToDelete.length; i < len; i++) { - let currEntityToDelete: Entities.ITableEntity = entitiesToDelete[i]; - let currQuery = query; - let partitionKeyValue = currEntityToDelete[partitionKeyProperty]; - if (partitionKeyValue._ != null && this.isStringType(partitionKeyValue.$)) { - currQuery = `${currQuery}${partitionKeyProperty} = '${partitionKeyValue._}' AND `; - } else { - currQuery = `${currQuery}${partitionKeyProperty} = ${partitionKeyValue._} AND `; - } - currQuery = currQuery.slice(0, currQuery.length - 5); - const notificationId = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Deleting row ${currEntityToDelete.RowKey._}` - ); - promiseArray.push( - this.queryDocuments(collection, currQuery) - .then( - () => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully deleted row ${currEntityToDelete.RowKey._}` - ); - }, - error => { - handleError(error, "DeleteRowCassandra", `Error while deleting row ${currEntityToDelete.RowKey._}`); - } - ) - .finally(() => { - NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); - }) - ); - } - return Q.all(promiseArray); + const partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection); + + await Promise.all( + entitiesToDelete.map(async (currEntityToDelete: Entities.ITableEntity) => { + const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting row ${currEntityToDelete.RowKey._}`); + const partitionKeyValue = currEntityToDelete[partitionKeyProperty]; + const currQuery = + query + this.isStringType(partitionKeyValue.$) + ? `${partitionKeyProperty} = '${partitionKeyValue._}'` + : `${partitionKeyProperty} = ${partitionKeyValue._}`; + + try { + await this.queryDocuments(collection, currQuery); + NotificationConsoleUtils.logConsoleInfo(`Successfully deleted row ${currEntityToDelete.RowKey._}`); + } catch (error) { + handleError(error, "DeleteRowCassandra", `Error while deleting row ${currEntityToDelete.RowKey._}`); + throw error; + } finally { + clearMessage(); + } + }) + ); } public createKeyspace( diff --git a/src/Explorer/Tabs/ConflictsTab.ts b/src/Explorer/Tabs/ConflictsTab.ts index bbe5370c0..abad6ea4e 100644 --- a/src/Explorer/Tabs/ConflictsTab.ts +++ b/src/Explorer/Tabs/ConflictsTab.ts @@ -16,18 +16,16 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import SaveIcon from "../../../images/save-cosmos.svg"; import DiscardIcon from "../../../images/discard.svg"; import DeleteIcon from "../../../images/delete.svg"; -import { QueryIterator, ItemDefinition, Resource, ConflictDefinition } from "@azure/cosmos"; +import { QueryIterator, Resource, ConflictDefinition, FeedOptions } from "@azure/cosmos"; import { MinimalQueryIterator } from "../../Common/IteratorUtilities"; import Explorer from "../Explorer"; -import { - queryConflicts, - deleteConflict, - deleteDocument, - createDocument, - updateDocument -} from "../../Common/DocumentClientUtilityBase"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; +import { createDocument } from "../../Common/dataAccess/createDocument"; +import { deleteDocument } from "../../Common/dataAccess/deleteDocument"; +import { updateDocument } from "../../Common/dataAccess/updateDocument"; +import { deleteConflict } from "../../Common/dataAccess/deleteConflict"; +import { queryConflicts } from "../../Common/dataAccess/queryConflicts"; export default class ConflictsTab extends TabsBase { public selectedConflictId: ko.Observable; @@ -225,25 +223,15 @@ export default class ConflictsTab extends TabsBase { }); } - public refreshDocumentsGrid(): Q.Promise { - // clear documents grid - this.conflictIds([]); - return this.createIterator() - .then( - // reset iterator - iterator => { - this._documentsIterator = iterator; - } - ) - .then( - // load documents - () => { - return this.loadNextPage(); - } - ) - .catch(error => { - window.alert(getErrorMessage(error)); - }); + public async refreshDocumentsGrid(): Promise { + try { + // clear documents grid + this.conflictIds([]); + this._documentsIterator = this.createIterator(); + await this.loadNextPage(); + } catch (error) { + window.alert(getErrorMessage(error)); + } } public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => { @@ -265,9 +253,9 @@ export default class ConflictsTab extends TabsBase { return Q(); } - public onAcceptChangesClick = (): Q.Promise => { + public onAcceptChangesClick = async (): Promise => { if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) { - return Q(); + return; } this.isExecutionError(false); @@ -285,81 +273,79 @@ export default class ConflictsTab extends TabsBase { conflictResourceId: selectedConflict.resourceId }); - let operationPromise: Q.Promise = Q(); - if (selectedConflict.operationType === Constants.ConflictOperationType.Replace) { - const documentContent = JSON.parse(this.selectedConflictContent()); + try { + if (selectedConflict.operationType === Constants.ConflictOperationType.Replace) { + const documentContent = JSON.parse(this.selectedConflictContent()); - operationPromise = updateDocument( - this.collection, - selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]), - documentContent - ); - } + await updateDocument( + this.collection, + selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]), + documentContent + ); + } - if (selectedConflict.operationType === Constants.ConflictOperationType.Create) { - const documentContent = JSON.parse(this.selectedConflictContent()); + if (selectedConflict.operationType === Constants.ConflictOperationType.Create) { + const documentContent = JSON.parse(this.selectedConflictContent()); - operationPromise = createDocument(this.collection, documentContent); - } + await createDocument(this.collection, documentContent); + } - if (selectedConflict.operationType === Constants.ConflictOperationType.Delete && !!this.selectedConflictContent()) { - const documentContent = JSON.parse(this.selectedConflictContent()); + if ( + selectedConflict.operationType === Constants.ConflictOperationType.Delete && + !!this.selectedConflictContent() + ) { + const documentContent = JSON.parse(this.selectedConflictContent()); - operationPromise = deleteDocument( - this.collection, - selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]) - ); - } + await deleteDocument( + this.collection, + selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]) + ); + } - return operationPromise - .then( - () => { - return deleteConflict(this.collection, selectedConflict).then(() => { - this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid); - this.selectedConflictContent(""); - this.selectedConflictCurrent(""); - this.selectedConflictId(null); - this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); - TelemetryProcessor.traceSuccess( - Action.ResolveConflict, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - conflictResourceType: selectedConflict.resourceType, - conflictOperationType: selectedConflict.operationType, - conflictResourceId: selectedConflict.resourceId - }, - startKey - ); - }); + await deleteConflict(this.collection, selectedConflict); + this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid); + this.selectedConflictContent(""); + this.selectedConflictCurrent(""); + this.selectedConflictId(null); + this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); + TelemetryProcessor.traceSuccess( + Action.ResolveConflict, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + conflictResourceType: selectedConflict.resourceType, + conflictOperationType: selectedConflict.operationType, + conflictResourceId: selectedConflict.resourceId }, - error => { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - window.alert(errorMessage); - TelemetryProcessor.traceFailure( - Action.ResolveConflict, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - conflictResourceType: selectedConflict.resourceType, - conflictOperationType: selectedConflict.operationType, - conflictResourceId: selectedConflict.resourceId, - error: errorMessage, - errorStack: getErrorStack(error) - }, - startKey - ); - } - ) - .finally(() => this.isExecuting(false)); + startKey + ); + } catch (error) { + this.isExecutionError(true); + const errorMessage = getErrorMessage(error); + window.alert(errorMessage); + TelemetryProcessor.traceFailure( + Action.ResolveConflict, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + conflictResourceType: selectedConflict.resourceType, + conflictOperationType: selectedConflict.operationType, + conflictResourceId: selectedConflict.resourceId, + error: errorMessage, + errorStack: getErrorStack(error) + }, + startKey + ); + } finally { + this.isExecuting(false); + } }; - public onDeleteClick = (): Q.Promise => { + public onDeleteClick = async (): Promise => { this.isExecutionError(false); this.isExecuting(true); @@ -375,50 +361,48 @@ export default class ConflictsTab extends TabsBase { conflictResourceId: selectedConflict.resourceId }); - return deleteConflict(this.collection, selectedConflict) - .then( - () => { - this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid); - this.selectedConflictContent(""); - this.selectedConflictCurrent(""); - this.selectedConflictId(null); - this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); - TelemetryProcessor.traceSuccess( - Action.DeleteConflict, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - conflictResourceType: selectedConflict.resourceType, - conflictOperationType: selectedConflict.operationType, - conflictResourceId: selectedConflict.resourceId - }, - startKey - ); + try { + await deleteConflict(this.collection, selectedConflict); + this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid); + this.selectedConflictContent(""); + this.selectedConflictCurrent(""); + this.selectedConflictId(null); + this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); + TelemetryProcessor.traceSuccess( + Action.DeleteConflict, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + conflictResourceType: selectedConflict.resourceType, + conflictOperationType: selectedConflict.operationType, + conflictResourceId: selectedConflict.resourceId }, - error => { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - window.alert(errorMessage); - TelemetryProcessor.traceFailure( - Action.DeleteConflict, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - conflictResourceType: selectedConflict.resourceType, - conflictOperationType: selectedConflict.operationType, - conflictResourceId: selectedConflict.resourceId, - error: errorMessage, - errorStack: getErrorStack(error) - }, - startKey - ); - } - ) - .finally(() => this.isExecuting(false)); + startKey + ); + } catch (error) { + this.isExecutionError(true); + const errorMessage = getErrorMessage(error); + window.alert(errorMessage); + TelemetryProcessor.traceFailure( + Action.DeleteConflict, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + conflictResourceType: selectedConflict.resourceType, + conflictOperationType: selectedConflict.operationType, + conflictResourceId: selectedConflict.resourceId, + error: errorMessage, + errorStack: getErrorStack(error) + }, + startKey + ); + } finally { + this.isExecuting(false); + } }; public onDiscardClick = (): Q.Promise => { @@ -445,60 +429,47 @@ export default class ConflictsTab extends TabsBase { return Q(); } - public onTabClick(): Q.Promise { - return super.onTabClick().then(() => { - this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Conflicts); - }); + public onTabClick(): void { + super.onTabClick(); + this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Conflicts); } - public onActivate(): Q.Promise { - return super.onActivate().then(() => { - if (this._documentsIterator) { - return Q.resolve(this._documentsIterator); - } + public async onActivate(): Promise { + super.onActivate(); - return this.createIterator().then( - (iterator: QueryIterator) => { - this._documentsIterator = iterator; - return this.loadNextPage(); - }, - error => { - if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { - TelemetryProcessor.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: getErrorMessage(error), - errorStack: getErrorStack(error) - }, - this.onLoadStartKey - ); - this.onLoadStartKey = null; - } + if (!this._documentsIterator) { + try { + this._documentsIterator = await this.createIterator(); + await this.loadNextPage(); + } catch (error) { + if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { + TelemetryProcessor.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: getErrorMessage(error), + errorStack: getErrorStack(error) + }, + this.onLoadStartKey + ); + this.onLoadStartKey = null; } - ); - }); + } + } } - public onRefreshClick(): Q.Promise { - return this.refreshDocumentsGrid().then(() => { - this.selectedConflictContent(""); - this.selectedConflictId(null); - this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); - }); - } - - public createIterator(): Q.Promise> { + public createIterator(): QueryIterator { // TODO: Conflict Feed does not allow filtering atm const query: string = undefined; - let options: any = {}; - options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); - return queryConflicts(this.collection.databaseId, this.collection.id(), query, options); + const options = { + enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey() + }; + return queryConflicts(this.collection.databaseId, this.collection.id(), query, options as FeedOptions); } public loadNextPage(): Q.Promise { diff --git a/src/Explorer/Tabs/DatabaseSettingsTab.ts b/src/Explorer/Tabs/DatabaseSettingsTab.ts index 75f441af4..b87adbf2b 100644 --- a/src/Explorer/Tabs/DatabaseSettingsTab.ts +++ b/src/Explorer/Tabs/DatabaseSettingsTab.ts @@ -429,11 +429,10 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. return Q(); }; - public onActivate(): Q.Promise { - return super.onActivate().then(async () => { - this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings); - await this.database.loadOffer(); - }); + public async onActivate(): Promise { + super.onActivate(); + this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings); + await this.database.loadOffer(); } private _setBaseline() { diff --git a/src/Explorer/Tabs/DocumentsTab.html b/src/Explorer/Tabs/DocumentsTab.html index 5a55c47ee..ffd2e05db 100644 --- a/src/Explorer/Tabs/DocumentsTab.html +++ b/src/Explorer/Tabs/DocumentsTab.html @@ -103,7 +103,7 @@
diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 57a6d97ec..0cc27d48d 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -551,7 +551,7 @@ export default class Collection implements ViewModels.Collection { const tabTitle = !this.offer() ? "Settings" : "Scale & Settings"; const pendingNotificationsPromise: Q.Promise = this._getPendingThroughputSplitNotification(); - const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Settings, tab => { + const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.SettingsV2, tab => { return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id(); }); From 6da43ee27bb83b4b13b4be6b2c79e7588bb2dc2c Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Thu, 17 Dec 2020 17:41:38 -0600 Subject: [PATCH 14/21] Publish IE specific Nuget package (#347) * Publish IE specific Nuget package * Require ally tests to pass --- .github/workflows/ci.yml | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d48c70a62..2540802bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -165,7 +165,7 @@ jobs: nuget: name: Publish Nuget if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') - needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted] + needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility] runs-on: ubuntu-latest env: NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} @@ -189,7 +189,7 @@ jobs: nugetmpac: name: Publish Nuget MPAC if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') - needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted] + needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility] runs-on: ubuntu-latest env: NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} @@ -211,3 +211,28 @@ jobs: name: packages with: path: "*.nupkg" + nugetie: + name: Publish Nuget IE + if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') + needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility] + runs-on: ubuntu-latest + env: + NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} + AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }} + steps: + - uses: nuget/setup-nuget@v1 + with: + nuget-api-key: ${{ secrets.NUGET_API_KEY }} + - name: Download Dist Folder + uses: actions/download-artifact@v2 + with: + name: dist + - run: cp ./configs/prod.json config.json + - run: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.IE/g' DataExplorer.nuspec + - run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "GitHub" -Password "$AZURE_DEVOPS_PAT" + - run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}" + - run: nuget push -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg + - uses: actions/upload-artifact@v2 + name: packages + with: + path: "*.nupkg" From 16bde97e47eb0e5a62bacda98de83ea252cc244d Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Fri, 18 Dec 2020 16:08:40 -0600 Subject: [PATCH 15/21] Rewrite URL for IE users (#340) --- .github/workflows/ci.yml | 2 ++ test/sql/container.spec.ts | 2 +- web.config | 11 ++++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2540802bd..440c65185 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,6 +101,7 @@ jobs: PLATFORM: "Emulator" NODE_TLS_REJECT_UNAUTHORIZED: 0 - uses: actions/upload-artifact@v2 + if: failure() with: name: screenshots path: failed-* @@ -159,6 +160,7 @@ jobs: TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }} DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html" - uses: actions/upload-artifact@v2 + if: failure() with: name: screenshots path: failed-* diff --git a/test/sql/container.spec.ts b/test/sql/container.spec.ts index ad5d7f251..853eff9e3 100644 --- a/test/sql/container.spec.ts +++ b/test/sql/container.spec.ts @@ -54,7 +54,7 @@ describe("Collection Add and Delete SQL spec", () => { // validate created // open database menu await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); - await frame.waitFor(LOADING_STATE_DELAY); + await frame.waitFor(CREATE_DELAY); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); const databases = await frame.$$(`div[class="databaseHeader main1 nodeItem "] > div[class="treeNodeHeader "]`); const selectedDbId = await frame.evaluate(element => { diff --git a/web.config b/web.config index 599176261..cde327431 100644 --- a/web.config +++ b/web.config @@ -15,6 +15,15 @@ + + + + + + + + + @@ -56,4 +65,4 @@ - \ No newline at end of file + From e8f4c8f93c1cdc2cb9d97cf6e3e9e1a1f5cdcec3 Mon Sep 17 00:00:00 2001 From: vchske Date: Fri, 18 Dec 2020 16:15:55 -0800 Subject: [PATCH 16/21] Cost Estimate Changes (#342) * Initial change of estimated cost to table format * Converted cost estimate to table format and added different data for current vs updated cost estimates. * lint fixes * Changed the names of some interfaces * Refactored a unit call to use an argument interface to avoid future confusion. * Changed the severity of the save warning * Format fix * Fixed test due to styling change Co-authored-by: Steve Faulkner --- .vs/slnx.sqlite | Bin 0 -> 651264 bytes .../Settings/SettingsRenderUtils.test.tsx | 46 ++- .../Controls/Settings/SettingsRenderUtils.tsx | 155 +++++--- .../MongoIndexingPolicyComponent.tsx | 14 +- ...roughputInputAutoPilotV3Component.test.tsx | 4 +- .../ThroughputInputAutoPilotV3Component.tsx | 260 +++++++++++- ...putInputAutoPilotV3Component.test.tsx.snap | 371 +++++++++++++++--- .../SettingsRenderUtils.test.tsx.snap | 148 ++++--- src/Utils/PricingUtils.test.ts | 126 +++++- src/Utils/PricingUtils.ts | 69 ++-- 10 files changed, 956 insertions(+), 237 deletions(-) create mode 100644 .vs/slnx.sqlite diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..2891048dc4387497230a4fef6fe9f9871dd59f83 GIT binary patch literal 651264 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%0AN2d6Ml2K1$v^|EJ1 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�&S1FMv)b@ka>Vh~i=tB^J!e@TJw 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%0A3x9d>k9g&G&z;}sWL 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 Cost, + hourly: $ 1.02, + daily: $ 24.48, + monthly: $ 744.6 + } + ]; + const priceBreakdown: PriceBreakdown = { + hourlyPrice: 1.02, + dailyPrice: 24.48, + monthlyPrice: 744.6, + pricePerRu: 0.00051, + currency: "RMB", + currencySign: "¥" + }; + return ( <> {getAutoPilotV3SpendElement(1000, false)} @@ -31,9 +57,7 @@ class SettingsRenderUtilsTestComponent extends React.Component { {getAutoPilotV3SpendElement(1000, true)} {getAutoPilotV3SpendElement(undefined, true)} - {getEstimatedSpendElement(1000, "mooncake", 2, false)} - - {getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)} + {getEstimatedSpendingElement(estimatedSpendingColumns, estimatedSpendingItems, 1000, 2, priceBreakdown, false)} {manualToAutoscaleDisclaimerElement} {ttlWarning} @@ -69,4 +93,14 @@ describe("SettingsUtils functions", () => { const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); + + it("should return correct price breakdown for a manual RU setting of 500, 1 region, multimaster disabled", () => { + const prices = getRuPriceBreakdown(500, "", 1, false, false); + expect(prices.hourlyPrice).toBe(0.04); + expect(prices.dailyPrice).toBe(0.96); + expect(prices.monthlyPrice).toBe(29.2); + expect(prices.pricePerRu).toBe(0.00008); + expect(prices.currency).toBe("USD"); + expect(prices.currencySign).toBe("$"); + }); }); diff --git a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx index 5aa871d1b..a3f186af7 100644 --- a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx @@ -3,14 +3,13 @@ import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants"; import { Urls, StyleConstants } from "../../../Common/Constants"; import { - computeAutoscaleUsagePriceHourly, getPriceCurrency, getCurrencySign, getAutoscalePricePerRu, getMultimasterMultiplier, computeRUUsagePriceHourly, getPricePerRu, - calculateEstimateNumber + estimatedCostDisclaimer } from "../../../Utils/PricingUtils"; import { ITextFieldStyles, @@ -32,10 +31,41 @@ import { MessageBarType, Stack, Spinner, - SpinnerSize + SpinnerSize, + DetailsList, + IColumn, + SelectionMode, + DetailsListLayoutMode, + IDetailsRowProps, + DetailsRow, + IDetailsColumnStyles } from "office-ui-fabric-react"; import { isDirtyTypes, isDirty } from "./SettingsUtils"; +export interface EstimatedSpendingDisplayProps { + costType: JSX.Element; +} + +export interface ManualEstimatedSpendingDisplayProps extends EstimatedSpendingDisplayProps { + hourly: JSX.Element; + daily: JSX.Element; + monthly: JSX.Element; +} + +export interface AutoscaleEstimatedSpendingDisplayProps extends EstimatedSpendingDisplayProps { + minPerMonth: JSX.Element; + maxPerMonth: JSX.Element; +} + +export interface PriceBreakdown { + hourlyPrice: number; + dailyPrice: number; + monthlyPrice: number; + pricePerRu: number; + currency: string; + currencySign: string; +} + export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } }; export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = { @@ -104,6 +134,16 @@ export const transparentDetailsRowStyles: Partial = { } }; +export const transparentDetailsHeaderStyle: Partial = { + root: { + selectors: { + ":hover": { + background: "transparent" + } + } + } +}; + export const customDetailsListStyles: Partial = { root: { selectors: { @@ -130,6 +170,10 @@ export const messageBarStyles: Partial = { root: { marginTop: export const throughputUnit = "RU/s"; +export function onRenderRow(props: IDetailsRowProps): JSX.Element { + return ; +} + export const getAutoPilotV3SpendElement = ( maxAutoPilotThroughputSet: number, isDatabaseThroughput: boolean, @@ -165,63 +209,61 @@ export const getAutoPilotV3SpendElement = ( ); }; -export const getEstimatedAutoscaleSpendElement = ( +export const getRuPriceBreakdown = ( throughput: number, serverId: string, - regions: number, - multimaster: boolean -): JSX.Element => { - const hourlyPrice: number = computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster); - const monthlyPrice: number = hourlyPrice * hoursInAMonth; - const currency: string = getPriceCurrency(serverId); - const currencySign: string = getCurrencySign(serverId); - const pricePerRu = - getAutoscalePricePerRu(serverId, getMultimasterMultiplier(regions, multimaster)) * - getMultimasterMultiplier(regions, multimaster); - - return ( - - Estimated monthly cost ({currency}) is{" "} - - {currencySign} - {calculateEstimateNumber(monthlyPrice / 10)} - {` - `} - {currencySign} - {calculateEstimateNumber(monthlyPrice)}{" "} - - ({"regions: "} {regions}, {throughput / 10} - {throughput} RU/s, {currencySign} - {pricePerRu}/RU) - - ); + numberOfRegions: number, + isMultimaster: boolean, + isAutoscale: boolean +): PriceBreakdown => { + const hourlyPrice: number = computeRUUsagePriceHourly({ + serverId: serverId, + requestUnits: throughput, + numberOfRegions: numberOfRegions, + multimasterEnabled: isMultimaster, + isAutoscale: isAutoscale + }); + const basePricePerRu: number = isAutoscale + ? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster)) + : getPricePerRu(serverId); + return { + hourlyPrice: hourlyPrice, + dailyPrice: hourlyPrice * 24, + monthlyPrice: hourlyPrice * hoursInAMonth, + pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster), + currency: getPriceCurrency(serverId), + currencySign: getCurrencySign(serverId) + }; }; -export const getEstimatedSpendElement = ( +export const getEstimatedSpendingElement = ( + estimatedSpendingColumns: IColumn[], + estimatedSpendingItems: EstimatedSpendingDisplayProps[], throughput: number, - serverId: string, - regions: number, - multimaster: boolean + numberOfRegions: number, + priceBreakdown: PriceBreakdown, + isAutoscale: boolean ): JSX.Element => { - const hourlyPrice: number = computeRUUsagePriceHourly(serverId, throughput, regions, multimaster); - const dailyPrice: number = hourlyPrice * 24; - const monthlyPrice: number = hourlyPrice * hoursInAMonth; - const currency: string = getPriceCurrency(serverId); - const currencySign: string = getCurrencySign(serverId); - const pricePerRu = getPricePerRu(serverId) * getMultimasterMultiplier(regions, multimaster); - + const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : ""; return ( - - Estimated cost ({currency}):{" "} - - {currencySign} - {calculateEstimateNumber(hourlyPrice)} hourly {` / `} - {currencySign} - {calculateEstimateNumber(dailyPrice)} daily {` / `} - {currencySign} - {calculateEstimateNumber(monthlyPrice)} monthly{" "} - - ({"regions: "} {regions}, {throughput}RU/s, {currencySign} - {pricePerRu}/RU) - + + + + ({"regions: "} {numberOfRegions}, {ruRange} + {throughput} RU/s, {priceBreakdown.currencySign} + {priceBreakdown.pricePerRu}/RU) + + + {estimatedCostDisclaimer} + + ); }; @@ -265,6 +307,13 @@ export const updateThroughputDelayedApplyWarningMessage: JSX.Element = ( ); +export const saveThroughputWarningMessage: JSX.Element = ( + + Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below + before saving your changes + +); + const getCurrentThroughput = ( isAutoscale: boolean, throughput: number, diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx index a74eaff1e..8c20c88e6 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx @@ -6,8 +6,6 @@ import { IconButton, Text, SelectionMode, - IDetailsRowProps, - DetailsRow, IColumn, MessageBar, MessageBarType, @@ -21,11 +19,11 @@ import { mongoIndexingPolicyDisclaimer, mediumWidthStackStyles, subComponentStackProps, - transparentDetailsRowStyles, createAndAddMongoIndexStackProps, separatorStyles, indexingPolicynUnsavedWarningMessage, - infoAndToolTipTextStyle + infoAndToolTipTextStyle, + onRenderRow } from "../../SettingsRenderUtils"; import { MongoIndex } from "../../../../../Utils/arm/generatedClients/2020-04-01/types"; import { @@ -140,10 +138,6 @@ export class MongoIndexingPolicyComponent extends React.Component { - return ; - }; - private getActionButton = (arrayPosition: number, isCurrentIndex: boolean): JSX.Element => { return isCurrentIndex ? ( {this.renderIndexesToBeAdded()} @@ -279,7 +273,7 @@ export class MongoIndexingPolicyComponent extends React.Component )} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx index 2ccf70079..605ab7a02 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx @@ -54,7 +54,6 @@ describe("ThroughputInputAutoPilotV3Component", () => { expect(wrapper.exists("#throughputInput")).toEqual(true); expect(wrapper.exists("#autopilotInput")).toEqual(false); expect(wrapper.exists("#throughputSpendElement")).toEqual(true); - expect(wrapper.exists("#autoscaleSpendElement")).toEqual(false); }); it("autopilot input visible", () => { @@ -72,8 +71,7 @@ describe("ThroughputInputAutoPilotV3Component", () => { wrapper.setProps({ wasAutopilotOriginallySet: true }); wrapper.update(); - expect(wrapper.exists("#autoscaleSpendElement")).toEqual(true); - expect(wrapper.exists("#throughputSpendElement")).toEqual(false); + expect(wrapper.exists("#throughputSpendElement")).toEqual(true); }); it("spendAck checkbox visible", () => { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx index cc23b0ec9..7cb3f81ce 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx @@ -8,10 +8,15 @@ import { checkBoxAndInputStackProps, getChoiceGroupStyles, messageBarStyles, - getEstimatedSpendElement, - getEstimatedAutoscaleSpendElement, + getEstimatedSpendingElement, getAutoPilotV3SpendElement, - manualToAutoscaleDisclaimerElement + manualToAutoscaleDisclaimerElement, + saveThroughputWarningMessage, + ManualEstimatedSpendingDisplayProps, + AutoscaleEstimatedSpendingDisplayProps, + PriceBreakdown, + getRuPriceBreakdown, + transparentDetailsHeaderStyle } from "../../SettingsRenderUtils"; import { Text, @@ -23,7 +28,9 @@ import { Label, Link, MessageBar, - MessageBarType + MessageBarType, + FontIcon, + IColumn } from "office-ui-fabric-react"; import { ToolTipLabelComponent } from "../ToolTipLabelComponent"; import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils"; @@ -32,7 +39,7 @@ import * as DataModels from "../../../../../Contracts/DataModels"; import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon"; import { userContext } from "../../../../../UserContext"; import { SubscriptionType } from "../../../../../Contracts/SubscriptionType"; -import { usageInGB } from "../../../../../Utils/PricingUtils"; +import { usageInGB, calculateEstimateNumber } from "../../../../../Utils/PricingUtils"; import { Features } from "../../../../../Common/Constants"; export interface ThroughputInputAutoPilotV3Props { @@ -165,33 +172,243 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< return <>; } + const isDirty: boolean = this.IsComponentDirty().isDiscardable; const serverId: string = this.props.serverId; - const offerThroughput: number = this.props.throughput; - const regions = account?.properties?.readLocations?.length || 1; const multimaster = account?.properties?.enableMultipleWriteLocations || false; let estimatedSpend: JSX.Element; if (!this.props.isAutoPilotSelected) { - estimatedSpend = getEstimatedSpendElement( + estimatedSpend = this.getEstimatedManualSpendElement( // if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set... - this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : offerThroughput, + this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : this.props.throughputBaseline, serverId, regions, - multimaster + multimaster, + isDirty ? this.props.throughput : undefined ); } else { - estimatedSpend = getEstimatedAutoscaleSpendElement( - this.props.maxAutoPilotThroughput, + estimatedSpend = this.getEstimatedAutoscaleSpendElement( + this.props.maxAutoPilotThroughputBaseline, serverId, regions, - multimaster + multimaster, + isDirty ? this.props.maxAutoPilotThroughput : undefined ); } return estimatedSpend; }; + private getEstimatedAutoscaleSpendElement = ( + throughput: number, + serverId: string, + numberOfRegions: number, + isMultimaster: boolean, + newThroughput?: number + ): JSX.Element => { + const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true); + const estimatedSpendingColumns: IColumn[] = [ + { + key: "costType", + name: "", + fieldName: "costType", + minWidth: 100, + maxWidth: 200, + isResizable: true, + styles: transparentDetailsHeaderStyle + }, + { + key: "minPerMonth", + name: "Min Per Month", + fieldName: "minPerMonth", + minWidth: 100, + maxWidth: 200, + isResizable: true, + styles: transparentDetailsHeaderStyle + }, + { + key: "maxPerMonth", + name: "Max Per Month", + fieldName: "maxPerMonth", + minWidth: 100, + maxWidth: 200, + isResizable: true, + styles: transparentDetailsHeaderStyle + } + ]; + const estimatedSpendingItems: AutoscaleEstimatedSpendingDisplayProps[] = [ + { + costType: Current Cost, + minPerMonth: ( + + {prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)} + + ), + maxPerMonth: ( + + {prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} + + ) + } + ]; + + if (newThroughput) { + const newPrices: PriceBreakdown = getRuPriceBreakdown( + newThroughput, + serverId, + numberOfRegions, + isMultimaster, + true + ); + estimatedSpendingItems.unshift({ + costType: ( + + Updated Cost + + ), + minPerMonth: ( + + + {newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)} + + + ), + maxPerMonth: ( + + + {newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} + + + ) + }); + } + + return getEstimatedSpendingElement( + estimatedSpendingColumns, + estimatedSpendingItems, + newThroughput ?? throughput, + numberOfRegions, + prices, + true + ); + }; + + private getEstimatedManualSpendElement = ( + throughput: number, + serverId: string, + numberOfRegions: number, + isMultimaster: boolean, + newThroughput?: number + ): JSX.Element => { + const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, false); + const estimatedSpendingColumns: IColumn[] = [ + { + key: "costType", + name: "", + fieldName: "costType", + minWidth: 100, + maxWidth: 200, + isResizable: true, + styles: transparentDetailsHeaderStyle + }, + { + key: "hourly", + name: "Hourly", + fieldName: "hourly", + minWidth: 100, + maxWidth: 200, + isResizable: true, + styles: transparentDetailsHeaderStyle + }, + { + key: "daily", + name: "Daily", + fieldName: "daily", + minWidth: 100, + maxWidth: 200, + isResizable: true, + styles: transparentDetailsHeaderStyle + }, + { + key: "monthly", + name: "Monthly", + fieldName: "monthly", + minWidth: 100, + maxWidth: 200, + isResizable: true, + styles: transparentDetailsHeaderStyle + } + ]; + const estimatedSpendingItems: ManualEstimatedSpendingDisplayProps[] = [ + { + costType: Current Cost, + hourly: ( + + {prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)} + + ), + daily: ( + + {prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)} + + ), + monthly: ( + + {prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} + + ) + } + ]; + + if (newThroughput) { + const newPrices: PriceBreakdown = getRuPriceBreakdown( + newThroughput, + serverId, + numberOfRegions, + isMultimaster, + false + ); + estimatedSpendingItems.unshift({ + costType: ( + + Updated Cost + + ), + hourly: ( + + + {newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)} + + + ), + daily: ( + + + {newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)} + + + ), + monthly: ( + + + {newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} + + + ) + }); + } + + return getEstimatedSpendingElement( + estimatedSpendingColumns, + estimatedSpendingItems, + newThroughput ?? throughput, + numberOfRegions, + prices, + false + ); + }; + private getAutoPilotUsageCost = (): JSX.Element => { if (!this.props.maxAutoPilotThroughput) { return <>; @@ -318,6 +535,12 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< private renderThroughputInput = (): JSX.Element => ( + + Estimate your required throughput with + + {` capacity calculator`} + + )} +
{this.props.isFixed &&

When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.

} ); + private renderWarningMessage = (): JSX.Element => { + let warningMessage: JSX.Element; + if (this.IsComponentDirty().isDiscardable) { + warningMessage = saveThroughputWarningMessage; + } + + return <>{warningMessage && {warningMessage}}; + }; + public render(): JSX.Element { return ( + {this.renderWarningMessage()} {this.renderThroughputModeChoices()} {this.props.isAutoPilotSelected ? this.renderAutoPilotInput() : this.renderThroughputInput()} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap index 9fe18a4cc..0cd8eb2c2 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap @@ -8,6 +8,21 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = ` } } > + + + Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below before saving your changes + + + + Estimate your required throughput with + + capacity calculator + + + + - - Estimated cost ( - USD - ): - - - $ - 0.0080 - hourly - / - $ - 0.19 - daily - / - $ - 5.84 - monthly + + Current Cost + , + "daily": + $ + + 0.19 + , + "hourly": + $ + + 0.0080 + , + "monthly": + $ + + 5.84 + , + }, + ] + } + layoutMode={1} + onRenderRow={[Function]} + selectionMode={0} + /> + + ( + regions: - - ( - regions: - - 1 - , - 100 - RU/s, - $ - 0.00008 - /RU) - + 1 + , + 100 + RU/s, + $ + 0.00008 + /RU) + + + + *This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account + + + +
`; @@ -369,6 +502,19 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = ` } } > + + Estimate your required throughput with + + capacity calculator + + + + - - Estimated cost ( - USD - ): - - - $ - 0.0080 - hourly - / - $ - 0.19 - daily - / - $ - 5.84 - monthly + + Current Cost + , + "daily": + $ + + 0.19 + , + "hourly": + $ + + 0.0080 + , + "monthly": + $ + + 5.84 + , + }, + ] + } + layoutMode={1} + onRenderRow={[Function]} + selectionMode={0} + /> + + ( + regions: - - ( - regions: - - 1 - , - 100 - RU/s, - $ - 0.00008 - /RU) - + 1 + , + 100 + RU/s, + $ + 0.00008 + /RU) + + + + *This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account + + + +
`; diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap index a13590e36..0efa51f9f 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap @@ -60,66 +60,100 @@ exports[`SettingsUtils functions render 1`] = ` . - - Estimated cost ( - RMB - ): - - - ¥ - 1.02 - hourly - / - ¥ - 24.48 - daily - / - ¥ - 744.60 - monthly + + Current Cost + , + "daily": + $ 24.48 + , + "hourly": + $ 1.02 + , + "monthly": + $ 744.6 + , + }, + ] + } + layoutMode={1} + onRenderRow={[Function]} + selectionMode={0} + /> + + ( + regions: - - ( - regions: - - 2 - , - 1000 - RU/s, - ¥ - 0.00051 - /RU) - - - Estimated monthly cost ( - RMB - ) is - - + 2 + , + 1000 + RU/s, ¥ - 111.69 - - - ¥ - 1116.90 - - - ( - regions: - - 2 - , - 100 - - - 1000 - RU/s, - ¥ - 0.000765 - /RU) - + 0.00051 + /RU) + + + + *This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account + + + { describe("computeRUUsagePriceHourly()", () => { it("should return 0 for NaN regions default cloud", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", 1, null, false); + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: null, + multimasterEnabled: false, + isAutoscale: false + }); + expect(value).toBe(0); + }); + it("should return 0 for NaN regions default cloud, autoscale", () => { + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: null, + multimasterEnabled: false, + isAutoscale: true + }); expect(value).toBe(0); }); it("should return 0 for -1 regions", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", 1, -1, false); + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: -1, + multimasterEnabled: false, + isAutoscale: false + }); + expect(value).toBe(0); + }); + it("should return 0 for -1 regions, autoscale", () => { + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: -1, + multimasterEnabled: false, + isAutoscale: true + }); expect(value).toBe(0); }); it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster disabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 1, false); + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: 1, + multimasterEnabled: false, + isAutoscale: false + }); expect(value).toBe(0.00008); }); + it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster disabled, autoscale", () => { + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: 1, + multimasterEnabled: false, + isAutoscale: true + }); + expect(value).toBe(0.00012); + }); it("should return 0.00051 for Mooncake cloud, 1RU, 1 region, multimaster disabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("mooncake", 1, 1, false); + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "mooncake", + requestUnits: 1, + numberOfRegions: 1, + multimasterEnabled: false, + isAutoscale: false + }); expect(value).toBe(0.00051); }); + it("should return 0.00051 for Mooncake cloud, 1RU, 1 region, multimaster disabled, autoscale", () => { + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "mooncake", + requestUnits: 1, + numberOfRegions: 1, + multimasterEnabled: false, + isAutoscale: true + }); + expect(value).toBe(0.00076); + }); it("should return 0.00016 for default cloud, 1RU, 2 regions, multimaster disabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 2, false); + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: 2, + multimasterEnabled: false, + isAutoscale: false + }); expect(value).toBe(0.00016); }); + it("should return 0.00016 for default cloud, 1RU, 2 regions, multimaster disabled, autoscale", () => { + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: 2, + multimasterEnabled: false, + isAutoscale: true + }); + expect(value).toBe(0.00024); + }); it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster enabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 1, true); + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: 1, + multimasterEnabled: true, + isAutoscale: false + }); expect(value).toBe(0.00008); }); + it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster enabled, autoscale", () => { + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: 1, + multimasterEnabled: true, + isAutoscale: true + }); + expect(value).toBe(0.00012); + }); it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled", () => { - const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 2, true); + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: 2, + multimasterEnabled: true, + isAutoscale: false + }); expect(value).toBe(0.00048); }); + it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled, autoscale", () => { + const value = PricingUtils.computeRUUsagePriceHourly({ + serverId: "default", + requestUnits: 1, + numberOfRegions: 2, + multimasterEnabled: true, + isAutoscale: true + }); + expect(value).toBe(0.00096); + }); }); describe("getPriceCurrency()", () => { diff --git a/src/Utils/PricingUtils.ts b/src/Utils/PricingUtils.ts index 5b6c4ec0e..af7d1d8d7 100644 --- a/src/Utils/PricingUtils.ts +++ b/src/Utils/PricingUtils.ts @@ -1,6 +1,17 @@ import * as AutoPilotUtils from "../Utils/AutoPilotUtils"; import * as Constants from "../Shared/Constants"; +interface ComputeRUUsagePriceHourlyArgs { + serverId: string; + requestUnits: number; + numberOfRegions: number; + multimasterEnabled: boolean; + isAutoscale: boolean; +} + +export const estimatedCostDisclaimer = + "*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account"; + /** * Anything that is not a number should return 0 * Otherwise, return numberOfRegions @@ -47,15 +58,16 @@ export function getMultimasterMultiplier(numberOfRegions: number, multimasterEna return multimasterMultiplier; } -export function computeRUUsagePriceHourly( - serverId: string, - requestUnits: number, - numberOfRegions: number, - multimasterEnabled: boolean -): number { +export function computeRUUsagePriceHourly({ + serverId, + requestUnits, + numberOfRegions, + multimasterEnabled, + isAutoscale +}: ComputeRUUsagePriceHourlyArgs): number { const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled); const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled); - const pricePerRu = getPricePerRu(serverId); + const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multimasterMultiplier) : getPricePerRu(serverId); const ruCharge = requestUnits * pricePerRu * multimasterMultiplier * regionMultiplier; return Number(ruCharge.toFixed(5)); @@ -159,28 +171,19 @@ export function getAutoPilotV3SpendHtml(maxAutoPilotThroughputSet: number, isDat }' target='_blank' aria-label='Learn more about autoscale throughput'>Learn more
.`; } -export function computeAutoscaleUsagePriceHourly( - serverId: string, - requestUnits: number, - numberOfRegions: number, - multimasterEnabled: boolean -): number { - const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled); - const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled); - - const pricePerRu = getAutoscalePricePerRu(serverId, multimasterMultiplier); - const ruCharge = requestUnits * pricePerRu * multimasterMultiplier * regionMultiplier; - - return Number(ruCharge.toFixed(5)); -} - export function getEstimatedAutoscaleSpendHtml( throughput: number, serverId: string, regions: number, multimaster: boolean ): string { - const hourlyPrice: number = computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster); + const hourlyPrice: number = computeRUUsagePriceHourly({ + serverId: serverId, + requestUnits: throughput, + numberOfRegions: regions, + multimasterEnabled: multimaster, + isAutoscale: true + }); const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth; const currency: string = getPriceCurrency(serverId); const currencySign: string = getCurrencySign(serverId); @@ -203,7 +206,13 @@ export function getEstimatedSpendHtml( regions: number, multimaster: boolean ): string { - const hourlyPrice: number = computeRUUsagePriceHourly(serverId, throughput, regions, multimaster); + const hourlyPrice: number = computeRUUsagePriceHourly({ + serverId: serverId, + requestUnits: throughput, + numberOfRegions: regions, + multimasterEnabled: multimaster, + isAutoscale: false + }); const dailyPrice: number = hourlyPrice * 24; const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth; const currency: string = getPriceCurrency(serverId); @@ -217,7 +226,7 @@ export function getEstimatedSpendHtml( `${currencySign}${calculateEstimateNumber(monthlyPrice)} monthly
` + `(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput}RU/s, ${currencySign}${pricePerRu}/RU)` + `

` + - `*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account

` + `${estimatedCostDisclaimer}

` ); } @@ -228,9 +237,13 @@ export function getEstimatedSpendAcknowledgeString( multimaster: boolean, isAutoscale: boolean ): string { - const hourlyPrice: number = isAutoscale - ? computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster) - : computeRUUsagePriceHourly(serverId, throughput, regions, multimaster); + const hourlyPrice: number = computeRUUsagePriceHourly({ + serverId: serverId, + requestUnits: throughput, + numberOfRegions: regions, + multimasterEnabled: multimaster, + isAutoscale: isAutoscale + }); const dailyPrice: number = hourlyPrice * 24; const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth; const currencySign: string = getCurrencySign(serverId); From b000631a0c3b2d9afbab466cc7982fb503f5ca0b Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Fri, 18 Dec 2020 19:26:10 -0600 Subject: [PATCH 17/21] Revert web.config changes (#349) --- web.config | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/web.config b/web.config index cde327431..599176261 100644 --- a/web.config +++ b/web.config @@ -15,15 +15,6 @@ - - - - - - - - - @@ -65,4 +56,4 @@ - + \ No newline at end of file From c3058ee5a9753f64fefbe87777b7d9dd371f0f55 Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Fri, 18 Dec 2020 19:55:32 -0600 Subject: [PATCH 18/21] Check for undefined query results (#350) --- src/Explorer/Tabs/QueryTab.html | 2 +- src/RouteHandlers/TabRouteHandler.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Explorer/Tabs/QueryTab.html b/src/Explorer/Tabs/QueryTab.html index 0d16152da..e27a4bb7c 100644 --- a/src/Explorer/Tabs/QueryTab.html +++ b/src/Explorer/Tabs/QueryTab.html @@ -103,7 +103,7 @@
this._tabRouter.parse(newHash); - const defaultRoutedCallback = (request: string, data: { route: any; params: string[]; isFirst: boolean }) => { - console.log(request); - }; + const defaultRoutedCallback = (request: string, data: { route: any; params: string[]; isFirst: boolean }) => {}; this._tabRouter.routed.add(onMatch || defaultRoutedCallback); hasher.initialized.add(parseHash); hasher.changed.add(parseHash); From cc63cdc1fd24e4c0487029bd66ba575124bb289d Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Sat, 26 Dec 2020 21:56:37 -0600 Subject: [PATCH 19/21] Remove dependency on canvas (#354) --- canvas/README.md | 7 ++ canvas/index.js | 1 + canvas/package.json | 11 +++ package-lock.json | 220 ++++++++------------------------------------ package.json | 2 +- 5 files changed, 57 insertions(+), 184 deletions(-) create mode 100644 canvas/README.md create mode 100644 canvas/index.js create mode 100644 canvas/package.json diff --git a/canvas/README.md b/canvas/README.md new file mode 100644 index 000000000..60df6d486 --- /dev/null +++ b/canvas/README.md @@ -0,0 +1,7 @@ +# Why? + +This adds a mock module for `canvas`. Nteract has a ignored require and undeclared dependency on this module. `cavnas` is a server side node module and is not used in browser side code for nteract. + +Installing it locally (`npm install canvas`) will resolve the problem, but it is a native module so it is flaky depending on the system, node version, processor arch, etc. This module provides a simpler, more robust solution. + +Remove this workaround if [this bug](https://github.com/nteract/any-vega/issues/2) ever gets resolved \ No newline at end of file diff --git a/canvas/index.js b/canvas/index.js new file mode 100644 index 000000000..7c6d6c73d --- /dev/null +++ b/canvas/index.js @@ -0,0 +1 @@ +module.exports = {} \ No newline at end of file diff --git a/canvas/package.json b/canvas/package.json new file mode 100644 index 000000000..ce5a08e02 --- /dev/null +++ b/canvas/package.json @@ -0,0 +1,11 @@ +{ + "name": "canvas", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} diff --git a/package-lock.json b/package-lock.json index 47b42c598..5929d906b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5393,11 +5393,6 @@ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, "abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -5641,6 +5636,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "optional": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -6883,14 +6879,7 @@ "dev": true }, "canvas": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.1.tgz", - "integrity": "sha512-S98rKsPcuhfTcYbtF53UIJhcbgIAK533d1kJKMwsMwAIFgfd58MOyxRud3kktlzWiEkFliaJtvyZCBtud/XVEA==", - "requires": { - "nan": "^2.14.0", - "node-pre-gyp": "^0.11.0", - "simple-get": "^3.0.3" - } + "version": "file:canvas" }, "capture-exit": { "version": "2.0.0", @@ -7454,7 +7443,8 @@ "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "optional": true }, "constants-browserify": { "version": "1.0.0", @@ -8435,6 +8425,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, "requires": { "mimic-response": "^2.0.0" } @@ -8460,7 +8451,8 @@ "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "optional": true }, "deep-is": { "version": "0.1.3", @@ -8652,7 +8644,8 @@ "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "optional": true }, "depd": { "version": "1.1.2", @@ -8688,7 +8681,8 @@ "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "optional": true }, "detect-newline": { "version": "2.1.0", @@ -10674,14 +10668,6 @@ "universalify": "^0.1.0" } }, - "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "requires": { - "minipass": "^2.6.0" - } - }, "fs-observable": { "version": "4.1.14", "resolved": "https://registry.npmjs.org/fs-observable/-/fs-observable-4.1.14.tgz", @@ -10823,6 +10809,7 @@ "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "optional": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -10837,12 +10824,14 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -10851,6 +10840,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -10861,6 +10851,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -11350,7 +11341,8 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "optional": true }, "has-value": { "version": "1.0.0", @@ -11832,14 +11824,6 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" }, - "ignore-walk": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", - "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", - "requires": { - "minimatch": "^3.0.4" - } - }, "image-size": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", @@ -15544,7 +15528,8 @@ "mimic-response": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true }, "min-document": { "version": "2.19.0", @@ -15601,15 +15586,6 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, "minipass-collect": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", @@ -15679,14 +15655,6 @@ } } }, - "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "requires": { - "minipass": "^2.9.0" - } - }, "mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", @@ -15861,7 +15829,8 @@ "nan": { "version": "2.14.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "optional": true }, "nanomatch": { "version": "1.2.13", @@ -15911,26 +15880,6 @@ "semver": "^5.4.1" } }, - "needle": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.2.tgz", - "integrity": "sha512-LbRIwS9BfkPvNwNHlsA41Q29kL2L/6VaOJ0qisM5lLWsTV3nP15abO5ITL6L81zqFhzjRKDAYjpcBcwM0AVvLQ==", - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - } - } - }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -16099,41 +16048,6 @@ "which": "^1.3.0" } }, - "node-pre-gyp": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", - "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - } - } - }, "node-releases": { "version": "1.1.66", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.66.tgz", @@ -16146,15 +16060,6 @@ "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=", "optional": true }, - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -16179,29 +16084,6 @@ "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz", "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==" }, - "npm-bundled": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", - "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" - }, - "npm-packlist": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -16214,6 +16096,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "optional": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -16605,7 +16488,8 @@ "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true }, "os-locale": { "version": "1.4.0", @@ -16624,20 +16508,6 @@ "windows-release": "^3.1.0" } }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", @@ -17690,6 +17560,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "optional": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -19116,12 +18987,14 @@ "simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "optional": true }, "simple-get": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "optional": true, "requires": { "decompress-response": "^4.2.0", "once": "^1.3.1", @@ -20113,30 +19986,6 @@ "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", "dev": true }, - "tar": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", - "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - } - } - } - }, "tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -22192,6 +22041,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "optional": true, "requires": { "string-width": "^1.0.2 || 2" }, @@ -22199,12 +22049,14 @@ "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "optional": true }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "optional": true, "requires": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" @@ -22214,6 +22066,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "optional": true, "requires": { "ansi-regex": "^3.0.0" } @@ -22383,7 +22236,8 @@ "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true }, "yargs": { "version": "13.3.2", diff --git a/package.json b/package.json index 5b2f9c22a..ee52bd28b 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "applicationinsights": "1.8.0", "babel-polyfill": "6.26.0", "bootstrap": "3.4.1", - "canvas": "2.6.1", + "canvas": "file:./canvas", "clean-webpack-plugin": "0.1.19", "copy-webpack-plugin": "6.0.2", "crossroads": "0.12.2", From d40b1aa9b564e22c2d489a0292e7ef522993f36f Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Mon, 4 Jan 2021 13:58:01 -0600 Subject: [PATCH 20/21] Remove Empty Query Logging (#361) --- src/Explorer/Tabs/QueryTab.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/Explorer/Tabs/QueryTab.ts b/src/Explorer/Tabs/QueryTab.ts index 9d40a5681..6a6d11007 100644 --- a/src/Explorer/Tabs/QueryTab.ts +++ b/src/Explorer/Tabs/QueryTab.ts @@ -324,21 +324,6 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`; this.showingDocumentsDisplayText(resultsDisplay); this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`); - - if (!this.queryResults() && !results) { - const errorMessage: string = JSON.stringify({ - error: `Returned no results after query execution`, - accountName: this.collection && this.collection.container.databaseAccount(), - databaseName: this.collection && this.collection.databaseId, - collectionName: this.collection && this.collection.id(), - sqlQuery: this.sqlStatementToExecute(), - hasMoreResults: resultsMetadata.hasMoreResults, - itemCount: resultsMetadata.itemCount, - responseHeaders: queryResults && queryResults.headers - }); - Logger.logError(errorMessage, "QueryTab"); - } - this.queryResults(results); TelemetryProcessor.traceSuccess( From a55f2d0de91385b8ceabc549a2c7f765f7a53751 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Mon, 4 Jan 2021 12:56:55 -0800 Subject: [PATCH 21/21] Free tier improvements in DE (#348) Co-authored-by: Steve Faulkner --- less/Common/Constants.less | 195 +- less/documentDB.less | 3829 +++++++++-------- .../Controls/Settings/SettingsRenderUtils.tsx | 7 +- ...dexingPolicyRefreshComponent.test.tsx.snap | 2 +- .../SettingsSubComponents/ScaleComponent.tsx | 31 +- .../SubSettingsComponent.tsx | 16 +- ...roughputInputAutoPilotV3Component.test.tsx | 1 + .../ThroughputInputAutoPilotV3Component.tsx | 38 +- ...putInputAutoPilotV3Component.test.tsx.snap | 28 +- .../ScaleComponent.test.tsx.snap | 2 +- .../SubSettingsComponent.test.tsx.snap | 8 +- .../SettingsComponent.test.tsx.snap | 16 + .../SettingsRenderUtils.test.tsx.snap | 30 +- .../ThroughputInputComponentAutoPilotV3.ts | 16 + .../ThroughputInputComponentAutoscaleV3.html | 13 + src/Explorer/Explorer.ts | 21 + src/Explorer/Panes/AddCollectionPane.html | 6 +- src/Explorer/Panes/AddCollectionPane.test.ts | 2 +- src/Explorer/Panes/AddCollectionPane.ts | 33 +- src/Explorer/Panes/AddDatabasePane.html | 3 +- src/Explorer/Panes/AddDatabasePane.test.ts | 2 +- src/Explorer/Panes/AddDatabasePane.ts | 28 +- src/Explorer/Tabs/DatabaseSettingsTab.html | 16 +- src/Explorer/Tabs/DatabaseSettingsTab.ts | 13 + src/Utils/PricingUtils.ts | 31 +- 25 files changed, 2356 insertions(+), 2031 deletions(-) diff --git a/less/Common/Constants.less b/less/Common/Constants.less index 46c24b07e..bc8616929 100644 --- a/less/Common/Constants.less +++ b/less/Common/Constants.less @@ -3,8 +3,8 @@ /******************************************************************************/ @font-face { - font-family: wf_segoe-ui_normal; - src: url('../../fonts/segoe-ui/west-european/normal/latest.woff'); + font-family: wf_segoe-ui_normal; + src: url("../../fonts/segoe-ui/west-european/normal/latest.woff"); } @DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; @@ -20,26 +20,26 @@ COLORS /******************************************************************************/ -@AccentMediumHigh: #0058AD; -@AccentMedium: #004E87; -@AccentHigh: #1EBAED; -@AccentExtraHigh: #55B3FF; -@AccentLow: #EDF6FF; -@AccentMediumLow: #DDEEFE; -@AccentLight: #EEF7FF; -@AccentExtra: #DDF0FF; +@AccentMediumHigh: #0058ad; +@AccentMedium: #004e87; +@AccentHigh: #1ebaed; +@AccentExtraHigh: #55b3ff; +@AccentLow: #edf6ff; +@AccentMediumLow: #ddeefe; +@AccentLight: #eef7ff; +@AccentExtra: #ddf0ff; -@SelectionHigh: #B91F26; -@BaseLight: #FFFFFF; +@SelectionHigh: #b91f26; +@BaseLight: #ffffff; @BaseDark: #000000; -@NotificationLow: #FFF4CE; -@NotificationHigh: #F9E9B0; -@Purple1: #8A2DA5; +@NotificationLow: #fff4ce; +@NotificationHigh: #f9e9b0; +@Purple1: #8a2da5; @Dirty: #9b4f96; -@BaseLow: #F2F2F2; -@BaseMediumLow: #E6E6E6; -@BaseMedium: #CCCCCC; +@BaseLow: #f2f2f2; +@BaseMediumLow: #e6e6e6; +@BaseMedium: #cccccc; @BaseMediumHigh: #767676; @BaseHigh: #393939; @@ -53,7 +53,7 @@ @ErrorColor: @SelectionHigh; -@SelectionColor: #3074B0; +@SelectionColor: #3074b0; @FocusColor: #605e5c; @@ -80,7 +80,7 @@ @ImgWidth: 14px; @ImgHeight: 14px; -@toggleFontWeight:700; +@toggleFontWeight: 700; //Resource Tree @TreeLineHeight: 17px; @@ -144,16 +144,16 @@ /**********************************************************************************/ .flex-display(@display: flex) { - display: ~"-webkit-@{display}"; - display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox - display: ~"-ms-@{display}"; // IE11 - display: @display; + display: ~"-webkit-@{display}"; + display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox + display: ~"-ms-@{display}"; // IE11 + display: @display; } .flex-direction(@direction: column) { -webkit-flex-direction: @direction; - -ms-flex-direction: @direction; - flex-direction: @direction; + -ms-flex-direction: @direction; + flex-direction: @direction; } /************************************************************************************* @@ -161,32 +161,31 @@ **************************************************************************************/ @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { - .selectedRadio, - .selectedRadio:hover, - .selectedRadio:active, - .selectedRadio.dirty, - .tab [type=radio]:checked ~ label, - .tab [type=radio]:checked ~ label:hover { - -ms-high-contrast-adjust: none; - -webkit-text-fill-color: HighlightText; - color: HighlightText; - border-color: HighlightText; - background-color: Highlight; - } - - .queryMetricsSummaryTuple { - - th, td { - - &:nth-child(2) { - width: @IETableDataWidth; - } - - &:nth-child(3) { - width: 50%; - } - } + .selectedRadio, + .selectedRadio:hover, + .selectedRadio:active, + .selectedRadio.dirty, + .tab [type="radio"]:checked ~ label, + .tab [type="radio"]:checked ~ label:hover { + -ms-high-contrast-adjust: none; + -webkit-text-fill-color: HighlightText; + color: HighlightText; + border-color: HighlightText; + background-color: Highlight; + } + + .queryMetricsSummaryTuple { + th, + td { + &:nth-child(2) { + width: @IETableDataWidth; + } + + &:nth-child(3) { + width: 50%; + } } + } } /******************************************************************************************** @@ -194,15 +193,15 @@ *********************************************************************************************/ .hover() { - background-color: @AccentLight; + background-color: @AccentLight; } .active() { - background-color: @AccentExtra; + background-color: @AccentExtra; } .focus() { - outline: 1px dashed @FocusColor; + outline: 1px dashed @FocusColor; } /************************************************************************************************ @@ -212,63 +211,87 @@ @ToggleWidth: 180px; .toggleSwitch() { - max-width: 100%; - margin-bottom: @SmallSpace; - padding: @SmallSpace; - cursor: pointer; - color: @BaseHigh; - font-weight: 400; - font-size: @mediumFontSize; - font-family: @DataExplorerFont; + max-width: 100%; + margin-bottom: @SmallSpace; + padding: @SmallSpace; + cursor: pointer; + color: @BaseHigh; + font-weight: 400; + font-size: @mediumFontSize; + font-family: @DataExplorerFont; } .selectedToggle() { - border-bottom: 2px solid @BaseHigh; + border-bottom: 2px solid @BaseHigh; } .unselectedToggle() { - color: @AccentMediumHigh; + color: @AccentMediumHigh; } /******************************************************************************************************** Common Data Explorer Icons *********************************************************************************************************/ .dataExplorerIcons() { - cursor: pointer; - width: @ImgWidth; - height: @ImgHeight; + cursor: pointer; + width: @ImgWidth; + height: @ImgHeight; } /********************************************************************************************************* Info Tooltip **********************************************************************************************************/ .infoTooltip() { - position: relative; - display: inline-block; + position: relative; + display: inline-block; } .tooltipText(@textColor: @BaseLight, @backgroundColor: @BaseHigh) { - visibility: hidden; - background-color: @backgroundColor; - color: @textColor; - position: absolute; - z-index: 1; - left: @MediumSpace; - padding: @MediumSpace; + visibility: hidden; + background-color: @backgroundColor; + color: @textColor; + position: absolute; + z-index: 1; + left: @MediumSpace; + padding: @MediumSpace; } .tooltipTextAfter(@color: @BaseDark) { - content: ""; - position: absolute; - right: 100%; - border-style: solid; - border-color: transparent @color transparent transparent; - left: 0px; - width: 0; - height: 0; - border-color: @InfoPointerColor transparent; + content: ""; + position: absolute; + right: 100%; + border-style: solid; + border-color: transparent @color transparent transparent; + left: 0px; + width: 0; + height: 0; + border-color: @InfoPointerColor transparent; } .tooltipVisible() { - visibility: visible; + visibility: visible; +} + +.inputTooltip() { + position: relative; +} + +.inputTooltipText(@textColor: @BaseLight, @backgroundColor: @BaseHigh) { + background-color: @backgroundColor; + color: @textColor; + position: absolute; + z-index: 1; + padding: @MediumSpace; +} + +.inputTooltipTextAfter(@color: @BaseDark) { + content: ""; + position: absolute; + right: 100%; + border-style: solid; + border-color: transparent @color transparent transparent; + left: 10px; + width: 0; + height: 0; + border-color: @InfoPointerColor transparent; } diff --git a/less/documentDB.less b/less/documentDB.less index e944f2a74..587670560 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -1,1681 +1,1696 @@ @import "./Common/Constants"; html { - font-family: @DataExplorerFont; - padding: 0px; - margin: 0px; - overflow: hidden; - position: fixed; - width: 100%; - height: 100%; + font-family: @DataExplorerFont; + padding: 0px; + margin: 0px; + overflow: hidden; + position: fixed; + width: 100%; + height: 100%; } body { - font-family: @DataExplorerFont; - font-size: 12px; - height: 100%; + font-family: @DataExplorerFont; + font-size: 12px; + height: 100%; - :focus { - .focus() - } + :focus { + .focus(); + } } .float-right { - float: right; + float: right; } .fixedleftpane { - background: #2f2d2d; - height: 100vh; - width: 80px; - float: left; + background: #2f2d2d; + height: 100vh; + width: 80px; + float: left; } .ui-dialog.ui-corner-all.ui-widget.ui-front.ui-widget-content.shareUrlDialog.no-close { - box-shadow: 0 0 @DefaultSpace @BoxShadow; - display: inline-block; + box-shadow: 0 0 @DefaultSpace @BoxShadow; + display: inline-block; + position: absolute; + background: #fff; + border: 1px solid @BaseMedium; + margin: 42px 20px; + border-radius: 3px; + + &:before { + content: ""; position: absolute; - background: #fff; - border: 1px solid @BaseMedium; - margin: 42px 20px; - border-radius: 3px; + top: -10px; + right: 13px; + border-width: 0 10px 10px; + border-style: solid; + border-color: @BaseMedium rgba(0, 0, 0, 0); + display: block; + width: 0; + } - &:before { - content:""; - position: absolute; - top: -10px; - right: 13px; - border-width: 0 10px 10px; - border-style: solid; - border-color: @BaseMedium rgba(0, 0, 0, 0); - display: block; - width: 0; + &:after { + content: ""; + position: absolute; + top: -9px; + right: 14px; + border-width: 0 9px 9px; + border-style: solid; + border-color: #fff rgba(0, 0, 0, 0); + display: block; + width: 0; + } + + .ui-dialog-titlebar.ui-corner-all.ui-helper-clearfix.ui-widget-header.shareUrlTitle { + border: none; + background-color: @BaseLight; + color: @BaseHigh; + font-size: @largeFontSize; + font-family: @DataExplorerFont; + padding: @MediumSpace; + margin: 0px @DefaultSpace 0px @MediumSpace; + font-weight: normal; + + .ui-dialog-titlebar-close.shareClose { + display: none; } + } - &:after { - content:""; - position: absolute; - top: -9px; - right: 14px; - border-width: 0 9px 9px; - border-style: solid; - border-color: #FFF rgba(0, 0, 0, 0); - display: block; - width: 0; - } + .shareDataAccessFlyout { + overflow: visible; + margin-bottom: @LargeSpace; + padding: @DefaultSpace @LargeSpace; - .ui-dialog-titlebar.ui-corner-all.ui-helper-clearfix.ui-widget-header.shareUrlTitle { - border: none; - background-color: @BaseLight; - color: @BaseHigh; - font-size: @largeFontSize; - font-family: @DataExplorerFont; - padding: @MediumSpace; - margin: 0px @DefaultSpace 0px @MediumSpace; - font-weight: normal; + .shareDataAccessFlyoutContent { + height: 100%; + width: 100%; - .ui-dialog-titlebar-close.shareClose { - display: none; + .urlContainer { + margin-left: @DefaultSpace; + + .urlContentText { + color: @BaseHigh; + font-size: @mediumFontSize; + font-family: @DataExplorerFont; } - } - .shareDataAccessFlyout { - overflow: visible; - margin-bottom: @LargeSpace; - padding: @DefaultSpace @LargeSpace; + .toggles { + height: @ToggleHeight; + width: @ToggleWidth; + margin: 24px 0px @LargeSpace 0px; - .shareDataAccessFlyoutContent { - height: 100%; - width: 100%; + &:focus { + .focus(); + } + .tab { + margin-right: @MediumSpace; + } - .urlContainer { - margin-left: @DefaultSpace; + .toggleSwitch { + .toggleSwitch(); + } - .urlContentText { - color: @BaseHigh; - font-size: @mediumFontSize; - font-family: @DataExplorerFont; - } + .selectedToggle { + .selectedToggle(); + } - .toggles { - height: @ToggleHeight; - width: @ToggleWidth; - margin: 24px 0px @LargeSpace 0px; + .unselectedToggle { + .unselectedToggle(); + } + } - &:focus { - .focus(); - } + .shareLabels { + color: @BaseHigh; + font-size: @mediumFontSize; + font-family: @DataExplorerFont; + margin-left: @DefaultSpace; + } - .tab { - margin-right: @MediumSpace; - } + .urlSpace { + margin: @LargeSpace 0px @DefaultSpace 0px; - .toggleSwitch { - .toggleSwitch(); - } + img { + cursor: pointer; + } + } - .selectedToggle { - .selectedToggle(); - } + .tokenSpace { + margin-bottom: (2 * @MediumSpace); - .unselectedToggle { - .unselectedToggle(); - } - } + img { + cursor: pointer; + } + } - .shareLabels { - color: @BaseHigh; - font-size: @mediumFontSize; - font-family: @DataExplorerFont; - margin-left: @DefaultSpace; - } + .urlTokenInfoTooltip { + .infoTooltip(); - .urlSpace { - margin: @LargeSpace 0px @DefaultSpace 0px; + &:hover .urlTokenTooltiptext { + .tooltipVisible(); + } - img { - cursor: pointer; - } - } + .urlTokenTooltiptext { + bottom: 28px; + width: 250px; + .tooltipText(); - .tokenSpace { - margin-bottom: (2 * @MediumSpace); - - img { - cursor: pointer; - } - } - - .urlTokenInfoTooltip { - .infoTooltip(); - - &:hover .urlTokenTooltiptext { - .tooltipVisible(); - } - - .urlTokenTooltiptext { - bottom:28px; - width: 250px; - .tooltipText(); - - &:after { - border-width: (2 * @MediumSpace) (2 * @MediumSpace) 0px 0px; - bottom: -15px; - .tooltipTextAfter(); - } - } - } - - .urlTokenCopyInfoTooltip { - .infoTooltip(); - padding: @SmallSpace; - - &:hover { - .hover(); - } - - &:active { - .active(); - } - - &:focus .urlTokenCopyTooltiptext, &:focus .urlTokenCopyTooltiptext { - .tooltipVisible(); - } - - &:focus .urlTokenCopyTooltiptext { - .tooltipVisible(); - } - - .urlTokenCopyTooltiptext { - visibility: hidden; - text-align: center; - background-color: @BaseHigh; - color: @BaseLight; - top: (2 * @LargeSpace); - width: 80px; - left: -26px; - position: absolute; - z-index: 1; - padding: @SmallSpace; - border-radius: 3px; - - &:after { - content: ""; - position: absolute; - border-style: solid; - border-color: transparent @BaseDark transparent transparent; - border-width: 0px @DefaultSpace 6px @DefaultSpace; - top: -5px; - left: 30px; - width: 0; - height: 0; - border-color: @InfoPointerColor transparent; - } - } - } - - .shareLink { - width: 300px; - background-color: #FFFFFF; - border: 1px solid @BaseMedium; - overflow: hidden; - text-overflow: ellipsis; - padding: 2px @SmallSpace 2px @SmallSpace; - margin: @SmallSpace @DefaultSpace 0px 0px; - font-family: @DataExplorerFont; - } + &:after { + border-width: (2 * @MediumSpace) (2 * @MediumSpace) 0px 0px; + bottom: -15px; + .tooltipTextAfter(); } + } } + + .urlTokenCopyInfoTooltip { + .infoTooltip(); + padding: @SmallSpace; + + &:hover { + .hover(); + } + + &:active { + .active(); + } + + &:focus .urlTokenCopyTooltiptext, + &:focus .urlTokenCopyTooltiptext { + .tooltipVisible(); + } + + &:focus .urlTokenCopyTooltiptext { + .tooltipVisible(); + } + + .urlTokenCopyTooltiptext { + visibility: hidden; + text-align: center; + background-color: @BaseHigh; + color: @BaseLight; + top: (2 * @LargeSpace); + width: 80px; + left: -26px; + position: absolute; + z-index: 1; + padding: @SmallSpace; + border-radius: 3px; + + &:after { + content: ""; + position: absolute; + border-style: solid; + border-color: transparent @BaseDark transparent transparent; + border-width: 0px @DefaultSpace 6px @DefaultSpace; + top: -5px; + left: 30px; + width: 0; + height: 0; + border-color: @InfoPointerColor transparent; + } + } + } + + .shareLink { + width: 300px; + background-color: #ffffff; + border: 1px solid @BaseMedium; + overflow: hidden; + text-overflow: ellipsis; + padding: 2px @SmallSpace 2px @SmallSpace; + margin: @SmallSpace @DefaultSpace 0px 0px; + font-family: @DataExplorerFont; + } + } + } + } + + .openFullScreenBtn { + background-color: @AccentMediumHigh; + color: @BaseLight; + padding: @SmallSpace 28px; + } + + .shareCancelButton { + background-color: @BaseLight; + color: @AccentMediumHigh; + padding: @SmallSpace 24px; + } + + .openFullScreenCancelBtn { + margin: @SmallSpace @DefaultSpace @SmallSpace @SmallSpace; + border: 1px solid @AccentMediumHigh; + cursor: pointer; + font-size: @mediumFontSize; + border-radius: 0px; + font-family: @DataExplorerFont; + } +} + +.connectExplorerContainer { + height: 100%; + width: 100%; + + .connectExplorerFormContainer { + .flex-display(); + .flex-direction(); + height: 100%; + width: 100%; + } + + .connectExplorer { + text-align: center; + .flex-display(); + .flex-direction(); + justify-content: center; + height: 100%; + margin-bottom: 60px; // this is to align the water mark in center between the top command bar and the notification console + + .welcomeText { + font-size: @DefaultFontSize; + color: @BaseHigh; + margin: @DefaultSpace @DefaultSpace @LargeSpace @DefaultSpace; } - .openFullScreenBtn { - background-color: @AccentMediumHigh; - color: @BaseLight; - padding: @SmallSpace 28px; + .switchConnectTypeText { + margin: @DefaultSpace; + font-size: @mediumFontSize; + color: @AccentMediumHigh; + cursor: pointer; } - .shareCancelButton { - background-color: @BaseLight; - color: @AccentMediumHigh; - padding: @SmallSpace 24px; + .connectStringText { + font-size: @mediumFontSize; + color: @BaseHigh; } - .openFullScreenCancelBtn { - margin: @SmallSpace @DefaultSpace @SmallSpace @SmallSpace; - border: 1px solid @AccentMediumHigh; + .connectExplorerContent { + margin: @DefaultSpace; + color: @BaseHigh; + + .inputToken { + width: 300px; + padding: 0px @SmallSpace; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + + &::placeholder { + font-style: italic; + } + } + + .errorDetailsInfoTooltip { + .infoTooltip(); + padding-left: @SmallSpace; + vertical-align: top; + + &:hover .errorDetails { + .tooltipVisible(); + } + + .errorDetails { + bottom: (2 * @MediumSpace); + width: @ErrorDetailsInfowidth; + visibility: hidden; + background-color: @BaseHigh; + color: @BaseLight; + position: absolute; + z-index: 1; + left: -10px; + padding: 6px; + + &:after { + border-width: 10px 10px 0px 10px; + bottom: -8px; + content: ""; + position: absolute; + right: 100%; + border-style: solid; + left: @MediumSpace; + width: 0; + height: 0; + border-color: @InfoPointerColor transparent; + } + } + + .errorImg { + height: @ImgWidth; + width: @ImgHeight; + } + } + } + } +} + +.dataExplorerLoaderContainer { + /* this should have more z-index value than the splitter to disable it */ + z-index: 5; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: @BaseLight; + opacity: @GreyOutOpacity; + + .dataExplorerLoader { + height: 8px; + margin-top: 50vh; + margin-left: 50%; + } +} + +.splashLoaderContainer { + z-index: 5; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: @BaseLight; + opacity: @GreyOutOpacity; + + .splashLoaderContentContainer { + .flex-display(); + .flex-direction(); + height: 100%; + text-align: center; + justify-content: center; + + .splashLoaderText { + margin-top: @LargeSpace; + } + + .splashLoaderTitle { + font-size: @DefaultFontSize; + color: @BaseHigh; + margin: @DefaultSpace @DefaultSpace @LargeSpace @DefaultSpace; + } + + .splashLoader { + display: block; + margin: @DefaultSpace auto; + width: @LoaderWidth; + height: @LoaderHeight; + } + } +} + +.dataExplorerPaneLoaderContainer { + right: 0; + width: 440px; + min-height: 565px; +} + +.dataExplorerTabLoaderContainer { + left: initial; + top: initial; + z-index: 0; +} + +.dataExplorerErrorConsoleContainer { + /* z-index should be greater than that of the splitter so it always overlaps the splitter */ + z-index: 2; + align-self: flex-end; + width: 100%; + .flex-display(); +} + +.ui-dialog.ui-corner-all.ui-widget.ui-widget-content.ui-front.no-close.ui-dialog-buttons { + border: 1px solid @BaseMedium; + box-shadow: 0 0 @DefaultSpace @BoxShadow; + padding: 0px; + + .ui-widget-header.ui-helper-clearfix.ui-dialog-titlebar.connectTitlebar { + background-color: @BaseLight; + font-size: @largeFontSize; + color: @BaseHigh; + font-family: @DataExplorerFont; + border: none; + font-weight: normal; + padding: @SmallSpace; + margin: @DefaultSpace @LargeSpace @MediumSpace (2 * @MediumSpace); + + .ui-button.ui-corner-all.ui-widget.ui-button-icon-only.ui-dialog-titlebar-close { + visibility: hidden; + } + } + + .dataAccessTokenModal { + margin: @LargeSpace 24px 24px; + padding: @SmallSpace; + overflow: visible; + .dataAccessTokenModalContent .dataAccessTokenExpireText { + margin-bottom: @LargeSpace; + color: @BaseHigh; + font-size: @DefaultFontSize; + font-family: @DataExplorerFont; + } + } + + .ui-dialog-buttonpane.ui-widget-content.ui-helper-clearfix { + border-top: @ButtonBorderWidth solid @BaseMediumLow; + padding: @LargeSpace 20px; + + .ui-dialog-buttonset { + float: none; + + .connectDialogButtons { + margin: @SmallSpace 0px @SmallSpace @MediumSpace; + border: @ButtonBorderWidth solid @AccentMediumHigh; cursor: pointer; font-size: @mediumFontSize; border-radius: 0px; font-family: @DataExplorerFont; - } -} + } -.connectExplorerContainer { - height: 100%; - width: 100%; + .connectButton { + padding: @SmallSpace @LargeSpace; + } - .connectExplorerFormContainer { - .flex-display(); - .flex-direction(); - height: 100%; - width: 100%; - } + .okBtn { + padding: @SmallSpace 30px; + } - .connectExplorer { - text-align: center; - .flex-display(); - .flex-direction(); - justify-content: center; - height: 100%; - margin-bottom: 60px; // this is to align the water mark in center between the top command bar and the notification console + .connectOkBtns { + background-color: @AccentMediumHigh; + color: @BaseLight; + } - .welcomeText { - font-size: @DefaultFontSize; - color: @BaseHigh; - margin: @DefaultSpace @DefaultSpace @LargeSpace @DefaultSpace; - } - - .switchConnectTypeText { - margin: @DefaultSpace; - font-size: @mediumFontSize; - color: @AccentMediumHigh; - cursor: pointer; - } - - .connectStringText { - font-size: @mediumFontSize; - color: @BaseHigh; - } - - .connectExplorerContent { - margin: @DefaultSpace; - color: @BaseHigh; - - .inputToken { - width: 300px; - padding: 0px @SmallSpace; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - - &::placeholder { - font-style: italic; - } - } - - .errorDetailsInfoTooltip { - .infoTooltip(); - padding-left: @SmallSpace; - vertical-align: top; - - &:hover .errorDetails { - .tooltipVisible(); - } - - .errorDetails { - bottom: (2 * @MediumSpace); - width: @ErrorDetailsInfowidth; - visibility: hidden; - background-color: @BaseHigh; - color: @BaseLight; - position: absolute; - z-index: 1; - left: -10px; - padding: 6px; - - &:after { - border-width: 10px 10px 0px 10px; - bottom: -8px; - content: ""; - position: absolute; - right: 100%; - border-style: solid; - left: @MediumSpace; - width: 0; - height: 0; - border-color: @InfoPointerColor transparent; - } - } - - .errorImg { - height: @ImgWidth; - width: @ImgHeight; - } - } - } - } -} - -.dataExplorerLoaderContainer { - /* this should have more z-index value than the splitter to disable it */ - z-index: 5; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: @BaseLight; - opacity: @GreyOutOpacity; - - .dataExplorerLoader { - height: 8px; - margin-top: 50vh; - margin-left: 50%; - } -} - -.splashLoaderContainer { - z-index: 5; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: @BaseLight; - opacity: @GreyOutOpacity; - - .splashLoaderContentContainer { - .flex-display(); - .flex-direction(); - height: 100%; - text-align: center; - justify-content: center; - - .splashLoaderText { - margin-top: @LargeSpace; - } - - .splashLoaderTitle { - font-size: @DefaultFontSize; - color: @BaseHigh; - margin: @DefaultSpace @DefaultSpace @LargeSpace @DefaultSpace; - } - - .splashLoader { - display: block; - margin: @DefaultSpace auto; - width: @LoaderWidth; - height: @LoaderHeight; - } - } -} - -.dataExplorerPaneLoaderContainer { - right: 0; - width: 440px; - min-height: 565px; -} - -.dataExplorerTabLoaderContainer { - left: initial; - top: initial; - z-index: 0; -} - -.dataExplorerErrorConsoleContainer { - /* z-index should be greater than that of the splitter so it always overlaps the splitter */ - z-index: 2; - align-self: flex-end; - width: 100%; - .flex-display(); -} - -.ui-dialog.ui-corner-all.ui-widget.ui-widget-content.ui-front.no-close.ui-dialog-buttons { - border: 1px solid @BaseMedium; - box-shadow:0 0 @DefaultSpace @BoxShadow; - padding: 0px; - - .ui-widget-header.ui-helper-clearfix.ui-dialog-titlebar.connectTitlebar { + .cancelBtn { background-color: @BaseLight; - font-size: @largeFontSize; - color: @BaseHigh; - font-family: @DataExplorerFont; - border: none; - font-weight: normal; - padding: @SmallSpace; - margin: @DefaultSpace @LargeSpace @MediumSpace (2 * @MediumSpace); - - .ui-button.ui-corner-all.ui-widget.ui-button-icon-only.ui-dialog-titlebar-close { - visibility: hidden; - } - } - - .dataAccessTokenModal { - margin: @LargeSpace 24px 24px; - padding: @SmallSpace; - overflow: visible; - .dataAccessTokenModalContent .dataAccessTokenExpireText { - margin-bottom: @LargeSpace; - color: @BaseHigh; - font-size: @DefaultFontSize; - font-family: @DataExplorerFont; - } - } - - .ui-dialog-buttonpane.ui-widget-content.ui-helper-clearfix { - border-top: @ButtonBorderWidth solid @BaseMediumLow; - padding: @LargeSpace 20px; - - .ui-dialog-buttonset { - float: none; - - .connectDialogButtons { - margin: @SmallSpace 0px @SmallSpace @MediumSpace; - border: @ButtonBorderWidth solid @AccentMediumHigh; - cursor: pointer; - font-size: @mediumFontSize; - border-radius: 0px; - font-family: @DataExplorerFont; - } - - .connectButton { - padding: @SmallSpace @LargeSpace; - } - - .okBtn { - padding: @SmallSpace 30px; - } - - .connectOkBtns { - background-color: @AccentMediumHigh; - color: @BaseLight; - } - - .cancelBtn { - background-color: @BaseLight; - color: @AccentMediumHigh; - margin-left: @DefaultSpace; - padding: @SmallSpace 20px; - } - } + color: @AccentMediumHigh; + margin-left: @DefaultSpace; + padding: @SmallSpace 20px; + } } + } } .ui-widget-overlay.ui-front { - background-color: @BaseLight; - opacity: @GreyOutOpacity; + background-color: @BaseLight; + opacity: @GreyOutOpacity; } .renewAccessInfo { - color: @BaseHigh; - padding-right: (2 * @LargeSpace); - margin-bottom: (2 * @MediumSpace); + color: @BaseHigh; + padding-right: (2 * @LargeSpace); + margin-bottom: (2 * @MediumSpace); } .renewUploadItemsHeader { - margin-bottom: @DefaultSpace; - color: @BaseHigh; + margin-bottom: @DefaultSpace; + color: @BaseHigh; } .accessKeyInput { - width: @AccessKeyInputWidth; - padding: 0px @SmallSpace; + width: @AccessKeyInputWidth; + padding: 0px @SmallSpace; - &::placeholder { - font-style: italic; - } + &::placeholder { + font-style: italic; + } } .renewAccessExpandCollapse { - margin-top: 24px; - cursor: pointer; + margin-top: 24px; + cursor: pointer; - img { - width: @AccountNavigationExpandCollapseSize; - height: @AccountNavigationExpandCollapseSize; - margin-bottom: @SmallSpace; - } + img { + width: @AccountNavigationExpandCollapseSize; + height: @AccountNavigationExpandCollapseSize; + margin-bottom: @SmallSpace; + } } .AccountNavigationText { - font-size: @mediumFontSize; - font-family: @DataExplorerFont; + font-size: @mediumFontSize; + font-family: @DataExplorerFont; } .renewAccessImg { - margin: @DefaultSpace @MediumSpace 0px @LargeSpace; + margin: @DefaultSpace @MediumSpace 0px @LargeSpace; - img { - margin-top: @DefaultSpace; - width: @AccountNavigationImgWidth; - height: @AccountNavigationImgHeight; - } + img { + margin-top: @DefaultSpace; + width: @AccountNavigationImgWidth; + height: @AccountNavigationImgHeight; + } } .importFilesTitle { - overflow: hidden; - text-overflow: ellipsis; - background-color: @BaseLight; - border: 1px solid @BaseMedium; - height: 24px; - width: 300px; - padding: 0px @DefaultSpace 0px @DefaultSpace; + overflow: hidden; + text-overflow: ellipsis; + background-color: @BaseLight; + border: 1px solid @BaseMedium; + height: 24px; + width: 300px; + padding: 0px @DefaultSpace 0px @DefaultSpace; } .sparkWorkerCountInput { - margin-top: 5px; - width: 100%; - padding: 1px; + margin-top: 5px; + width: 100%; + padding: 1px; } .fileImportImg { - padding: @SmallSpace; - vertical-align: top; - border: @ButtonBorderWidth solid transparent; - &:hover { - background-color: @BaseMediumLow; - } - &:active { - background-color: @BaseMediumLow; - } - &:focus { - .focus(); - } + padding: @SmallSpace; + vertical-align: top; + border: @ButtonBorderWidth solid transparent; + &:hover { + background-color: @BaseMediumLow; + } + &:active { + background-color: @BaseMediumLow; + } + &:focus { + .focus(); + } } .fileImportButton { - height: 24px; - border: @ButtonBorderWidth solid transparent; - vertical-align: top; + height: 24px; + border: @ButtonBorderWidth solid transparent; + vertical-align: top; } .fileUploadSummaryContainer { - margin-top: 40px; + margin-top: 40px; - .fileUploadSummary { - margin-top: @DefaultSpace; - width: calc(~"100% - 25px"); - table-layout: fixed; + .fileUploadSummary { + margin-top: @DefaultSpace; + width: calc(~"100% - 25px"); + table-layout: fixed; - .fileUploadSummaryHeader { - font-size: 10px; - } - - .fileUploadSummaryTuple { - text-overflow: ellipsis; - overflow: hidden; - border-bottom: 1px solid @BaseMedium; - height: 28px; - - td { - overflow: hidden; - text-overflow: ellipsis; - } - } + .fileUploadSummaryHeader { + font-size: 10px; } + + .fileUploadSummaryTuple { + text-overflow: ellipsis; + overflow: hidden; + border-bottom: 1px solid @BaseMedium; + height: 28px; + + td { + overflow: hidden; + text-overflow: ellipsis; + } + } + } } execute-sproc-params-pane { - @textInputWidth: 200px; - @textInputHeight: 24px; - @dataTypeSelectorWidth: 100px; - @paramTableTypeWidth: 110px; + @textInputWidth: 200px; + @textInputHeight: 24px; + @dataTypeSelectorWidth: 100px; + @paramTableTypeWidth: 110px; - .partitionKeyContainer, - .paramsTable { - padding-bottom: @DefaultSpace; + .partitionKeyContainer, + .paramsTable { + padding-bottom: @DefaultSpace; - .inputHeader, - .enterInputParams { - margin-bottom: @SmallSpace; - font-size: @DefaultFontSize; - } - - .scrollBox { - width: 100%; - overflow: auto; - overflow-x: hidden; - padding-bottom: @SmallSpace; - - .paramsClauseTable { - border-spacing: 0px; - display: table; - width: 100%; - margin-top: 8px; - - .paramTableTypeHead { - width: @paramTableTypeWidth; - } - - .paramTemplateRow { - padding-top: @MediumSpace; - } - - .dataTypeSelector { - width: @dataTypeSelectorWidth; - height: @textInputHeight; - border: @ButtonBorderWidth solid @BaseMedium; - color: @BaseHigh; - padding-left: @SmallSpace; - } - - .partitionKeyValue, - .valueTextBox { - width: @textInputWidth; - height: @textInputHeight; - border: @ButtonBorderWidth solid @BaseMedium; - padding: @SmallSpace @DefaultSpace; - } - - .partitionKeyValue { - margin-right: 30px; - } - - .spEntityAddCancel { - margin-right: 1px; - cursor: pointer; - - &:hover { - .hover(); - } - - &:active { - .active(); - } - - &:focus { - .focus(); - } - - img { - width: @ImgWidth; - height: @ImgHeight; - margin: 0px 0px @SmallSpace @SmallSpace; - } - } - } - } - - .addNewParam { - padding: @DefaultSpace 0px 5px @SmallSpace; - margin-top: @MediumSpace; - width: 125px; - cursor: pointer; - - &:hover { - .hover(); - } - - &:active { - .active(); - } - - &:focus { - .focus(); - } - - img { - width: @ImgWidth; - height: @ImgHeight; - margin-bottom: @SmallSpace; - } - - .addNewParamLabel { - margin-left: @SmallSpace; - } - } + .inputHeader, + .enterInputParams { + margin-bottom: @SmallSpace; + font-size: @DefaultFontSize; } + + .scrollBox { + width: 100%; + overflow: auto; + overflow-x: hidden; + padding-bottom: @SmallSpace; + + .paramsClauseTable { + border-spacing: 0px; + display: table; + width: 100%; + margin-top: 8px; + + .paramTableTypeHead { + width: @paramTableTypeWidth; + } + + .paramTemplateRow { + padding-top: @MediumSpace; + } + + .dataTypeSelector { + width: @dataTypeSelectorWidth; + height: @textInputHeight; + border: @ButtonBorderWidth solid @BaseMedium; + color: @BaseHigh; + padding-left: @SmallSpace; + } + + .partitionKeyValue, + .valueTextBox { + width: @textInputWidth; + height: @textInputHeight; + border: @ButtonBorderWidth solid @BaseMedium; + padding: @SmallSpace @DefaultSpace; + } + + .partitionKeyValue { + margin-right: 30px; + } + + .spEntityAddCancel { + margin-right: 1px; + cursor: pointer; + + &:hover { + .hover(); + } + + &:active { + .active(); + } + + &:focus { + .focus(); + } + + img { + width: @ImgWidth; + height: @ImgHeight; + margin: 0px 0px @SmallSpace @SmallSpace; + } + } + } + } + + .addNewParam { + padding: @DefaultSpace 0px 5px @SmallSpace; + margin-top: @MediumSpace; + width: 125px; + cursor: pointer; + + &:hover { + .hover(); + } + + &:active { + .active(); + } + + &:focus { + .focus(); + } + + img { + width: @ImgWidth; + height: @ImgHeight; + margin-bottom: @SmallSpace; + } + + .addNewParamLabel { + margin-left: @SmallSpace; + } + } + } } stored-procedure-tab { - @ToggleHeight: 30px; - @ToggleWidth: 180px; + @ToggleHeight: 30px; + @ToggleWidth: 180px; - .results-container, .errors-container { - padding: @MediumSpace 0px 0px @MediumSpace; - height: 100%; - .flex-display(); - .flex-direction(); + .results-container, + .errors-container { + padding: @MediumSpace 0px 0px @MediumSpace; + height: 100%; + .flex-display(); + .flex-direction(); + overflow: hidden; + + .toggles { + height: @ToggleHeight; + width: @ToggleWidth; + margin-left: @MediumSpace; + + &:focus { + .focus(); + } + + .tab { + margin-right: @MediumSpace; + } + + .toggleSwitch { + .toggleSwitch(); + } + + .selectedToggle { + .selectedToggle(); + } + + .unselectedToggle { + .unselectedToggle(); + } + } + + .enterInputParameters { + padding: @LargeSpace @MediumSpace; + } + } + + .errors-container { + padding-left: (2 * @MediumSpace); + .errors-header { + font-weight: 700; + font-size: @DefaultFontSize; + padding-bottom: @DefaultSpace; + } + + .errorContent { + .flex-display(); + width: 60%; + white-space: nowrap; + font-size: @mediumFontSize; + + .errorMessage { + padding: @SmallSpace; overflow: hidden; - - .toggles { - height: @ToggleHeight; - width: @ToggleWidth; - margin-left: @MediumSpace; - - &:focus { - .focus(); - } - - .tab { - margin-right: @MediumSpace; - } - - .toggleSwitch { - .toggleSwitch(); - } - - .selectedToggle { - .selectedToggle(); - } - - .unselectedToggle { - .unselectedToggle(); - } - } - - .enterInputParameters { - padding: @LargeSpace @MediumSpace; - } + text-overflow: ellipsis; + } } - .errors-container { - padding-left: (2 * @MediumSpace); - .errors-header { - font-weight: 700; - font-size: @DefaultFontSize; - padding-bottom: @DefaultSpace; - } - - .errorContent { - .flex-display(); - width: 60%; - white-space: nowrap; - font-size: @mediumFontSize; - - .errorMessage { - padding: @SmallSpace; - overflow: hidden; - text-overflow: ellipsis; - } - } - - .errorDetailsLink { - cursor: pointer; - padding: @SmallSpace; - } + .errorDetailsLink { + cursor: pointer; + padding: @SmallSpace; } + } - editor { - .flex-display(); - .flex-direction(); - overflow: hidden; - height: 100%; - width: 100%; - padding-left: 22px; - } + editor { + .flex-display(); + .flex-direction(); + overflow: hidden; + height: 100%; + width: 100%; + padding-left: 22px; + } - json-editor { - .flex-display(); - .flex-direction(); - overflow: hidden; - height: 100%; - width: 100%; - padding: @SmallSpace 0px @SmallSpace 10px; - } + json-editor { + .flex-display(); + .flex-direction(); + overflow: hidden; + height: 100%; + width: 100%; + padding: @SmallSpace 0px @SmallSpace 10px; + } } notification-console { - width: 100%; + width: 100%; } #divQuickStart { - display: inline-block; - width: 100%; + display: inline-block; + width: 100%; } #imgiconwidth1 { - width: 72%; + width: 72%; } #Quickstart { - text-align: center; - width: 80px; - height: 60px; - margin: 0 auto; - padding-top: 5px; - position: relative; + text-align: center; + width: 80px; + height: 60px; + margin: 0 auto; + padding-top: 5px; + position: relative; } .flexContainer { - height:100%; - width:100%; - .flex-display(); - .flex-direction(); + height: 100%; + width: 100%; + .flex-display(); + .flex-direction(); } .hideOverflows { - overflow: hidden; + overflow: hidden; } .explorerContent { - flex-grow: 1; + flex-grow: 1; } .collectionheading { - text-transform: uppercase; - font-size: 10px; + text-transform: uppercase; + font-size: 10px; } #Quickstart #imgiconwidth1 { - width: 24px; - height: 24px; - position: absolute; - right: 27px; + width: 24px; + height: 24px; + position: absolute; + right: 27px; } .topSelected { - border-left: 4px solid @AccentMediumHigh; - background: #666666; + border-left: 4px solid @AccentMediumHigh; + background: #666666; } .topSelected:hover { - border-left: 4px solid @AccentMediumHigh; - background: #666666!important; - cursor: default!important; + border-left: 4px solid @AccentMediumHigh; + background: #666666 !important; + cursor: default !important; } #Quickstart:hover span.activemenu, #Quickstart:active span.activemenu { - color: #fff; + color: #fff; } #Explorer:hover span.menuExplorer, #Explorer:active span.menuExplorer { - color: #fff; + color: #fff; } menuQuickStart { - margin-left: 0; - padding-left: 0; - display: block; - right: 12px; - top: 30px; - position: absolute; + margin-left: 0; + padding-left: 0; + display: block; + right: 12px; + top: 30px; + position: absolute; } #Explorer { - text-align: center; - display: inline-block; - width: 80px; - height: 60px; - margin: 0 auto; - padding-top: 9px; - position: relative; + text-align: center; + display: inline-block; + width: 80px; + height: 60px; + margin: 0 auto; + padding-top: 9px; + position: relative; } #Explorer #imgiconwidth1, .feedbackstyle #imgiconwidth1, .settingstyle #imgiconwidth1 { - width: 24px; - height: 24px; - position: absolute; - right: 30px; + width: 24px; + height: 24px; + position: absolute; + right: 30px; } #Explorer span.menuExplorer { - margin-left: 0; - padding-left: 0; - display: block; - right: 19px; - top: 33px; - position: absolute; + margin-left: 0; + padding-left: 0; + display: block; + right: 19px; + top: 33px; + position: absolute; } .feedbackstyle span.menuExplorer { - margin-left: 0; - padding-left: 0; - display: block; - right: 19px; - top: 33px; - position: absolute; + margin-left: 0; + padding-left: 0; + display: block; + right: 19px; + top: 33px; + position: absolute; } .settingstyle span.menuExplorer { - margin-left: 0; - padding-left: 0; - display: block; - right: 19px; - top: 33px; - position: absolute; + margin-left: 0; + padding-left: 0; + display: block; + right: 19px; + top: 33px; + position: absolute; } .content { - display: inline-block; - width: 100%; - transition: all .4s ease-in-out; - -ms-transition: all .4s ease-in-out; - -webkit-transition: all .4s ease-in-out; - -moz-transition: all .4s ease-in-out; - height: 100vh; + display: inline-block; + width: 100%; + transition: all 0.4s ease-in-out; + -ms-transition: all 0.4s ease-in-out; + -webkit-transition: all 0.4s ease-in-out; + -moz-transition: all 0.4s ease-in-out; + height: 100vh; } .mini { - width: 0%; - float: left; - transition: all .4s ease-in-out; - -webkit-transition: all .4s ease-in-out; - -moz-transition: all .4s ease-in-out; - height: 100vh; - background-color: white; + width: 0%; + float: left; + transition: all 0.4s ease-in-out; + -webkit-transition: all 0.4s ease-in-out; + -moz-transition: all 0.4s ease-in-out; + height: 100vh; + background-color: white; } #sidebar-wrapper { - z-index: 1000; - position: fixed; - left: 250px; - width: 0; - height: 100%; - margin-left: -250px; - overflow-y: auto; - background: white; - -webkit-transition: all 0.5s ease; - -moz-transition: all 0.5s ease; - -o-transition: all 0.5s ease; - transition: all 0.5s ease; + z-index: 1000; + position: fixed; + left: 250px; + width: 0; + height: 100%; + margin-left: -250px; + overflow-y: auto; + background: white; + -webkit-transition: all 0.5s ease; + -moz-transition: all 0.5s ease; + -o-transition: all 0.5s ease; + transition: all 0.5s ease; } .toggle-left { - width: 0%; - overflow: hidden; + width: 0%; + overflow: hidden; } .toggle-minicontent { - width: 100%; + width: 100%; } .toggle-maincontent { - width: 100%; + width: 100%; } .toggle-mini { - width: 36px; + width: 36px; } .toggle-main { - width: 100%; + width: 100%; } .activepartitionmode { - background-color: @AccentMediumHigh; + background-color: @AccentMediumHigh; } .paddingpartition { - color: white; - padding-left: 15px; - padding-top: 25px; + color: white; + padding-left: 15px; + padding-top: 25px; } .paddingspan2 { - padding-top: 20px; - color: #000; - padding-left: 15px; + padding-top: 20px; + color: #000; + padding-left: 15px; } .paddingspan4 { - padding-top: 20px; - padding-bottom: 20px; - color: white; - padding-left: 15px; + padding-top: 20px; + padding-bottom: 20px; + color: white; + padding-left: 15px; } .whitegroove { - width: 344px; - border: groove; + width: 344px; + border: groove; } .dropdownbtn { - color: white; - width: 340px; - background: #262626; + color: white; + width: 340px; + background: #262626; } .queryclr { - color: white; - background: #262626; + color: white; + background: #262626; } .panelContent { - display: flex; - flex-direction: column; - flex: 1; + display: flex; + flex-direction: column; + flex: 1; } .panelContentWrapper { - display: flex; - flex-direction: column; - height: 100%; + display: flex; + flex-direction: column; + height: 100%; } .contextual-pane { - top: 0px; - right: 0 !important; - left: auto; - -webkit-box-flex: 0; - -webkit-flex: 0 0 auto; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - right: auto; - z-index: 1000 !important; - -webkit-align-self: auto !important; - -ms-flex-item-align: auto !important; - align-self: auto !important; - height: 100%; - position: absolute; - width: 440px; - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - font: inherit; - vertical-align: baseline; - outline: none; - background-color: #fff; - /* border-left: 1px solid #cbcbcb; */ - -webkit-animation: mymove 0.2s; - -webkit-animation-timing-function: cubic-bezier(0.47, 0, 0.75, 0.72); - animation: mymove 0.2s; - animation-timing-function: cubic-bezier(0.22, 0.61, 0.36, 1); - box-shadow: -5px 0px 7px 0px #cbcbcb; + top: 0px; + right: 0 !important; + left: auto; + -webkit-box-flex: 0; + -webkit-flex: 0 0 auto; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + right: auto; + z-index: 1000 !important; + -webkit-align-self: auto !important; + -ms-flex-item-align: auto !important; + align-self: auto !important; + height: 100%; + position: absolute; + width: 440px; + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; + outline: none; + background-color: #fff; + /* border-left: 1px solid #cbcbcb; */ + -webkit-animation: mymove 0.2s; + -webkit-animation-timing-function: cubic-bezier(0.47, 0, 0.75, 0.72); + animation: mymove 0.2s; + animation-timing-function: cubic-bezier(0.22, 0.61, 0.36, 1); + box-shadow: -5px 0px 7px 0px #cbcbcb; } @keyframes mymove { - from { - right: -1000px; - } - to { - right: 0px; - } + from { + right: -1000px; + } + to { + right: 0px; + } } .contextual-pane-out { - width: 100%; - height: 100vh; - z-index: 1; - position: absolute; - top: 0px; + width: 100%; + height: 100vh; + z-index: 1; + position: absolute; + top: 0px; } .contextual-pane-in { - width: 100%; - height: 100%; + width: 100%; + height: 100%; } .pointer { - cursor: pointer; + cursor: pointer; } -#tbodycontent>tr>td { - border-bottom: 1px solid #CCCCCC; - border-top: none; - padding: 6px; +#tbodycontent > tr > td { + border-bottom: 1px solid #cccccc; + border-top: none; + padding: 6px; } -#tbodycontent>tr:last-child>td { - border-bottom: 1px solid #ddd; +#tbodycontent > tr:last-child > td { + border-bottom: 1px solid #ddd; } .documentsTabGridAndEditor { + height: 100%; + overflow: hidden; + .flex-display(); + + &.documentsTabGridAndEditorUpperPadding { + padding-top: 16px; + } + + .documentsContainerWithSplitter { + height: 100% !important; + width: 200px; + padding-left: 8px; + flex-shrink: 0; + } + + .documentWaterMark { + margin: auto; + height: 35vh; // this is to align the water mark in center of the layout + text-align: center; + + p { + margin-bottom: @LargeSpace; + } + + .documentWaterMarkText { + color: @BaseHigh; + font-size: @DefaultFontSize; + font-family: @DataExplorerFont; + } + + .loadErrorIcon { + width: 128px; + height: 128px; + } + + .loadErrorDetailsLink { + cursor: pointer; + } + } + + json-editor { + padding-top: 28px; + padding-left: @MediumSpace; height: 100%; + width: 100%; overflow: hidden; .flex-display(); - &.documentsTabGridAndEditorUpperPadding { - padding-top: 16px; + .jsonEditor { + border: none; } + } - .documentsContainerWithSplitter { - height: 100% !important; - width: 200px; - padding-left: 8px; - flex-shrink: 0; + diff-editor { + padding-top: 28px; + height: 100%; + width: 100%; + overflow: hidden; + .flex-display(); + + .jsonEditor { + border: none; } + } - .documentWaterMark { - margin: auto; - height: 35vh; // this is to align the water mark in center of the layout - text-align: center; + .conflictEditorContainer { + width: 100%; - p { - margin-bottom: @LargeSpace; - } + .conflictEditorHeader { + padding: 6px 12px; + width: 100%; + overflow: hidden; - .documentWaterMarkText { - color: @BaseHigh; - font-size: @DefaultFontSize; - font-family: @DataExplorerFont; - } - - .loadErrorIcon { - width:128px; - height:128px; - } - - .loadErrorDetailsLink { - cursor: pointer; - } + .conflictEditorHeaderLabel { + width: 50%; + float: left; + color: @BaseDark; + font-weight: bold; + } } json-editor { - padding-top: 28px; - padding-left: @MediumSpace; - height: 100%; - width:100%; - overflow: hidden; - .flex-display(); - - .jsonEditor { - border: none; - } + padding-top: 0; } diff-editor { - padding-top: 28px; - height: 100%; - width:100%; - overflow: hidden; - .flex-display(); - - .jsonEditor { - border: none; - } - } - - .conflictEditorContainer { - width: 100%; - - .conflictEditorHeader { - padding: 6px 12px; - width: 100%; - overflow: hidden; - - .conflictEditorHeaderLabel { - width: 50%; - float: left; - color: @BaseDark; - font-weight: bold; - } - } - - json-editor { - padding-top: 0; - } - - diff-editor { - padding-top: 0; - height: calc(~"100% - 30px"); - } + padding-top: 0; + height: calc(~"100% - 30px"); } + } } .gridRowSelected { - .active(); + .active(); } .gridRowSelected:hover { - cursor: default; - .hover() + cursor: default; + .hover(); } .gridRowHighlighted { - border-style: dotted; - border-width: 2px; + border-style: dotted; + border-width: 2px; } -.table-hover>tbody>tr:hover { - .hover(); +.table-hover > tbody > tr:hover { + .hover(); } .collectionNodeSelected { - .active(); + .active(); } .collectionNodeSelected:hover { - cursor: default; - .hover(); + cursor: default; + .hover(); } .databaseNodeSelected { - .active(); + .active(); } .databaseNodeSelected:hover { - cursor: default; - .hover(); + cursor: default; + .hover(); } .leftsidepanle-hr { - margin: 16px 0px; - border-top: 1px solid #eee; - margin-left: -17px; - width: 100%; - color: 1px solid #53575B; + margin: 16px 0px; + border-top: 1px solid #eee; + margin-left: -17px; + width: 100%; + color: 1px solid #53575b; } .partitioning-btn { - padding-bottom: 16px; + padding-bottom: 16px; } .btncreatecoll1 { - border: 1px solid @AccentMediumHigh; - background-color: @AccentMediumHigh; - color: #fff; - padding: 2px 30px; - cursor: pointer; - font-size: 12px; + border: 1px solid @AccentMediumHigh; + background-color: @AccentMediumHigh; + color: #fff; + padding: 2px 30px; + cursor: pointer; + font-size: 12px; - &:active { - border-color: #0072c6; - background-color: #0072c6; - } + &:active { + border-color: #0072c6; + background-color: #0072c6; + } } .leftpanel-okbut .genericPaneSubmitBtn { - border: 1px solid @AccentMediumHigh; - background-color: @AccentMediumHigh; - color: #fff; - cursor: pointer; - font-size: 12px; - height: 24px; + border: 1px solid @AccentMediumHigh; + background-color: @AccentMediumHigh; + color: #fff; + cursor: pointer; + font-size: 12px; + height: 24px; - &:active { - border-color: #0072c6; - background-color: #0072c6; - } + &:active { + border-color: #0072c6; + background-color: #0072c6; + } } .btncreatecoll1-off { - border: 1px solid #969696; - background-color: #000; - color: white; - padding: 2px 30px; - cursor: pointer; - font-size: 12px; - margin-left: -5px; + border: 1px solid #969696; + background-color: #000; + color: white; + padding: 2px 30px; + cursor: pointer; + font-size: 12px; + margin-left: -5px; } .leftpanel-okbut { - padding: 20px 0px 24px 30px; + padding: 20px 0px 24px 30px; } .btnpricepad { - margin-left: 24px; + margin-left: 24px; } .btnSetupQueries { - margin-top: @LargeSpace; + margin-top: @LargeSpace; } .collid { - background: #fff; - width: @newCollectionPaneInputWidth; + background: #fff; + width: @newCollectionPaneInputWidth; } .textfontclr { - color: #000; + color: #000; } .collid-white { - width: 100%; - border: solid 1px #DDD; + width: 100%; + border: solid 1px #ddd; } .plusimg-but { - background-image: url(../images/plus_normal.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/plus_normal.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .plusimg-but:hover { - background-image: url(../images/plus_hover.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/plus_hover.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .plusimg-but:active { - background-image: url(../images/plus_pressed.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/plus_pressed.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .plusimg-but:disabled { - background-image: url(../images/plus_disabled.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/plus_disabled.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .minusimg-but { - background-image: url(../images/minus_normal.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/minus_normal.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .minusimg-but:hover { - background-image: url(../images/minus_hover.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/minus_hover.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .minusimg-but:active { - background-image: url(../images/minus_pressed.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/minus_pressed.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .minusimg-but:disabled { - background-image: url(../images/minus_disabled.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/minus_disabled.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .firstdivbg { - padding: @MediumSpace 0px @DefaultSpace (2 * @LargeSpace); - background-color: @BaseLight; + padding: @MediumSpace 0px @DefaultSpace (2 * @LargeSpace); + background-color: @BaseLight; } p { - margin: 0 0 4px; - color: #000; + margin: 0 0 4px; + color: #000; } .headerline .closePaneBtn { - float: right; - cursor: pointer; - width: 16px; - height: 100%; - margin-right: 4px; - color: #000; + float: right; + cursor: pointer; + width: 16px; + height: 100%; + margin-right: 4px; + color: #000; } .closeImg { - float: right; - cursor: pointer; - width: 16px; - height: 16px; - margin-right: 4px; + float: right; + cursor: pointer; + width: 16px; + height: 16px; + margin-right: 4px; - img { - height: 25px; - width: 25px; - } + img { + height: 25px; + width: 25px; + } } .closeImg[tabindex]:active { - outline: none; + outline: none; } .seconddivpadding { - padding-top: 16px; + padding-top: 16px; } .seconddivbg { - height: 100vh; - padding-left: 32px; - padding-top: 16px; + height: 100vh; + padding-left: 32px; + padding-top: 16px; } .pkPadding { - padding-top: 12px; + padding-top: 12px; } .mandatoryStar { - color: #ff0707; - font-size: 14px; - font-weight: bold; + color: #ff0707; + font-size: 14px; + font-weight: bold; } .createNewDatabaseOrUseExisting { - margin-bottom: @SmallSpace; + margin-bottom: @SmallSpace; - .createNewDatabaseOrUseExistingRadio { - vertical-align: text-bottom; - } + .createNewDatabaseOrUseExistingRadio { + vertical-align: text-bottom; + } - .createNewDatabaseOrUseExistingRadio:nth-child(n+2) { - margin-left: @LargeSpace; - } + .createNewDatabaseOrUseExistingRadio:nth-child(n + 2) { + margin-left: @LargeSpace; + } - .createNewDatabaseOrUseExistingSpace { - padding-left: @SmallSpace; - } + .createNewDatabaseOrUseExistingSpace { + padding-left: @SmallSpace; + } } .throughputModeContainer { - margin-bottom: @SmallSpace; + margin-bottom: @SmallSpace; - .throughputModeRadio { - vertical-align: text-bottom; - } + .throughputModeRadio { + vertical-align: text-bottom; + } - .nonFirstRadio { - margin-left: @LargeSpace; - } + .nonFirstRadio { + margin-left: @LargeSpace; + } - .throughputModeSpace { - padding-left: @SmallSpace; - } + .throughputModeSpace { + padding-left: @SmallSpace; + } } .databaseProvision { - margin-top: @SmallSpace; + margin-top: @SmallSpace; - input { - vertical-align: text-bottom; - } + input { + vertical-align: text-bottom; + } - .databaseProvisionText { - padding-left: @SmallSpace; - } + .databaseProvisionText { + padding-left: @SmallSpace; + } } .infoImg { - margin: 0px 0px 2px 2px; + margin: 0px 0px 2px 2px; } .largePartitionKey { - margin: @SmallSpace 0px; + margin: @SmallSpace 0px; - input { - vertical-align: text-bottom; - } + input { + vertical-align: text-bottom; + } - .largePartitionKeyDescription { - margin: @DefaultSpace 0px 0px; - } + .largePartitionKeyDescription { + margin: @DefaultSpace 0px 0px; + } } .enableAnalyticalStorage { - margin: @SmallSpace 0px; + margin: @SmallSpace 0px; - input { - vertical-align: text-bottom; - } + input { + vertical-align: text-bottom; + } } .infoTooltip { - .infoTooltip(); + .infoTooltip(); } .infoTooltip .tooltiptext { - top: 30px; - .tooltipText(); + top: 30px; + .tooltipText(); } .infoTooltip .tooltiptext::after { - border-width: 0px (2 * @MediumSpace) (2 * @MediumSpace) 0px; - top: -15px; - .tooltipTextAfter(); + border-width: 0px (2 * @MediumSpace) (2 * @MediumSpace) 0px; + top: -15px; + .tooltipTextAfter(); } .infoTooltip:hover .tooltiptext { - .tooltipVisible(); + .tooltipVisible(); } .infoTooltip:focus .tooltiptext { - .tooltipVisible(); + .tooltipVisible(); } .infoTooltip a { - color: @AccentHigh; + color: @AccentHigh; +} + +.inputTooltip { + .inputTooltip(); +} + +.inputTooltip .inputTooltipText { + top: -68px; + .inputTooltipText(); +} + +.inputTooltip .inputTooltipText::after { + border-width: @MediumSpace @MediumSpace 0 @MediumSpace; + top: 55px; + .inputTooltipTextAfter(); } .nowrap { - white-space: nowrap; + white-space: nowrap; } .leftAlignInfoTooltip { - .infoTooltip(); - white-space: normal; + .infoTooltip(); + white-space: normal; - .tooltiptext { - .tooltipText(); - top: 30px; - visibility: hidden; - left: -300px; + .tooltiptext { + .tooltipText(); + top: 30px; + visibility: hidden; + left: -300px; - &::after { - .tooltipTextAfter(); - border-width: 0px 0px (2 * @MediumSpace) (2 * @MediumSpace); - top: -15px; - left: 287px; - } + &::after { + .tooltipTextAfter(); + border-width: 0px 0px (2 * @MediumSpace) (2 * @MediumSpace); + top: -15px; + left: 287px; } + } - &:hover .tooltiptext { - .tooltipVisible(); - } + &:hover .tooltiptext { + .tooltipVisible(); + } } .pageOptionTooltipWidth { - min-width: @optionsInfoWidth; + min-width: @optionsInfoWidth; } .noFixedCollectionsTooltipWidth { - min-width: @noFixedCollectionsTooltipWidth; + min-width: @noFixedCollectionsTooltipWidth; } .infoTooltipWidth { - min-width: @tooltipTextWidth; + min-width: @tooltipTextWidth; } .mongoWildcardIndexTooltipWidth { - min-width: @mongoWildcardIndexTooltipWidth; + min-width: @mongoWildcardIndexTooltipWidth; } .sharedCollectionThroughputTooltipWidth { - min-width: @sharedCollectionThroughputTooltipTextWidth; + min-width: @sharedCollectionThroughputTooltipTextWidth; } .addContainerThroughputInput { - min-width: @addContainerPaneThroughputInfoWidth; + min-width: @addContainerPaneThroughputInfoWidth; } .renewInfoTooltipWidth { - width: @RenewAccessInfoWidth; + width: @RenewAccessInfoWidth; } .throughputInfo { - min-width: @ThroughputInfoWidth; + min-width: @ThroughputInfoWidth; } .throughputRuInfo { - min-width: @ThroughputRuInfoWidth; + min-width: @ThroughputRuInfoWidth; } .provisionDatabaseThroughput { - width: @provisionDatabaseThroughputInfo; + width: @provisionDatabaseThroughputInfo; } .pricingtierimg { - padding-left: 20px; - padding-top: 10px; - padding-bottom: 20px; + padding-left: 20px; + padding-top: 10px; + padding-bottom: 20px; } .headerline { - color: @BaseDark; - font-size: 16px; - border-bottom: 1px solid @BaseMedium; - height: 41px; + color: @BaseDark; + font-size: 16px; + border-bottom: 1px solid @BaseMedium; + height: 41px; } .partitionkeystyle { - font-size: 10px; + font-size: 10px; } .arrowprice { - margin-left: 230px; + margin-left: 230px; } .paddingspan { - padding: 20px; - color: white; - font-size: 14px; + padding: 20px; + color: white; + font-size: 14px; } .contextual-pane .paddingspan3 { - padding-left: 0px; - position: absolute; - width: 100%; - height: 100px; - bottom: -40px; - margin: 0 -15px; - border-top: solid 1px #bbbbbb; - margin-left: -32px; + padding-left: 0px; + position: absolute; + width: 100%; + height: 100px; + bottom: -40px; + margin: 0 -15px; + border-top: solid 1px #bbbbbb; + margin-left: -32px; } - /* Variant of paddingspan3 without the margins */ .contextual-pane .paddingspan3b { - padding-left: 0px; - position: absolute; - width: 100%; - height: 100px; - bottom: -40px; - border-top: solid 1px #bbbbbb; + padding-left: 0px; + position: absolute; + width: 100%; + height: 100px; + bottom: -40px; + border-top: solid 1px #bbbbbb; } .contextual-pane hr { - border: 1px solid #53575B; - margin-right: 20px; + border: 1px solid #53575b; + margin-right: 20px; } .contextual-pane .tabs .tab label { - padding: 5px 20px; - margin-bottom: 0px; + padding: 5px 20px; + margin-bottom: 0px; } .contextual-pane .collid { - border: 1px solid #605e5c; - font-size: 10px; - padding: 5px 10px; - color: #000; + border: 1px solid #605e5c; + font-size: 10px; + padding: 5px 10px; + color: #000; } .contextual-pane .select-font-size { - font-size: 12px; + font-size: 12px; } input::-webkit-calendar-picker-indicator { - opacity: 100; + opacity: 100; } .contextual-pane input.collid[type="text"] { - font-size: 12px; - /* color: #000; */ - padding: 4px 10px; + font-size: 12px; + /* color: #000; */ + padding: 4px 10px; } .contextual-pane textarea.collid { - font-size: 12px; + font-size: 12px; } /* Start -- Contextual pane components @@ -1691,650 +1706,650 @@ input::-webkit-calendar-picker-indicator { */ .contextual-pane .paneContentContainer { - display: flex; - flex-direction: column; - height: 100%; + display: flex; + flex-direction: column; + height: 100%; } .contextual-pane .paneErrorDetailsContainer { - display: flex; - flex-direction: column; - height: 100vh; + display: flex; + flex-direction: column; + height: 100vh; } .contextual-pane .paneErrorDetails { - padding: 16px 32px; - color: #000; - overflow-x: hidden; - overflow-y: auto; - flex: 1; + padding: 16px 32px; + color: #000; + overflow-x: hidden; + overflow-y: auto; + flex: 1; } .contextual-pane .paneErrorDetailsHeader { - display: flex; - padding-top: 7px; - padding-bottom: 12px; - height: 46px; - background-color: #000; + display: flex; + padding-top: 7px; + padding-bottom: 12px; + height: 46px; + background-color: #000; } .contextual-pane .backBtn { - cursor: pointer; + cursor: pointer; } .contextual-pane .backBtn img { - width: 18px; - height: 18px; - margin-bottom: @SmallSpace; + width: 18px; + height: 18px; + margin-bottom: @SmallSpace; } .contextual-pane .moreDetails { - padding-left: @DefaultSpace; + padding-left: @DefaultSpace; } .contextual-pane .paneErrorDetailsHeader .errorDetailsTitle { - flex: 1; - padding-left: @DefaultSpace; + flex: 1; + padding-left: @DefaultSpace; } .contextual-pane .paneErrors a { - cursor: pointer; + cursor: pointer; } .contextual-pane .paneMainContent { - flex: 1; - padding-left: 34px; - padding-right: 34px; - color: @BaseDark; - overflow-y: auto; - overflow-x: auto; - margin: (2 * @MediumSpace) 0px; + flex: 1; + padding-left: 34px; + padding-right: 34px; + color: @BaseDark; + overflow-y: auto; + overflow-x: auto; + margin: (2 * @MediumSpace) 0px; } .contextual-pane .panelMainContent { - padding-left: 34px; - padding-right: 34px; - color: @BaseDark; - margin: (2 * @MediumSpace) 0px; + padding-left: 34px; + padding-right: 34px; + color: @BaseDark; + margin: (2 * @MediumSpace) 0px; } .contextual-pane .paneFooter { - width: 100%; - height: 60px; - border-top: solid 1px #bbbbbb; + width: 100%; + height: 60px; + border-top: solid 1px #bbbbbb; } /* End -- Contextual pane components */ .paddingspan3 { - color: white; - font-size: 14px; - position: absolute; - width: 100%; - height: 100px; - bottom: 150px; + color: white; + font-size: 14px; + position: absolute; + width: 100%; + height: 100px; + bottom: 150px; } .paddingspan4 { - padding-top: 20px; - padding-left: 20px; - color: white; - font-size: 14px; + padding-top: 20px; + padding-left: 20px; + color: white; + font-size: 14px; } .closebtnn { - float: right; - padding: 0 10px; - cursor: pointer; + float: right; + padding: 0 10px; + cursor: pointer; } label { - white-space: nowrap; - font: 12px "Segoe UI"; - padding: 5px 25px; + white-space: nowrap; + font: 12px "Segoe UI"; + padding: 5px 25px; } .Introlines { - padding-top: 27px; - padding-left: 40px; + padding-top: 27px; + padding-left: 40px; } .Introline1 { - font-size: 16px; + font-size: 16px; } .Introline2 { - font-size: 14px; - padding-top: 10px; + font-size: 14px; + padding-top: 10px; } .datalist-arrow { - position: relative; + position: relative; } .datalist-arrow:hover:after { - background: #969696; + background: #969696; } .datalist-arrow:focus:after, .datalist-arrow:active:after { - background: #1EBBEE; + background: #1ebbee; } input::-webkit-calendar-picker-indicator::after { - content: '\276F'; - right: 0; - top: -8%; - display: block; - width: 27px; - height: 25px; - line-height: 25px; - color: #fff; - text-align: center; - pointer-events: none; - transform: rotate(90deg); + content: "\276F"; + right: 0; + top: -8%; + display: block; + width: 27px; + height: 25px; + line-height: 25px; + color: #fff; + text-align: center; + pointer-events: none; + transform: rotate(90deg); } .datalist-arrow:after:hover { - content: '\276F'; - position: absolute; - right: 1px; - top: 6%; - transform: rotate(90deg); - display: block; - width: 27px; - height: 25px; - line-height: 25px; - color: #fff; - text-align: center; - pointer-events: none; - background-color: #1EBBEE; + content: "\276F"; + position: absolute; + right: 1px; + top: 6%; + transform: rotate(90deg); + display: block; + width: 27px; + height: 25px; + line-height: 25px; + color: #fff; + text-align: center; + pointer-events: none; + background-color: #1ebbee; } .Introline3 { - padding-top: 10px; - font-size: 14px; - font-weight: 1000; + padding-top: 10px; + font-size: 14px; + font-weight: 1000; } .collectionsTreeWithSplitter { - height: 100%; + height: 100%; } .collectionCollapsed { - color: black; - font-weight: 400; - font-size: 14px; - position: relative; - display: block; - padding: 0px 8px 4px 4px; - cursor: pointer; - float: right; + color: black; + font-weight: 400; + font-size: 14px; + position: relative; + display: block; + padding: 0px 8px 4px 4px; + cursor: pointer; + float: right; } .resourceTreeCollapse { - margin-right: 2px; - padding: 2px 5px 0px 5px; - border: 1px solid #fff; + margin-right: 2px; + padding: 2px 5px 0px 5px; + border: 1px solid #fff; } .resourceTreeCollapse:hover { - background-color: @BaseLow; + background-color: @BaseLow; } .resourceTreeCollapse:active { - background-color: @AccentMediumLow; - outline: none; + background-color: @AccentMediumLow; + outline: none; } .arrowCollapsed { - cursor: pointer; - width: 16px; - height: 16px; - transform: rotate(-90deg) translateX(-50%); - -webkit-transform: rotate(-90deg) translateX(-50%); - -ms-transform: rotate(-90deg) translateX(-50%); - float: right; + cursor: pointer; + width: 16px; + height: 16px; + transform: rotate(-90deg) translateX(-50%); + -webkit-transform: rotate(-90deg) translateX(-50%); + -ms-transform: rotate(-90deg) translateX(-50%); + float: right; } .qslevel { - padding-top: 10px; - border: none; + padding-top: 10px; + border: none; } -.qslevel>li>a { - border: none !important; +.qslevel > li > a { + border: none !important; } -.qslevel>li.active { - border-bottom: 4px solid #767676; +.qslevel > li.active { + border-bottom: 4px solid #767676; } .nav-tabs-margin { - padding-top: 8px; - background-color: #F2F2F2; + padding-top: 8px; + background-color: #f2f2f2; } .navTabHeight { - height: 31px; + height: 31px; } -.qslevel>li.active>a, -.qslevel>li>a:focus, -.nav.nav-tabs.qslevel>li>a:hover { - border: none; - border-radius: 0; - background-color: transparent !important; - border-color: transparent; +.qslevel > li.active > a, +.qslevel > li > a:focus, +.nav.nav-tabs.qslevel > li > a:hover { + border: none; + border-radius: 0; + background-color: transparent !important; + border-color: transparent; } .numbersize { - font-size: 60px; - display: inline; + font-size: 60px; + display: inline; } .numberheading { - display: inline; - position: absolute; - padding-top: 20px; - font-size: 16px; - padding-left: 20px; + display: inline; + position: absolute; + padding-top: 20px; + font-size: 16px; + padding-left: 20px; } -.numberheading>p { - padding-top: 10px; - font-size: 12px !important; +.numberheading > p { + padding-top: 10px; + font-size: 12px !important; } -.numberheading>ul { - padding-top: 10px; +.numberheading > ul { + padding-top: 10px; } -.numberheading>ul>li>a { - font-size: 12px !important; +.numberheading > ul > li > a { + font-size: 12px !important; } .step1 { - padding-bottom: 60px; + padding-bottom: 60px; } -.step1>input { - font-size: 12px; +.step1 > input { + font-size: 12px; } .btncreatecoll { - background: @AccentMediumHigh; - color: #fff; - padding: 0 20px; - cursor: pointer; - font-size: 12px; - border: 1px solid @AccentMediumHigh; + background: @AccentMediumHigh; + color: #fff; + padding: 0 20px; + cursor: pointer; + font-size: 12px; + border: 1px solid @AccentMediumHigh; } .atags:focus { - color: @AccentMediumHigh; + color: @AccentMediumHigh; } .atags { - color: @AccentMediumHigh; - font-weight: 400; - cursor: pointer + color: @AccentMediumHigh; + font-weight: 400; + cursor: pointer; } .qsmenuicons { - width: 25px; - height: 25px; - margin-right: 5px; + width: 25px; + height: 25px; + margin-right: 5px; } .HeaderBg { - background-color: #202428; - height: 60px; + background-color: #202428; + height: 60px; } .title { - color: @AccentMediumHigh; - font-size: 20px; - padding-left: 10px; + color: @AccentMediumHigh; + font-size: 20px; + padding-left: 10px; } .items { - padding-left: 24px; - padding-top: 15px; + padding-left: 24px; + padding-top: 15px; } .divmenuquickstartpadding { - padding-left: 24px; - padding-bottom: 8px; + padding-left: 24px; + padding-bottom: 8px; } .menuQuickStart { - font-size: 12px; - color: white; - padding-left: 10px; + font-size: 12px; + color: white; + padding-left: 10px; } .menuExplorer { - font-size: 12px; - color: white; - padding-left: 20px; + font-size: 12px; + color: white; + padding-left: 20px; } .activemenu { - color: #fff; + color: #fff; } .rightarrowimg { - padding-left: 5px; - padding-bottom: 2px; + padding-left: 5px; + padding-bottom: 2px; } .underlinedLink { - text-decoration: underline !important; - color: @SelectionColor; + text-decoration: underline !important; + color: @SelectionColor; } a:hover, a:visited, a:active, a:link { - text-decoration: none; + text-decoration: none; } -.nav>li>a:focus { - background-color: white; - outline: none; +.nav > li > a:focus { + background-color: white; + outline: none; } .iconpadclick { - background-color: #e6e6e6; - cursor: pointer; - border: 1px solid #1ebbee; - padding: 5px; + background-color: #e6e6e6; + cursor: pointer; + border: 1px solid #1ebbee; + padding: 5px; } .divimgleftarrow { - display: inline-block; - margin-top: 16px; - margin-right: 10px; + display: inline-block; + margin-top: 16px; + margin-right: 10px; } .divimgleftarrow:hover { - background-color: #e6e6e6; - cursor: pointer; - border: 1px solid #1ebbee; + background-color: #e6e6e6; + cursor: pointer; + border: 1px solid #1ebbee; } .adddeliconspan { - display: none; - float: right; - padding: 5px; + display: none; + float: right; + padding: 5px; } .spanparent:hover .adddeliconspan { - display: inline; + display: inline; } .spanchild:hover .adddeliconspan { - display: inline; + display: inline; } .resourceTreeAndTabs { - display: flex; - flex: 1 1 auto; - overflow-x: auto; - overflow-y: hidden; - height: 100%; + display: flex; + flex: 1 1 auto; + overflow-x: auto; + overflow-y: hidden; + height: 100%; } .collectiontitle { - font-size: 14px; - text-transform: uppercase; + font-size: 14px; + text-transform: uppercase; } .coltitle { - background: white; - text-align: justify; - padding: @SmallSpace 0px @DefaultSpace 0px; + background: white; + text-align: justify; + padding: @SmallSpace 0px @DefaultSpace 0px; } .titlepadcol { - padding-left: 20px; - font-weight: 500; - height: 28px; - display: inline-block; - padding-top: 5px; + padding-left: 20px; + font-weight: 500; + height: 28px; + display: inline-block; + padding-top: 5px; } .padimgcolrefresh { - padding: 0px 0px 4px 4px; + padding: 0px 0px 4px 4px; } .padimgcolrefresh:hover { - background: @BaseLow; + background: @BaseLow; } .padimgcolrefresh:active { - background: @AccentMediumLow; - outline: none; + background: @AccentMediumLow; + outline: none; } .refreshcol { - cursor: pointer; - width: 14px; - height: 14px; + cursor: pointer; + width: 14px; + height: 14px; } .refreshcol:focus { - border: Solid 1px @AccentMediumHigh; + border: Solid 1px @AccentMediumHigh; } .refreshcol1 { - cursor: pointer; - width: 16px; - height: 16px; + cursor: pointer; + width: 16px; + height: 16px; } .padimgcolrefresh1 { - padding: 0px 4px 4px 4px; + padding: 0px 4px 4px 4px; } .padimgcolrefresh1:hover { - background-color: @BaseLow; + background-color: @BaseLow; } .padimgcolrefresh1:active { - background-color: @AccentMediumLow; - outline: none; + background-color: @AccentMediumLow; + outline: none; } .btnmainslide { - height: 14px; - margin-top: 14px; - cursor: pointer; + height: 14px; + margin-top: 14px; + cursor: pointer; } .well { - padding: 19px 0px; - padding-top: 0px; - margin-bottom: 20px; - border: 0px; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0); - background: white; + padding: 19px 0px; + padding-top: 0px; + margin-bottom: 20px; + border: 0px; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0); + background: white; } .splitter, .ui-resizable-handle.ui-resizable-e { - z-index: 1; - width: 5px; - border-right: 1px solid @BaseMedium; - border-bottom: none; - padding: 0px; - background-color: @BaseLight; + z-index: 1; + width: 5px; + border-right: 1px solid @BaseMedium; + border-bottom: none; + padding: 0px; + background-color: @BaseLight; } .splitter, .ui-resizable-handle.ui-resizable-s { - z-index: 1; - height: 5px; - border-bottom: 1px solid @BaseMedium; - border-right: none; - padding: 0px; - background-color: transparent; + z-index: 1; + height: 5px; + border-bottom: 1px solid @BaseMedium; + border-right: none; + padding: 0px; + background-color: transparent; } .ui-resizable-helper { - border: 1px dotted; + border: 1px dotted; } .testClass { - padding-left: 30px; + padding-left: 30px; } .level { - padding-left: 16px; - padding-top: 0px; + padding-left: 16px; + padding-top: 0px; } .tabCommandButton { - border-bottom: 1px solid #ddd; - box-sizing: border-box; - padding-left: @DefaultSpace; - .flex-display(); - flex: 0 0 auto; + border-bottom: 1px solid #ddd; + box-sizing: border-box; + padding-left: @DefaultSpace; + .flex-display(); + flex: 0 0 auto; } .imgiconwidth { - margin-right: 5px; + margin-right: 5px; } .id { - padding-left: 8px; - color: #000; - font-weight: bold; - margin-left: 6px; + padding-left: 8px; + color: #000; + font-weight: bold; + margin-left: 6px; } .documentsGridHeaderContainer { - padding-left: 5px; - width: 100%; - border-bottom: 1px solid #CCCCCC; + padding-left: 5px; + width: 100%; + border-bottom: 1px solid #cccccc; } -.documentsGridHeaderContainer>table { - width: 100%; - table-layout: fixed; - border-collapse: unset; +.documentsGridHeaderContainer > table { + width: 100%; + table-layout: fixed; + border-collapse: unset; } .documentsGridHeaderContainer table thead tr { + position: sticky; + top: 0; + th { position: sticky; top: 0; - th { - position: sticky; - top: 0; - background-color: #fff !important; - border-bottom: 1px solid #CCCCCC !important; - } + background-color: #fff !important; + border-bottom: 1px solid #cccccc !important; + } } .documentsGridHeader { - padding-left: 8px; - color: #000; - font-weight: bold; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - cursor: default; - width: 45%; + padding-left: 8px; + color: #000; + font-weight: bold; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: default; + width: 45%; } .refreshColHeader { - padding: 3px 6px 6px 6px; + padding: 3px 6px 6px 6px; } .refreshColHeader:hover { - background-color: @BaseLow; + background-color: @BaseLow; } .refreshColHeader:active { - background-color: @AccentMediumLow; + background-color: @AccentMediumLow; } .refreshColHeader .refreshcol { - float: right; + float: right; } .fixedWidthHeader { - width: 82px; + width: 82px; } .tabdocuments .scrollable { - height: 100%; - overflow-y: auto; - overflow-x: hidden; - padding-left: 5px; - padding-right: 5px; - width:100%; + height: 100%; + overflow-y: auto; + overflow-x: hidden; + padding-left: 5px; + padding-right: 5px; + width: 100%; } -.tabdocuments>.tabdocumentsGridElement { - width: 50%; +.tabdocuments > .tabdocumentsGridElement { + width: 50%; } -.tabdocuments>.evenlySpacedHeader { - width: 30%; +.tabdocuments > .evenlySpacedHeader { + width: 30%; } .tabdocuments.scrollable:focus, .tabdocuments.scrollable:active { - outline: 1px dotted; + outline: 1px dotted; } .tabdocuments .scrollable table td { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .mongoDocumentEditor .monaco-editor.vs .redsquiggly { - display: none !important; + display: none !important; } td a { - color: #393939; + color: #393939; } td a:hover { - color: #393939; + color: #393939; } .loadMore { - width: 100%; - padding-left: 30%; - padding-top: 2px; - cursor: pointer; + width: 100%; + padding-left: 30%; + padding-top: 2px; + cursor: pointer; } -.loadMore>a:focus { - outline: 1px dotted; +.loadMore > a:focus { + outline: 1px dotted; } #content.active .tabdocuments .scrollable { - height: 100%; - overflow-y: auto; + height: 100%; + overflow-y: auto; } .table-fixed thead { - width: 97%; - padding-left: 18px; + width: 97%; + padding-left: 18px; } .table-fixed tbody { - height: 510px; - overflow-y: auto; - width: 100%; - overflow-x: hidden; + height: 510px; + overflow-y: auto; + width: 100%; + overflow-x: hidden; } .table-fixed thead, @@ -2342,560 +2357,560 @@ td a:hover { .table-fixed tr, .table-fixed td, .table-fixed th { - display: block; + display: block; } .table-fixed tbody td, -.table-fixed thead>tr>th { - float: left; - border-bottom-width: 0; +.table-fixed thead > tr > th { + float: left; + border-bottom-width: 0; } a:hover, a:visited, a:active, a:link { - text-decoration: none; + text-decoration: none; } .tabsManagerContainer { - height: 100%; - flex-grow: 1; - overflow: hidden; + height: 100%; + flex-grow: 1; + overflow: hidden; } .tabs { - position: relative; - margin: 15px 0 25px 0; - display: table; - width: 100%; + position: relative; + margin: 15px 0 25px 0; + display: table; + width: 100%; } .tab { - float: left; + float: left; } .tab label { - border: 1px solid #bbbbbb; - margin-left: -1px; - position: inherit; - left: 1px; - color: #393939; + border: 1px solid #bbbbbb; + margin-left: -1px; + position: inherit; + left: 1px; + color: #393939; } -.tab [type=radio] { - display: none; +.tab [type="radio"] { + display: none; } .tabcontent { - clear:both; - left: 0; - right: 0; - bottom: 0; - padding: @MediumSpace 0px; + clear: both; + left: 0; + right: 0; + bottom: 0; + padding: @MediumSpace 0px; } -.tab [type=radio]:checked~label { - border: 1px solid #0072c6; - background-color: @AccentMediumHigh; - color: white; - z-index: 2; +.tab [type="radio"]:checked ~ label { + border: 1px solid #0072c6; + background-color: @AccentMediumHigh; + color: white; + z-index: 2; } -.tab [type=radio]:checked~label:hover { - border: 1px solid @AccentMediumHigh; - background-color: @AccentMediumHigh; - color: white; - z-index: 2; +.tab [type="radio"]:checked ~ label:hover { + border: 1px solid @AccentMediumHigh; + background-color: @AccentMediumHigh; + color: white; + z-index: 2; } -.tab [type=radio]:checked~label:active { - border: 1px solid #0072c6; - background-color: #0072c6; - color: white; - z-index: 2; +.tab [type="radio"]:checked ~ label:active { + border: 1px solid #0072c6; + background-color: #0072c6; + color: white; + z-index: 2; } -.tab [type=radio]:checked~label~.tabcontent { - z-index: 1; - display: initial; +.tab [type="radio"]:checked ~ label ~ .tabcontent { + z-index: 1; + display: initial; } -.tab [type=radio]:not(:checked)~label:hover { - border: 1px solid #969696; - background-color: #969696; - color: white; - cursor: pointer; +.tab [type="radio"]:not(:checked) ~ label:hover { + border: 1px solid #969696; + background-color: #969696; + color: white; + cursor: pointer; } -.tab [type=radio]:not(:checked)~label~.tabcontent { - display: none; +.tab [type="radio"]:not(:checked) ~ label ~ .tabcontent { + display: none; } ::-ms-expand { - color: #969696; + color: #969696; } .atagdetails { - padding-left: 55px!important; + padding-left: 55px !important; } -.contextual-pane-in .form-errors+img { - display: block; - position: absolute; - top: 92px; - left: 12px; +.contextual-pane-in .form-errors + img { + display: block; + position: absolute; + top: 92px; + left: 12px; } .path { - color: lightgray; - font-style: italic; - padding-top: 12px; - padding-left: 20px; + color: lightgray; + font-style: italic; + padding-top: 12px; + padding-left: 20px; } .queryPath { - line-height: 16px; - padding-left: 33px; - padding-bottom: 12px; + line-height: 16px; + padding-left: 33px; + padding-bottom: 12px; } .filterDocCollapsed { - .flex-display(); - padding: 0px 36px 0px 20px; + .flex-display(); + padding: 0px 36px 0px 20px; } .filterDocCollapsed.active { - display: block; + display: block; } .selectQuery { - padding: @SmallSpace @SmallSpace 0px 0px; + padding: @SmallSpace @SmallSpace 0px 0px; } .noFilterApplied { - padding-top: @SmallSpace; + padding-top: @SmallSpace; } .appliedQuery { - overflow: hidden; - text-overflow: ellipsis; - padding-top: @SmallSpace; + overflow: hidden; + text-overflow: ellipsis; + padding-top: @SmallSpace; } .filterDocExpanded { - padding-left: 20px; + padding-left: 20px; } .filterDocExpanded.active { - display: block; + display: block; } .filterbtnstyle { - background: @AccentMediumHigh; - width: 90px; - height: 25px; - color: white; - border: solid 1px; + background: @AccentMediumHigh; + width: 90px; + height: 25px; + color: white; + border: solid 1px; } .filterbtnstyle:hover { - background: @AccentMediumHigh; - width: 90px; - height: 25px; - color: white; - border: none; + background: @AccentMediumHigh; + width: 90px; + height: 25px; + color: white; + border: none; } .filterbtnstyle:active { - background: #0072c6; - width: 90px; - height: 25px; - color: white; - border: none; + background: #0072c6; + width: 90px; + height: 25px; + color: white; + border: none; } .filterbtnstyle:focus { - background: #0072c6; - width: 90px; - height: 25px; - color: white; - border: none; - border: 1px solid #0072c6; + background: #0072c6; + width: 90px; + height: 25px; + color: white; + border: none; + border: 1px solid #0072c6; } .filterbtnstyle:not(:enabled) { - background: lightgray; - width: 90px; - height: 25px; - color: white; - border: none; + background: lightgray; + width: 90px; + height: 25px; + color: white; + border: none; } -.queryButton{ - margin-left:@LargeSpace; +.queryButton { + margin-left: @LargeSpace; } .hrline1 { - color: #d6d7d8; - margin-left: -20px; + color: #d6d7d8; + margin-left: -20px; } .filtdocheader { - font-size: 18px; + font-size: 18px; } .editFilter { - margin-left: 20px; + margin-left: 20px; } .filterdivs { - padding-top: 15px; - height: 45px; - margin-bottom: 8px; - white-space: nowrap; + padding-top: 15px; + height: 45px; + margin-bottom: 8px; + white-space: nowrap; } .editFilterContainer { - display: flex; + display: flex; } .filterspan { - margin-top: @SmallSpace; - padding: 0px @LargeSpace 0px 0px; + margin-top: @SmallSpace; + padding: 0px @LargeSpace 0px 0px; } .filterclose { - padding: @SmallSpace 10px; - cursor: pointer; + padding: @SmallSpace 10px; + cursor: pointer; } .querydropdown { - border: 1px solid @BaseMedium; - font-style: normal; - width: 100%; + border: 1px solid @BaseMedium; + font-style: normal; + width: 100%; } .querydropdown.placeholderVisible { - font-style: italic; + font-style: italic; } .querydropdown:hover { - background-color: @AccentLow; + background-color: @AccentLow; } .querydropdown::-webkit-input-placeholder { - color: lightgray; - padding-left: 3px; + color: lightgray; + padding-left: 3px; } .querydropdown:-moz-placeholder { - /* Firefox 18- */ - color: lightgray; + /* Firefox 18- */ + color: lightgray; } .querydropdown::-moz-placeholder { - /* Firefox 19+ */ - color: lightgray; + /* Firefox 19+ */ + color: lightgray; } .querydropdown:-ms-input-placeholder { - color: lightgray; - padding-left: 7px; + color: lightgray; + padding-left: 7px; } .rowoverride { - margin-left: 7px; - margin-top: 20px; + margin-left: 7px; + margin-top: 20px; } .tabPanesContainer { - display: flex; - height: 100%; - overflow: hidden; + display: flex; + height: 100%; + overflow: hidden; } .tabs-container { - height: 100%; - width: 100%; + height: 100%; + width: 100%; } .paddingspan4 { - padding-top: 20px; - color: white; - padding-left: 25px; - padding-right: 25px; + padding-top: 20px; + color: white; + padding-left: 25px; + padding-right: 25px; } .colResizePointer { - cursor: col-resize; + cursor: col-resize; } -.nav-tabs>li.active>.tabNavContentContainer, -.nav-tabs>li.active>.tabNavContentContainer:focus, -.nav-tabs>li.active>.tabNavContentContainer:hover { - color: #555; - cursor: default; - background-color: @BaseLight; - border-color: @BaseMedium; - border-bottom-color: @BaseLight; - border-style: solid; - border-width: 1px; - height: @ActiveTabHeight; - width: @ActiveTabWidth; +.nav-tabs > li.active > .tabNavContentContainer, +.nav-tabs > li.active > .tabNavContentContainer:focus, +.nav-tabs > li.active > .tabNavContentContainer:hover { + color: #555; + cursor: default; + background-color: @BaseLight; + border-color: @BaseMedium; + border-bottom-color: @BaseLight; + border-style: solid; + border-width: 1px; + height: @ActiveTabHeight; + width: @ActiveTabWidth; } -.nav-tabs>li.active:focus>.tabNavContentContainer { - .focus(); +.nav-tabs > li.active:focus > .tabNavContentContainer { + .focus(); } .tabNavContentContainer { + .flex-display(); + height: @TabsHeight; + justify-content: space-between; + border-radius: 2px 2px 0 0; + padding: @DefaultSpace 0px @SmallSpace 0px; + color: @BaseHigh; + width: @TabsWidth; + text-align: center; + position: relative; + border: @ButtonBorderWidth solid transparent; + + &:hover { + text-decoration: none; + background-color: @BaseMediumLow; + border-color: @BaseMediumLow; + } + + &:active { + background-color: @BaseMediumLow; + } + + .tab_Content { .flex-display(); - height: @TabsHeight; - justify-content: space-between; - border-radius: 2px 2px 0 0; - padding: @DefaultSpace 0px @SmallSpace 0px; - color: @BaseHigh; width: @TabsWidth; - text-align: center; - position: relative; - border: @ButtonBorderWidth solid transparent; + border-right: @ButtonBorderWidth solid @BaseMedium; + white-space: nowrap; - &:hover { - text-decoration: none; - background-color: @BaseMediumLow; - border-color: @BaseMediumLow; - } + .statusIconContainer { + width: @StatusIconContainerSize; + height: @StatusIconContainerSize; + margin-left: @SmallSpace; + display: inline-flex; - &:active { - background-color: @BaseMediumLow; - } + .errorIconContainer { + width: @ErrorIconContainer; + height: @ErrorIconContainer; + margin-top: 1px; - .tab_Content { - .flex-display(); - width: @TabsWidth; - border-right: @ButtonBorderWidth solid @BaseMedium; - white-space: nowrap; + .errorIcon { + width: @ErrorIconWidth; + height: @LoadingErrorIconSize; + background-image: url(../images/error_no_outline.svg); + background-repeat: no-repeat; + background-position: center; + background-size: 3px; + display: block; + margin: 1px 0px 0px 6px; + } + } - .statusIconContainer { - width: @StatusIconContainerSize; - height: @StatusIconContainerSize; - margin-left: @SmallSpace; - display: inline-flex; - - .errorIconContainer { - width: @ErrorIconContainer; - height: @ErrorIconContainer; - margin-top: 1px; - - .errorIcon { - width: @ErrorIconWidth; - height: @LoadingErrorIconSize; - background-image: url(../images/error_no_outline.svg); - background-repeat: no-repeat; - background-position: center; - background-size: 3px; - display: block; - margin: 1px 0px 0px 6px; - } - } - - .errorIconContainer.actionsEnabled { - &:hover { - .hover(); - } - - &:focus { - .focus(); - } - - &:active { - .active(); - } - } - - .errorIconContainer[tabindex]:active { - outline: none; - } - - .loadingIcon { - width: @LoadingErrorIconSize; - height: @LoadingErrorIconSize; - margin: 0px 0px @SmallSpace @SmallSpace; - } + .errorIconContainer.actionsEnabled { + &:hover { + .hover(); } - .tabNavText { - margin-left: @SmallSpace; - margin-right: 2px; - color: @BaseDark; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - flex-grow: 1 + &:focus { + .focus(); } - .tabIconSection { - width: 30px; - position: relative; - padding-top: 2px; - - .cancelButton { - padding: 0px @SmallSpace 0px @SmallSpace; - - &:hover { - .hover(); - } - - &:focus { - .focus(); - } - - &:active { - .active(); - } - } + &:active { + .active(); } + } + + .errorIconContainer[tabindex]:active { + outline: none; + } + + .loadingIcon { + width: @LoadingErrorIconSize; + height: @LoadingErrorIconSize; + margin: 0px 0px @SmallSpace @SmallSpace; + } } + + .tabNavText { + margin-left: @SmallSpace; + margin-right: 2px; + color: @BaseDark; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + flex-grow: 1; + } + + .tabIconSection { + width: 30px; + position: relative; + padding-top: 2px; + + .cancelButton { + padding: 0px @SmallSpace 0px @SmallSpace; + + &:hover { + .hover(); + } + + &:focus { + .focus(); + } + + &:active { + .active(); + } + } + } + } } .cancelButton[tabindex]:active { - outline: none; + outline: none; } .clickableLink { - color: @AccentMediumHigh; - font-family: @DataExplorerFont; - font-size: 12px; - cursor: pointer; + color: @AccentMediumHigh; + font-family: @DataExplorerFont; + font-size: 12px; + cursor: pointer; } .clickableLink:hover { - background-color: #e7f6fc; + background-color: #e7f6fc; } .clickableLink:active { - background-color: #e6f8fe; + background-color: #e6f8fe; } .clickableLink:focus { - outline: 1px dashed #000000; - outline-offset: 0px; + outline: 1px dashed #000000; + outline-offset: 0px; } .paneselect { - height: 23px; + height: 23px; } @media @screen { - .commandBar-ie { - padding-top: 7px; - } - .filterspan { - margin: 0px; - padding-top: 2px; - } - .tabdocuments .scrollable { - height: 100%; - } + .commandBar-ie { + padding-top: 7px; + } + .filterspan { + margin: 0px; + padding-top: 2px; + } + .tabdocuments .scrollable { + height: 100%; + } - .nav-tabs>li>a:active { - background-color: #e0e0e0; - width: 100%; - border: 1px solid @AccentMediumHigh; - cursor: pointer; - } + .nav-tabs > li > a:active { + background-color: #e0e0e0; + width: 100%; + border: 1px solid @AccentMediumHigh; + cursor: pointer; + } } .headerWithoutPartitionKey { - width: 172px; + width: 172px; } .headerWithPartitionKey { - width: 86px; + width: 86px; } .nodeIconSet { - color: black; - margin-left: 7px; - padding-left: 5px; + color: black; + margin-left: 7px; + padding-left: 5px; } .tabCommandDisabled { - color: #CCCCCC; - cursor: default; - background-color: #FFFFFF; + color: #cccccc; + cursor: default; + background-color: #ffffff; } .tabCommandDisabled:active { - border: 1px solid #FFFFFF; + border: 1px solid #ffffff; } .tabCommandDisabled:hover { - background-color: #FFFFFF; + background-color: #ffffff; } #explorerNotificationConsole { - z-index: 1000; + z-index: 1000; } .uniqueIndexesContainer { - width: 100%; + width: 100%; - .uniqueKeys { - padding-bottom: @SmallSpace; + .uniqueKeys { + padding-bottom: @SmallSpace; - .uniqueInfoTooltip { - .infoTooltip(); + .uniqueInfoTooltip { + .infoTooltip(); - &:hover .uniqueTooltiptext { - .tooltipVisible(); - } + &:hover .uniqueTooltiptext { + .tooltipVisible(); + } - &:focus .uniqueTooltiptext { - .tooltipVisible(); - } + &:focus .uniqueTooltiptext { + .tooltipVisible(); + } - .uniqueTooltiptext { - bottom:28px; - .tooltipText(); - } + .uniqueTooltiptext { + bottom: 28px; + .tooltipText(); + } - .uniqueTooltiptext::after { - border-width: (2 * @MediumSpace) (2 * @MediumSpace) 0px 0px; - bottom: -15px; - .tooltipTextAfter(); - } - } + .uniqueTooltiptext::after { + border-width: (2 * @MediumSpace) (2 * @MediumSpace) 0px 0px; + bottom: -15px; + .tooltipTextAfter(); + } } + } } settings-pane { - .settingsSection { - border-bottom: 1px solid @BaseMedium; - margin-right: 24px; - padding: @MediumSpace 0px; + .settingsSection { + border-bottom: 1px solid @BaseMedium; + margin-right: 24px; + padding: @MediumSpace 0px; - &:first-child { - padding-top: 0px; - } - - &:last-child { - border-bottom: none; - } - - .settingsSectionPart { - padding-left: 8px; - } - - .settingsSectionLabel { - margin-bottom: @DefaultSpace; - } - - .pageOptionsPart { - padding-bottom: @MediumSpace; - } + &:first-child { + padding-top: 0px; } + + &:last-child { + border-bottom: none; + } + + .settingsSectionPart { + padding-left: 8px; + } + + .settingsSectionLabel { + margin-bottom: @DefaultSpace; + } + + .pageOptionsPart { + padding-bottom: @MediumSpace; + } + } } // TODO: Remove these styles once we refactor all buttons to use the command button component @@ -2903,127 +2918,169 @@ settings-pane { @ButtonBorderWidth: 1px; .commandButton { - padding: 6px @DefaultSpace @DefaultSpace; - border: @ButtonBorderWidth solid transparent; - color: @BaseHigh; - background-color: transparent; + padding: 6px @DefaultSpace @DefaultSpace; + border: @ButtonBorderWidth solid transparent; + color: @BaseHigh; + background-color: transparent; - &:hover:not(.commandDisabled) { - background-color: @AccentLight; - cursor: pointer; - } + &:hover:not(.commandDisabled) { + background-color: @AccentLight; + cursor: pointer; + } - &:active:not(.commandDisabled) { - background-color: @AccentExtra; - border: @ButtonBorderWidth dashed @AccentMedium; - } + &:active:not(.commandDisabled) { + background-color: @AccentExtra; + border: @ButtonBorderWidth dashed @AccentMedium; + } - &:focus:not(.commandDisabled) { - border: @ButtonBorderWidth dashed @AccentMedium; - } + &:focus:not(.commandDisabled) { + border: @ButtonBorderWidth dashed @AccentMedium; + } - .commandIcon { - margin: 0 @SmallSpace 0 0; - vertical-align: text-top; - width: @ButtonIconSize; - height: @ButtonIconSize; - } + .commandIcon { + margin: 0 @SmallSpace 0 0; + vertical-align: text-top; + width: @ButtonIconSize; + height: @ButtonIconSize; + } } .commandButton.commandDisabled { - color: @BaseMediumHigh; - opacity: 0.5; + color: @BaseMediumHigh; + opacity: 0.5; } .commandButton[tabindex]:focus { - outline: none; + outline: none; } .linkDarkBackground { - color: @AccentExtraHigh + color: @AccentExtraHigh; } .linkDarkBackground:hover, .linkDarkBackground:active, .linkDarkBackground:focus { - color: @AccentHigh + color: @AccentHigh; } .library-add-button { - margin-top: @LargeSpace; + margin-top: @LargeSpace; } .library-grid-container { - margin-top: 24px; + margin-top: 24px; } .library-delete { - cursor: pointer; - margin-left: 1em; + cursor: pointer; + margin-left: 1em; } -#deletecollectionconfirmationpane .paneMainContent > div:not(:first-child){ - margin-top: 12px; +#deletecollectionconfirmationpane .paneMainContent > div:not(:first-child) { + margin-top: 12px; } -#deletedatabaseconfirmationpane .paneMainContent > div:not(:first-child){ - margin-top: 12px; +#deletedatabaseconfirmationpane .paneMainContent > div:not(:first-child) { + margin-top: 12px; } .enableAnalyticalStorage { - margin-bottom: @SmallSpace; + margin-bottom: @SmallSpace; - .enableAnalyticalStorageRadio { - vertical-align: text-bottom; - margin-top: @SmallSpace; - } + .enableAnalyticalStorageRadio { + vertical-align: text-bottom; + margin-top: @SmallSpace; + } - .enableAnalyticalStorageRadio:nth-child(n+2) { - margin-left: @LargeSpace; - } + .enableAnalyticalStorageRadio:nth-child(n + 2) { + margin-left: @LargeSpace; + } - .enableAnalyticalStorageRadioLabel { - padding: 0px - } + .enableAnalyticalStorageRadioLabel { + padding: 0px; + } } .addCollectionLabel { - color: #393939; - font-weight: 600; + color: #393939; + font-weight: 600; } -.button.enabled{ - background: #FFF; - border-radius: 2px; - color: #323130; - padding: 3px 20px; - border: 1px solid #8A8886; +.button.enabled { + background: #fff; + border-radius: 2px; + color: #323130; + padding: 3px 20px; + border: 1px solid #8a8886; } -.button.disabled{ - background: #F3F2F1; - border: 0px solid #8A8886; - border-radius: 2px; - color: #A19F9D; - padding: 3px 20px; +.button.disabled { + background: #f3f2f1; + border: 0px solid #8a8886; + border-radius: 2px; + color: #a19f9d; + padding: 3px 20px; } .paragraph { - margin-top: 8px; + margin-top: 8px; } .italic { - font-style: italic; + font-style: italic; } .warningErrorContent a { - color: @AccentMediumHigh + color: @AccentMediumHigh; } .infoBoxContent a { - color: @AccentMediumHigh + color: @AccentMediumHigh; } -.collapsibleSection :hover{ - cursor: pointer; -} \ No newline at end of file +.collapsibleSection :hover { + cursor: pointer; +} + +.messageBarInfoIcon { + color: #0072c6; +} + +.messageBarWarningIcon { + color: #db7500; +} + +.freeTierInfoBanner { + background-color: @BaseLow; + display: inline-flex; + padding: @DefaultSpace; + width: 100%; + + .freeTierInfoIcon img { + height: 28px; + width: 28px; + margin-left: 4px; + } + + .freeTierInfoMessage { + margin: auto 0; + padding-left: @MediumSpace; + } +} + +.freeTierInlineWarning { + display: inline-flex; + padding: 8px 8px 8px 0; + width: 100%; + + .freeTierWarningIcon img { + height: 20px; + width: 20px; + } + + .freeTierWarningMessage { + margin: auto 0; + padding-left: @SmallSpace; + } +} diff --git a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx index a3f186af7..1891f5875 100644 --- a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx @@ -66,7 +66,7 @@ export interface PriceBreakdown { currencySign: string; } -export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } }; +export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14 } }; export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = { label: { @@ -166,7 +166,10 @@ export const separatorStyles: Partial = { ] }; -export const messageBarStyles: Partial = { root: { marginTop: "5px" } }; +export const messageBarStyles: Partial = { + root: { marginTop: "5px", backgroundColor: "white" }, + text: { fontSize: 14 } +}; export const throughputUnit = "RU/s"; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/__snapshots__/IndexingPolicyRefreshComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/__snapshots__/IndexingPolicyRefreshComponent.test.tsx.snap index 1c44dd435..f067bed43 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/__snapshots__/IndexingPolicyRefreshComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/__snapshots__/IndexingPolicyRefreshComponent.test.tsx.snap @@ -8,7 +8,7 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx index 10aaa0cbf..373e03c4c 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx @@ -16,7 +16,7 @@ import { } from "../SettingsRenderUtils"; import { hasDatabaseSharedThroughput } from "../SettingsUtils"; import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils"; -import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react"; +import { Link, Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react"; import { configContext, Platform } from "../../../../ConfigContext"; export interface ScaleComponentProps { @@ -176,6 +176,7 @@ export class ScaleComponent extends React.Component { label={this.getThroughputTitle()} isEmulator={this.isEmulator} isFixed={this.props.isFixedContainer} + isFreeTierAccount={this.isFreeTierAccount()} isAutoPilotSelected={this.props.isAutoPilotSelected} onAutoPilotSelected={this.props.onAutoPilotSelected} wasAutopilotOriginallySet={this.props.wasAutopilotOriginallySet} @@ -190,9 +191,37 @@ export class ScaleComponent extends React.Component { /> ); + private isFreeTierAccount(): boolean { + const databaseAccount = this.props.container?.databaseAccount(); + return databaseAccount?.properties?.enableFreeTier; + } + + private getFreeTierInfoMessage(): JSX.Element { + return ( + + With free tier, you will get the first 400 RU/s and 5 GB of storage in this account for free. To keep your + account free, keep the total RU/s across all resources in the account to 400 RU/s. + + Learn more. + + + ); + } + public render(): JSX.Element { return ( + {this.isFreeTierAccount() && ( + + {this.getFreeTierInfoMessage()} + + )} {this.getInitialNotificationElement() && ( {this.getInitialNotificationElement()} )} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx index 4322bd6e8..04944804a 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx @@ -13,16 +13,7 @@ import { } from "../SettingsUtils"; import Explorer from "../../../Explorer"; import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon"; -import { - Label, - Text, - TextField, - Stack, - IChoiceGroupOption, - ChoiceGroup, - MessageBar, - MessageBarType -} from "office-ui-fabric-react"; +import { Label, Text, TextField, Stack, IChoiceGroupOption, ChoiceGroup, MessageBar } from "office-ui-fabric-react"; import { getTextFieldStyles, changeFeedPolicyToolTip, @@ -190,7 +181,10 @@ export class SubSettingsComponent extends React.Component {isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && ( - + {ttlWarning} )} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx index 605ab7a02..fc030193f 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx @@ -26,6 +26,7 @@ describe("ThroughputInputAutoPilotV3Component", () => { spendAckVisible: false, showAsMandatory: true, isFixed: false, + isFreeTierAccount: false, label: "label", infoBubbleText: "infoBubbleText", canExceedMaximumValue: true, diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx index 7cb3f81ce..b524750a7 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx @@ -28,7 +28,6 @@ import { Label, Link, MessageBar, - MessageBarType, FontIcon, IColumn } from "office-ui-fabric-react"; @@ -58,6 +57,7 @@ export interface ThroughputInputAutoPilotV3Props { spendAckVisible?: boolean; showAsMandatory?: boolean; isFixed: boolean; + isFreeTierAccount: boolean; isEmulator: boolean; label: string; infoBubbleText?: string; @@ -76,6 +76,7 @@ export interface ThroughputInputAutoPilotV3Props { interface ThroughputInputAutoPilotV3State { spendAckChecked: boolean; + exceedFreeTierThroughput: boolean; } export class ThroughputInputAutoPilotV3Component extends React.Component< @@ -149,7 +150,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< public constructor(props: ThroughputInputAutoPilotV3Props) { super(props); this.state = { - spendAckChecked: this.props.spendAckChecked + spendAckChecked: this.props.spendAckChecked, + exceedFreeTierThroughput: + this.props.isFreeTierAccount && !this.props.isAutoPilotSelected && this.props.throughput > 400 }; this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep; @@ -436,6 +439,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< if (this.overrideWithAutoPilotSettings()) { this.props.onMaxAutoPilotThroughputChange(newThroughput); } else { + this.setState({ exceedFreeTierThroughput: this.props.isFreeTierAccount && newThroughput > 400 }); this.props.onThroughputChange(newThroughput); } }; @@ -479,7 +483,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< /> {this.overrideWithProvisionedThroughputSettings() && ( - + {manualToAutoscaleDisclaimerElement} )} @@ -556,8 +563,21 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< } onChange={this.onThroughputChange} /> + {this.state.exceedFreeTierThroughput && ( + + { + "Billing will apply if you provision more than 400 RU/s of manual throughput, or if the resource scales beyond 400 RU/s with autoscale." + } + + )} {this.props.getThroughputWarningMessage() && ( - + {this.props.getThroughputWarningMessage()} )} @@ -583,7 +603,15 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< warningMessage = saveThroughputWarningMessage; } - return <>{warningMessage && {warningMessage}}; + return ( + <> + {warningMessage && ( + + {warningMessage} + + )} + + ); }; public render(): JSX.Element { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap index 0cd8eb2c2..74161bd49 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap @@ -9,13 +9,18 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = ` } > @@ -59,7 +73,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -171,7 +185,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -444,7 +458,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ScaleComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ScaleComponent.test.tsx.snap index d498de8e7..abba424b9 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ScaleComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ScaleComponent.test.tsx.snap @@ -16,7 +16,7 @@ exports[`ScaleComponent renders with correct initial notification 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/SubSettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/SubSettingsComponent.test.tsx.snap index b0144bf7a..ff376c3c5 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/SubSettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/SubSettingsComponent.test.tsx.snap @@ -136,7 +136,7 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -412,7 +412,7 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -952,7 +952,7 @@ exports[`SubSettingsComponent renders 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -1228,7 +1228,7 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index aef4316e1..d8cf13e79 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -55,6 +55,7 @@ exports[`SettingsComponent renders 1`] = ` "firstFieldHasFocus": [Function], "formErrors": [Function], "formErrorsDetails": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "adddatabasepane", "isAutoPilotSelected": [Function], "isExecuting": [Function], @@ -104,6 +105,7 @@ exports[`SettingsComponent renders 1`] = ` "formErrors": [Function], "formErrorsDetails": [Function], "formWarnings": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "addcollectionpane", "isAnalyticalStorageOn": [Function], "isAutoPilotSelected": [Function], @@ -591,6 +593,7 @@ exports[`SettingsComponent renders 1`] = ` "formErrors": [Function], "formErrorsDetails": [Function], "formWarnings": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "addcollectionpane", "isAnalyticalStorageOn": [Function], "isAutoPilotSelected": [Function], @@ -665,6 +668,7 @@ exports[`SettingsComponent renders 1`] = ` "firstFieldHasFocus": [Function], "formErrors": [Function], "formErrorsDetails": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "adddatabasepane", "isAutoPilotSelected": [Function], "isExecuting": [Function], @@ -1326,6 +1330,7 @@ exports[`SettingsComponent renders 1`] = ` "firstFieldHasFocus": [Function], "formErrors": [Function], "formErrorsDetails": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "adddatabasepane", "isAutoPilotSelected": [Function], "isExecuting": [Function], @@ -1375,6 +1380,7 @@ exports[`SettingsComponent renders 1`] = ` "formErrors": [Function], "formErrorsDetails": [Function], "formWarnings": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "addcollectionpane", "isAnalyticalStorageOn": [Function], "isAutoPilotSelected": [Function], @@ -1862,6 +1868,7 @@ exports[`SettingsComponent renders 1`] = ` "formErrors": [Function], "formErrorsDetails": [Function], "formWarnings": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "addcollectionpane", "isAnalyticalStorageOn": [Function], "isAutoPilotSelected": [Function], @@ -1936,6 +1943,7 @@ exports[`SettingsComponent renders 1`] = ` "firstFieldHasFocus": [Function], "formErrors": [Function], "formErrorsDetails": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "adddatabasepane", "isAutoPilotSelected": [Function], "isExecuting": [Function], @@ -2610,6 +2618,7 @@ exports[`SettingsComponent renders 1`] = ` "firstFieldHasFocus": [Function], "formErrors": [Function], "formErrorsDetails": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "adddatabasepane", "isAutoPilotSelected": [Function], "isExecuting": [Function], @@ -2659,6 +2668,7 @@ exports[`SettingsComponent renders 1`] = ` "formErrors": [Function], "formErrorsDetails": [Function], "formWarnings": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "addcollectionpane", "isAnalyticalStorageOn": [Function], "isAutoPilotSelected": [Function], @@ -3146,6 +3156,7 @@ exports[`SettingsComponent renders 1`] = ` "formErrors": [Function], "formErrorsDetails": [Function], "formWarnings": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "addcollectionpane", "isAnalyticalStorageOn": [Function], "isAutoPilotSelected": [Function], @@ -3220,6 +3231,7 @@ exports[`SettingsComponent renders 1`] = ` "firstFieldHasFocus": [Function], "formErrors": [Function], "formErrorsDetails": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "adddatabasepane", "isAutoPilotSelected": [Function], "isExecuting": [Function], @@ -3881,6 +3893,7 @@ exports[`SettingsComponent renders 1`] = ` "firstFieldHasFocus": [Function], "formErrors": [Function], "formErrorsDetails": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "adddatabasepane", "isAutoPilotSelected": [Function], "isExecuting": [Function], @@ -3930,6 +3943,7 @@ exports[`SettingsComponent renders 1`] = ` "formErrors": [Function], "formErrorsDetails": [Function], "formWarnings": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "addcollectionpane", "isAnalyticalStorageOn": [Function], "isAutoPilotSelected": [Function], @@ -4417,6 +4431,7 @@ exports[`SettingsComponent renders 1`] = ` "formErrors": [Function], "formErrorsDetails": [Function], "formWarnings": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "addcollectionpane", "isAnalyticalStorageOn": [Function], "isAutoPilotSelected": [Function], @@ -4491,6 +4506,7 @@ exports[`SettingsComponent renders 1`] = ` "firstFieldHasFocus": [Function], "formErrors": [Function], "formErrorsDetails": [Function], + "freeTierExceedThroughputTooltip": [Function], "id": "adddatabasepane", "isAutoPilotSelected": [Function], "isExecuting": [Function], diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap index 0efa51f9f..25cb13989 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap @@ -159,7 +159,7 @@ exports[`SettingsUtils functions render 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -176,7 +176,7 @@ exports[`SettingsUtils functions render 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -195,7 +195,7 @@ exports[`SettingsUtils functions render 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -207,7 +207,7 @@ exports[`SettingsUtils functions render 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -219,7 +219,7 @@ exports[`SettingsUtils functions render 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -230,7 +230,7 @@ exports[`SettingsUtils functions render 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -249,7 +249,7 @@ exports[`SettingsUtils functions render 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -268,7 +268,7 @@ exports[`SettingsUtils functions render 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -286,7 +286,7 @@ exports[`SettingsUtils functions render 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -299,7 +299,7 @@ exports[`SettingsUtils functions render 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -310,7 +310,7 @@ exports[`SettingsUtils functions render 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -329,7 +329,7 @@ exports[`SettingsUtils functions render 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -371,7 +371,7 @@ exports[`SettingsUtils functions render 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -386,7 +386,7 @@ exports[`SettingsUtils functions render 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } @@ -402,7 +402,7 @@ exports[`SettingsUtils functions render 1`] = ` styles={ Object { "root": Object { - "fontSize": 12, + "fontSize": 14, }, } } diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3.ts b/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3.ts index bf0bc9970..1286e73cf 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3.ts +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3.ts @@ -129,6 +129,8 @@ export interface ThroughputInputParams { showAutoPilot?: ko.Observable; overrideWithAutoPilotSettings: ko.Observable; overrideWithProvisionedThroughputSettings: ko.Observable; + freeTierExceedThroughputTooltip?: ko.Observable; + freeTierExceedThroughputWarning?: ko.Observable; } export class ThroughputInputViewModel extends WaitsForTemplateViewModel { @@ -165,6 +167,10 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel { public overrideWithProvisionedThroughputSettings: ko.Observable; public isManualThroughputInputFieldRequired: ko.Computed; public isAutoscaleThroughputInputFieldRequired: ko.Computed; + public freeTierExceedThroughputTooltip: ko.Observable; + public freeTierExceedThroughputWarning: ko.Observable; + public showFreeTierExceedThroughputTooltip: ko.Computed; + public showFreeTierExceedThroughputWarning: ko.Computed; public constructor(options: ThroughputInputParams) { super(); @@ -219,6 +225,16 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel { this.isAutoscaleThroughputInputFieldRequired = ko.pureComputed( () => this.isEnabled() && this.isAutoPilotSelected() ); + + this.freeTierExceedThroughputTooltip = options.freeTierExceedThroughputTooltip || ko.observable(); + this.freeTierExceedThroughputWarning = options.freeTierExceedThroughputWarning || ko.observable(); + this.showFreeTierExceedThroughputTooltip = ko.pureComputed( + () => !!this.freeTierExceedThroughputTooltip() && this.value() > 400 + ); + + this.showFreeTierExceedThroughputWarning = ko.pureComputed( + () => !!this.freeTierExceedThroughputWarning() && this.value() > 400 + ); } public decreaseThroughput() { diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoscaleV3.html b/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoscaleV3.html index 1057cac75..8ec328aba 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoscaleV3.html +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoscaleV3.html @@ -132,6 +132,14 @@ capacity calculator

+ +
+ +
+
+ Warning + +
+

diff --git a/src/Explorer/Explorer.ts b/src/Explorer/Explorer.ts index 58490c8d1..612f24cc0 100644 --- a/src/Explorer/Explorer.ts +++ b/src/Explorer/Explorer.ts @@ -3014,4 +3014,25 @@ export default class Explorer { }) ); } + + public isFirstResourceCreated(): boolean { + const databases: ViewModels.Database[] = this.databases(); + + if (!databases || databases.length === 0) { + return false; + } + + return databases.some(database => { + // user has created at least one collection + if (database.collections()?.length > 0) { + return true; + } + // user has created a database with shared throughput + if (database.offer()) { + return true; + } + // use has created an empty database without shared throughput + return false; + }); + } } diff --git a/src/Explorer/Panes/AddCollectionPane.html b/src/Explorer/Panes/AddCollectionPane.html index af007c63b..89096032c 100644 --- a/src/Explorer/Panes/AddCollectionPane.html +++ b/src/Explorer/Panes/AddCollectionPane.html @@ -152,7 +152,8 @@ maxAutoPilotThroughputSet: sharedAutoPilotThroughput, autoPilotUsageCost: autoPilotUsageCost, canExceedMaximumValue: canExceedMaximumValue, - showAutoPilot: !isFreeTierAccount() + showAutoPilot: !isFreeTierAccount(), + freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip }">
@@ -333,7 +334,8 @@ maxAutoPilotThroughputSet: autoPilotThroughput, autoPilotUsageCost: autoPilotUsageCost, canExceedMaximumValue: canExceedMaximumValue, - showAutoPilot: !isFixedStorageSelected() + showAutoPilot: !isFixedStorageSelected(), + freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip }">
diff --git a/src/Explorer/Panes/AddCollectionPane.test.ts b/src/Explorer/Panes/AddCollectionPane.test.ts index ab57ef4f3..09d46614c 100644 --- a/src/Explorer/Panes/AddCollectionPane.test.ts +++ b/src/Explorer/Panes/AddCollectionPane.test.ts @@ -74,7 +74,7 @@ describe("Add Collection Pane", () => { explorer.databaseAccount(mockFreeTierDatabaseAccount); const addCollectionPane = explorer.addCollectionPane as AddCollectionPane; expect(addCollectionPane.isFreeTierAccount()).toBe(true); - expect(addCollectionPane.upsellMessage()).toContain("With free tier discount"); + expect(addCollectionPane.upsellMessage()).toContain("With free tier"); expect(addCollectionPane.upsellAnchorUrl()).toBe(Constants.Urls.freeTierInformation); expect(addCollectionPane.upsellAnchorText()).toBe("Learn more"); }); diff --git a/src/Explorer/Panes/AddCollectionPane.ts b/src/Explorer/Panes/AddCollectionPane.ts index 08bd372f1..4818a3e47 100644 --- a/src/Explorer/Panes/AddCollectionPane.ts +++ b/src/Explorer/Panes/AddCollectionPane.ts @@ -89,6 +89,7 @@ export default class AddCollectionPane extends ContextualPaneBase { public isSynapseLinkUpdating: ko.Computed; public canExceedMaximumValue: ko.PureComputed; public ruToolTipText: ko.Computed; + public freeTierExceedThroughputTooltip: ko.Computed; public canConfigureThroughput: ko.PureComputed; public showUpsellMessage: ko.PureComputed; public shouldCreateMongoWildcardIndex: ko.Observable; @@ -99,7 +100,6 @@ export default class AddCollectionPane extends ContextualPaneBase { super(options); this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText()); this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled()); - this.showUpsellMessage = ko.pureComputed(() => !this.container.isServerlessEnabled()); this.formWarnings = ko.observable(); this.collectionId = ko.observable(); this.databaseId = ko.observable(); @@ -481,8 +481,20 @@ export default class AddCollectionPane extends ContextualPaneBase { this.resetData(); }); + this.freeTierExceedThroughputTooltip = ko.pureComputed(() => + this.isFreeTierAccount() && !this.container.isFirstResourceCreated() + ? "The first 400 RU/s in this account are free. Billing will apply to any throughput beyond 400 RU/s." + : "" + ); + this.upsellMessage = ko.pureComputed(() => { - return PricingUtils.getUpsellMessage(this.container.serverId(), this.isFreeTierAccount()); + return PricingUtils.getUpsellMessage( + this.container.serverId(), + this.isFreeTierAccount(), + this.container.isFirstResourceCreated(), + this.container.defaultExperience(), + true + ); }); this.upsellMessageAriaLabel = ko.pureComputed(() => { @@ -534,6 +546,23 @@ export default class AddCollectionPane extends ContextualPaneBase { return isFreeTierAccount; }); + this.showUpsellMessage = ko.pureComputed(() => { + if (this.container.isServerlessEnabled()) { + return false; + } + + if ( + this.isFreeTierAccount() && + !this.databaseCreateNew() && + this.databaseHasSharedOffer() && + !this.collectionWithThroughputInShared() + ) { + return false; + } + + return true; + }); + this.showIndexingOptionsForSharedThroughput = ko.computed(() => { const newDatabaseWithSharedOffer = this.databaseCreateNew() && this.databaseCreateNewShared(); const existingDatabaseWithSharedOffer = !this.databaseCreateNew() && this.databaseHasSharedOffer(); diff --git a/src/Explorer/Panes/AddDatabasePane.html b/src/Explorer/Panes/AddDatabasePane.html index e93da1ec0..b96f665fd 100644 --- a/src/Explorer/Panes/AddDatabasePane.html +++ b/src/Explorer/Panes/AddDatabasePane.html @@ -114,7 +114,8 @@ maxAutoPilotThroughputSet: maxAutoPilotThroughputSet, autoPilotUsageCost: autoPilotUsageCost, canExceedMaximumValue: canExceedMaximumValue, - showAutoPilot: !isFreeTierAccount() + showAutoPilot: !isFreeTierAccount(), + freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip }">

diff --git a/src/Explorer/Panes/AddDatabasePane.test.ts b/src/Explorer/Panes/AddDatabasePane.test.ts index d5844eedf..68cb461a9 100644 --- a/src/Explorer/Panes/AddDatabasePane.test.ts +++ b/src/Explorer/Panes/AddDatabasePane.test.ts @@ -77,7 +77,7 @@ describe("Add Database Pane", () => { explorer.databaseAccount(mockFreeTierDatabaseAccount); const addDatabasePane = explorer.addDatabasePane as AddDatabasePane; expect(addDatabasePane.isFreeTierAccount()).toBe(true); - expect(addDatabasePane.upsellMessage()).toContain("With free tier discount"); + expect(addDatabasePane.upsellMessage()).toContain("With free tier"); expect(addDatabasePane.upsellAnchorUrl()).toBe(Constants.Urls.freeTierInformation); expect(addDatabasePane.upsellAnchorText()).toBe("Learn more"); }); diff --git a/src/Explorer/Panes/AddDatabasePane.ts b/src/Explorer/Panes/AddDatabasePane.ts index 16f800923..5119923b0 100644 --- a/src/Explorer/Panes/AddDatabasePane.ts +++ b/src/Explorer/Panes/AddDatabasePane.ts @@ -44,6 +44,7 @@ export default class AddDatabasePane extends ContextualPaneBase { public autoPilotUsageCost: ko.Computed; public canExceedMaximumValue: ko.PureComputed; public ruToolTipText: ko.Computed; + public freeTierExceedThroughputTooltip: ko.Computed; public isFreeTierAccount: ko.Computed; public canConfigureThroughput: ko.PureComputed; public showUpsellMessage: ko.PureComputed; @@ -54,7 +55,6 @@ export default class AddDatabasePane extends ContextualPaneBase { this.databaseId = ko.observable(); this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText()); this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled()); - this.showUpsellMessage = ko.pureComputed(() => !this.container.isServerlessEnabled()); this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue()); @@ -182,6 +182,18 @@ export default class AddDatabasePane extends ContextualPaneBase { return isFreeTierAccount; }); + this.showUpsellMessage = ko.pureComputed(() => { + if (this.container.isServerlessEnabled()) { + return false; + } + + if (this.isFreeTierAccount()) { + return this.databaseCreateNewShared(); + } + + return true; + }); + this.maxThroughputRUText = ko.pureComputed(() => { return this.maxThroughputRU().toLocaleString(); }); @@ -219,8 +231,20 @@ export default class AddDatabasePane extends ContextualPaneBase { this.resetData(); }); + this.freeTierExceedThroughputTooltip = ko.pureComputed(() => + this.isFreeTierAccount() && !this.container.isFirstResourceCreated() + ? "The first 400 RU/s in this account are free. Billing will apply to any throughput beyond 400 RU/s." + : "" + ); + this.upsellMessage = ko.pureComputed(() => { - return PricingUtils.getUpsellMessage(this.container.serverId(), this.isFreeTierAccount()); + return PricingUtils.getUpsellMessage( + this.container.serverId(), + this.isFreeTierAccount(), + this.container.isFirstResourceCreated(), + this.container.defaultExperience(), + false + ); }); this.upsellMessageAriaLabel = ko.pureComputed(() => { diff --git a/src/Explorer/Tabs/DatabaseSettingsTab.html b/src/Explorer/Tabs/DatabaseSettingsTab.html index d3f01198a..e18080bbb 100644 --- a/src/Explorer/Tabs/DatabaseSettingsTab.html +++ b/src/Explorer/Tabs/DatabaseSettingsTab.html @@ -23,6 +23,19 @@

Scale
+
+ Info + With free tier, you'll get the first 400 RU/s and 5 GB of storage in this account for free. To keep your + account free, keep the total RU/s across all resources in the account to 400 RU/s. + + Learn more. + +
diff --git a/src/Explorer/Tabs/DatabaseSettingsTab.ts b/src/Explorer/Tabs/DatabaseSettingsTab.ts index b87adbf2b..492eba648 100644 --- a/src/Explorer/Tabs/DatabaseSettingsTab.ts +++ b/src/Explorer/Tabs/DatabaseSettingsTab.ts @@ -57,6 +57,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. public canThroughputExceedMaximumValue: ko.Computed; public costsVisible: ko.Computed; public displayedError: ko.Observable; + public isFreeTierAccount: ko.Computed; public isTemplateReady: ko.Observable; public minRUAnotationVisible: ko.Computed; public minRUs: ko.Observable; @@ -82,6 +83,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. public throughputAutoPilotRadioId: string; public throughputProvisionedRadioId: string; public throughputModeRadioName: string; + public freeTierExceedThroughputWarning: ko.Computed; private _hasProvisioningTypeChanged: ko.Computed; private _wasAutopilotOriginallySet: ko.Observable; @@ -359,6 +361,17 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. this.isTemplateReady = ko.observable(false); + this.isFreeTierAccount = ko.computed(() => { + const databaseAccount = this.container?.databaseAccount(); + return databaseAccount?.properties?.enableFreeTier; + }); + + this.freeTierExceedThroughputWarning = ko.computed(() => + this.isFreeTierAccount() + ? "Billing will apply if you provision more than 400 RU/s of manual throughput, or if the resource scales beyond 400 RU/s with autoscale." + : "" + ); + this._buildCommandBarOptions(); } diff --git a/src/Utils/PricingUtils.ts b/src/Utils/PricingUtils.ts index af7d1d8d7..686eedebc 100644 --- a/src/Utils/PricingUtils.ts +++ b/src/Utils/PricingUtils.ts @@ -1,5 +1,6 @@ import * as AutoPilotUtils from "../Utils/AutoPilotUtils"; import * as Constants from "../Shared/Constants"; +import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType"; interface ComputeRUUsagePriceHourlyArgs { serverId: string; @@ -256,9 +257,19 @@ export function getEstimatedSpendAcknowledgeString( )} - ${currencySign}${calculateEstimateNumber(monthlyPrice)} monthly cost for the throughput above.`; } -export function getUpsellMessage(serverId = "default", isFreeTier = false): string { +export function getUpsellMessage( + serverId = "default", + isFreeTier = false, + isFirstResourceCreated = false, + defaultExperience: string, + isCollection: boolean +): string { if (isFreeTier) { - return "With free tier discount, you'll get the first 400 RU/s and 5 GB of storage in this account for free. Charges will apply if your resource throughput exceeds 400 RU/s."; + const collectionName = getCollectionName(defaultExperience); + const resourceType = isCollection ? collectionName : "database"; + return isFirstResourceCreated + ? `The free tier discount of 400 RU/s has already been applied to a database or ${collectionName} in this account. Billing will apply to this ${resourceType} after it is created.` + : `With free tier, you'll get the first 400 RU/s and 5 GB of storage in this account for free. Billing will apply if you provision more than 400 RU/s of manual throughput, or if the ${resourceType} scales beyond 400 RU/s with autoscale.`; } else { let price: number = Constants.OfferPricing.MonthlyPricing.default.Standard.StartingPrice; @@ -269,3 +280,19 @@ export function getUpsellMessage(serverId = "default", isFreeTier = false): stri return `Start at ${getCurrencySign(serverId)}${price}/mo per database, multiple containers included`; } } + +function getCollectionName(defaultExperience: string): string { + switch (defaultExperience) { + case DefaultAccountExperienceType.DocumentDB: + return "container"; + case DefaultAccountExperienceType.MongoDB: + return "collection"; + case DefaultAccountExperienceType.Table: + case DefaultAccountExperienceType.Cassandra: + return "table"; + case DefaultAccountExperienceType.Graph: + return "graph"; + default: + throw Error("unknown API type"); + } +}