Compare commits

..

81 Commits

Author SHA1 Message Date
Steve Faulkner
c857b9aab9 Remove any 2021-01-19 10:05:48 -06:00
Srinath Narayanan
cd45f2943d Merge branch 'master' into users/srnara/selfserve 2021-01-18 16:37:45 -08:00
Srinath Narayanan
385c9f216f Addressed PR comments 2021-01-18 16:21:31 -08:00
Deborah Chen
8c40df0fa1 Adding in experimentation for autoscale test (#345)
* Adding autoscale flight info

* Add flight info to cassandra collection pane

* Add telemetry for autoscale toggle on/off in create resource blade and scale/settings

* Run formatting and add expected properties to test file

* removing empty line

* Updating to pass unit tests

Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-01-15 17:15:15 -06:00
Srinath Narayanan
d17508cc27 Added SelfServeComponent 2021-01-15 12:34:08 -08:00
Srinath Narayanan
2ec2a891b4 renamed dropdown -> choice 2021-01-15 08:27:26 -08:00
Steve Faulkner
fcbc9474ea Remove Preview for Synapse Link (#389) 2021-01-15 09:51:14 -06:00
Srinath Narayanan
b34628e9fc Merge branch 'master' into users/srnara/selfserve 2021-01-15 07:39:50 -08:00
Srinath Narayanan
3fb4af53c8 Removed reactbinding changes 2021-01-15 07:09:14 -08:00
Steve Faulkner
81f861af39 Empty commit to refresh nuget after transient failures 2021-01-14 17:37:24 -06:00
victor-meng
9afa29cdb6 Properly construct the query to delete Cassandra row (#388) 2021-01-14 16:59:31 -06:00
Chris-MS-896
9a1e8b2d87 Add rest of three utils files to Matser (#370)
* 'minor change'
2021-01-13 17:49:06 -06:00
Srinath Narayanan
41f37055ef Merge branch 'master' into users/srnara/selfserve 2021-01-13 06:21:29 -08:00
Srinath Narayanan
aa925d8d54 fixed compilation errors 2021-01-13 06:12:37 -08:00
Srinath Narayanan
c9cea86225 fixed lint errors 2021-01-13 05:26:47 -08:00
Srinath Narayanan
318842624f Resolved PR comments
Added tests
Moved onSubmt and initialize inside base class
Moved testExplorer to separate folder
made fields of SelfServe Class non static
2021-01-12 22:02:45 -08:00
Tim Sander
babda4d9cb fix issue where Mongo indexing checkbox stops adding wildcard index (#384) 2021-01-12 18:38:16 -06:00
Steve Faulkner
9d20a13dd4 Warn on SubQuery (#378) 2021-01-12 13:53:15 -06:00
Chris-MS-896
3effbe6991 no message (#372) 2021-01-12 13:09:20 -06:00
Chris-MS-896
af53697ff4 Add file of Terminal to Master (#371)
* "minor changes"
2021-01-12 12:55:47 -06:00
Chris-MS-896
b1ad80480e Add two files of notebook component in Matser (#363)
* “minor changes”
2021-01-12 12:55:21 -06:00
Armando Trejo Oliver
9247a6c4a2 A11y fixes - Add a skip link and remove duplicate ids (#381)
* Add a skip link to allow people who navigate sequentially through content more direct access to the primary content of the Data Explorer

Co-authored-by: Chris Cao (Zen3 Infosolutions America Inc) <v-yiqcao@microsoft.com>

* Rename id of partition key field in  Add Collection Pane to ensure no  elements contain duplicate attributes.

Co-authored-by: Chris Cao (Zen3 Infosolutions America Inc) <v-yiqcao@microsoft.com>
2021-01-12 09:55:04 -08:00
Steve Faulkner
767d46480e Revert TablesEntitiyListViewModel changes (#382) 2021-01-11 16:16:40 -06:00
Chris-MS-896
2d98c5d269 add ArraysByKeyCache.ts (#366)
* 'add ArraysByKeyCache'
* "minor change"
2021-01-08 22:51:50 -06:00
Steve Faulkner
6627172a52 Add Architecture Diagram to README (#380) 2021-01-08 22:20:40 -06:00
Steve Faulkner
19fa5e17a5 Fix JSONEditor bug with undefined value (#379) 2021-01-08 22:20:06 -06:00
Chris-MS-896
a4a367a212 Add all arm request related files to Matser (#373)
* “minor changes”
* 'changes for unit test'
2021-01-08 21:56:29 -06:00
Chris-MS-896
983c9201bb Add two files of GraphExplorer component in Master (#365) 2021-01-08 21:14:53 -06:00
Chris-MS-896
76d7f00a90 Add two files of Table to master (#364) 2021-01-08 20:56:59 -06:00
Chris-MS-896
6490597736 add CollapsiblePanel/CollapsiblePanelComponent.ts and /ErrorDisplayComponent to Master (#357) 2021-01-08 20:29:15 -06:00
Chris-MS-896
229119e697 add file offerUtility to tsconfig (#356) 2021-01-08 20:14:12 -06:00
Steve Faulkner
ceefd7c615 Fix Conflict Resolution path setting (#377)
* Fix Conflict Resolution path setting

* Fix test
2021-01-08 12:36:44 -06:00
Laurent Nguyen
6e619175c6 Fix missing scrollbar in left pane when too many collections/notebooks (#375)
Constrain left pane container to height: 100% so that scrollbar show up when content wants to overflow.
The `main` classname seems too generic, but I left it alone (so I don't break anything), since this part will eventually be ported to React.
2021-01-08 14:00:26 +00:00
victor-meng
08e8bf4bcf Fix two settings tab issues (#374) 2021-01-07 15:38:13 -06:00
Chris-MS-896
89dc0f394b Add Spliter file to Master (#358) 2021-01-06 12:51:42 -06:00
Chris-MS-896
30e0001b7f no message (#359) 2021-01-05 16:45:13 -06:00
Steve Faulkner
4a8f408112 Add UX for Mongo indexing experiment (#368)
Co-authored-by: Tim Sander <tisande@microsoft.com>
2021-01-05 16:04:55 -06:00
Armando Trejo Oliver
e801364800 Remove stale .main class from tree.less (#362)
.main CSS class has a naming conflict with Moncao editor CSS classes and this is causing  A11y issues with Moncao editor.

This class should no longer be used since we moved to the new tree component in REACT, so I am removing it. From my testing, this is not affecting anything.

If we find any styling issue later, we should fix without adding back this class.
2021-01-05 10:53:55 -08:00
Srinath Narayanan
373327dc88 removed unnecessary changes 2021-01-04 18:34:24 -08:00
Srinath Narayanan
8333ee7ec4 Merge branch 'master' into users/srnara/selfserve 2021-01-04 18:29:33 -08:00
Srinath Narayanan
97116175ab Added comments for Example 2021-01-04 18:15:09 -08:00
victor-meng
a55f2d0de9 Free tier improvements in DE (#348)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-01-04 12:56:55 -08:00
Steve Faulkner
d40b1aa9b5 Remove Empty Query Logging (#361) 2021-01-04 13:58:01 -06:00
Steve Faulkner
cc63cdc1fd Remove dependency on canvas (#354) 2020-12-26 21:56:37 -06:00
Srinath Narayanan
f770bb193e renamed sqlx->example and added invalid type 2020-12-22 22:25:52 -08:00
Srinath Narayanan
8cb8d10bc3 added feature for self serve type 2020-12-22 21:10:27 -08:00
Srinath Narayanan
b298caf9ff removed landingpage 2020-12-21 03:44:46 -08:00
Srinath Narayanan
a2022fbbac added inital open from data explorer 2020-12-21 03:40:48 -08:00
Steve Faulkner
c3058ee5a9 Check for undefined query results (#350) 2020-12-18 19:55:32 -06:00
Steve Faulkner
b000631a0c Revert web.config changes (#349) 2020-12-18 19:26:10 -06:00
vchske
e8f4c8f93c 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 <southpolesteve@gmail.com>
2020-12-18 16:15:55 -08:00
Steve Faulkner
16bde97e47 Rewrite URL for IE users (#340) 2020-12-18 16:08:40 -06:00
Steve Faulkner
6da43ee27b Publish IE specific Nuget package (#347)
* Publish IE specific Nuget package

* Require ally tests to pass
2020-12-17 17:41:38 -06:00
Gahl Levy
ebae484b8f Fix duplicate settings tabs (#343)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2020-12-17 13:01:36 -08:00
Steve Faulkner
dfb1b50621 Explorer.ts Cleanup (#341)
Co-authored-by: victor-meng <56978073+victor-meng@users.noreply.github.com>
2020-12-16 20:00:39 -06:00
victor-meng
f54e8eb692 Move queryDocuments out of DataAccessUtility (#334) 2020-12-16 15:27:17 -08:00
Steve Faulkner
ea39c1d092 Fix offer update notification for AAD users (#338) 2020-12-11 13:38:57 -06:00
vchske
c21f42159f 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
2020-12-11 10:06:43 -08:00
Srinath Narayanan
b3b57462ef Added overall defaults 2020-12-10 16:59:16 -08:00
victor-meng
31e4b49f11 Only call getCollectionDataUsageSize for AAD users (#337) 2020-12-10 14:13:08 -08:00
Tanuj Mittal
40491ec9c5 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
2020-12-10 13:09:18 -08:00
Srinath Narayanan
95fc75cb23 Added custom renderer as async type 2020-12-10 12:50:04 -08:00
Tanuj Mittal
e133df18dd Record baseUrl for OpenTerminal success/failure telemetry (#335)
This is useful to know which terminal is opening.
2020-12-10 19:54:21 +00:00
Srinath Narayanan
c97eb6018b Made selfServe standalone page 2020-12-08 03:00:12 -08:00
Srinath Narayanan
90fb7e7d8f added custom element and base class 2020-12-07 02:23:20 -08:00
Srinath Narayanan
2dbde9c31a proper resolution of promises 2020-12-03 01:11:07 -08:00
Srinath Narayanan
4381ea447c removed type requirement 2020-12-02 01:45:59 -08:00
Steve Faulkner
0532ed26a2 Remove runner workflow that is no longer functioning (#332) 2020-12-01 10:23:18 -06:00
Srinath Narayanan
69b17f1a00 Added Recursive add 2020-12-01 03:57:54 -08:00
victor-meng
fd60c9c15e Remove RUPM (#328)
Remove all RUPM code
2020-12-01 07:06:38 +00:00
Chris-MS-896
04ab1f3918 '[Visual Requirement-Data Explorer (iframe)] On the Data Explorer page, luminosity contrast ratio of the borderline button is less than 3.:1.' (#331) 2020-11-30 15:32:28 -06:00
Chris-MS-896
b784ac0f86 [967093][Screen Readers- CosmosDB – Notification] Screen reader does not pass the combo-box list information (#329)
* ‘Bug fix: Screen reader does not pass the combo-box list information under notification field.’

* ‘update for comments’

* ‘load path refator’
2020-11-30 14:33:18 -06:00
Srinath Narayanan
28899f63d7 Fixed bug in fetching 'index transformation progress' header (#330)
* bug fix

* fixed formatting errors
2020-11-24 10:32:18 -08:00
Srinath Narayanan
8cf160d818 added todo comment and removed console.log 2020-11-24 07:37:52 -08:00
Srinath Narayanan
88d71d7070 working version 2020-11-23 17:46:59 -08:00
Srinath Narayanan
84017660c1 added recursion and inition decorators 2020-11-23 14:21:52 -08:00
victor-meng
9cbf632577 Get collection usage size with ARM metrics call (#327)
- Removed `readCollectionQuotaInfo` call. The only data we need is the usage size which we can get via the ARM metrics call instead.
- Added `getCollectionUsageSize` which fetches the `DataUsage` and `IndexUsage` metrics, converts them to KB, and returns the sum as the total usage size
2020-11-20 20:21:16 +00:00
victor-meng
17fd2185dc Move read offer to RP (#326) 2020-11-19 17:13:11 -08:00
Srinath Narayanan
a93c8509cd Added testExplorer and notebooks UI automated tests (#323)
* initial commit for notbooks pupeteer tests

* Added Auth

* added try catch block with screenshot for error

* Addressed PR comments

* renamed params

* renamed param

* fixed formatting error

* Updates mongo spec to remove waitFor on already awaited selector

* added logging statements

* format errors fixed

* added ci env variables

* increased delay for render

* removed logging

* added delay

* fix format error

* removed deletion

* reverted package.json change

Co-authored-by: zfoster <notzachfoster@gmail.com>
2020-11-19 09:29:38 -08:00
Laurent Nguyen
5c93c11bd9 Bug fix: match monaco-editor version with @nteract/monaco-editor (#322)
Opening notebook (which contains code cell), then "Items"-> document editor is broken. Our JsonEditor component will hang at `monaco.editor.create()`.

Matching the `monaco-editor` version with `@nteract/monaco-editor` fixes it.

I looked into using one, but we cannot rely nteract, because it does not get loaded if notebook isn't enabled. Forcing nteract to use ours (if at all possible) isn't a good idea, since their code is tuned to their version.

For now, we'll have to keep the versions in sync.
2020-11-19 14:33:23 +00:00
Srinath Narayanan
85d2378d3a Removed SettingsV1 code paths (#325)
* removed settingsv1 code path in collection.ts

* removed Settingsv1 code

* Moved AAD error message up the chain
2020-11-18 12:11:25 -08:00
198 changed files with 10684 additions and 10919 deletions

View File

@@ -3,7 +3,11 @@ PORTAL_RUNNER_PASSWORD=
PORTAL_RUNNER_SUBSCRIPTION=
PORTAL_RUNNER_RESOURCE_GROUP=
PORTAL_RUNNER_DATABASE_ACCOUNT=
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY=
PORTAL_RUNNER_CONNECTION_STRING=
NOTEBOOKS_TEST_RUNNER_TENANT_ID=
NOTEBOOKS_TEST_RUNNER_CLIENT_ID=
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET=
CASSANDRA_CONNECTION_STRING=
MONGO_CONNECTION_STRING=
TABLES_CONNECTION_STRING=

View File

@@ -14,7 +14,6 @@ src/Common/DataAccessUtilityBase.ts
src/Common/DeleteFeedback.ts
src/Common/DocumentClientUtilityBase.ts
src/Common/EditableUtility.ts
src/Common/EnvironmentUtility.ts
src/Common/HashMap.test.ts
src/Common/HashMap.ts
src/Common/HeadersUtility.test.ts
@@ -202,8 +201,6 @@ src/Explorer/Tabs/QueryTab.test.ts
src/Explorer/Tabs/QueryTab.ts
src/Explorer/Tabs/QueryTablesTab.ts
src/Explorer/Tabs/ScriptTabBase.ts
src/Explorer/Tabs/SettingsTab.test.ts
src/Explorer/Tabs/SettingsTab.ts
src/Explorer/Tabs/SparkMasterTab.ts
src/Explorer/Tabs/StoredProcedureTab.ts
src/Explorer/Tabs/TabComponents.ts
@@ -290,8 +287,6 @@ src/Utils/DatabaseAccountUtils.ts
src/Utils/JunoUtils.ts
src/Utils/MessageValidation.ts
src/Utils/NotebookConfigurationUtils.ts
src/Utils/OfferUtils.test.ts
src/Utils/OfferUtils.ts
src/Utils/PricingUtils.test.ts
src/Utils/QueryUtils.test.ts
src/Utils/QueryUtils.ts

View File

@@ -101,6 +101,7 @@ jobs:
PLATFORM: "Emulator"
NODE_TLS_REJECT_UNAUTHORIZED: 0
- uses: actions/upload-artifact@v2
if: failure()
with:
name: screenshots
path: failed-*
@@ -146,19 +147,27 @@ jobs:
shell: bash
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
PORTAL_RUNNER_SUBSCRIPTION: ${{ secrets.PORTAL_RUNNER_SUBSCRIPTION }}
PORTAL_RUNNER_RESOURCE_GROUP: ${{ secrets.PORTAL_RUNNER_RESOURCE_GROUP }}
PORTAL_RUNNER_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT }}
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY }}
NOTEBOOKS_TEST_RUNNER_TENANT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_TENANT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
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-*
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 }}
@@ -182,7 +191,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 }}
@@ -204,3 +213,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"

View File

@@ -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

BIN
.vs/slnx.sqlite Normal file

Binary file not shown.

View File

@@ -13,29 +13,18 @@ UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), ht
### Watch mode
Run `npm run watch` to start the development server and automatically rebuild on changes
Run `npm start` to start the development server and automatically rebuild on changes
### Specifying Development Platform
### Hosted Development (https://cosmos.azure.com)
Setting the environment variable `PLATFORM` during the build process will force the explorer to load the specified platform. By default in development it will run in `Hosted` mode. Valid options:
- Hosted
- Emulator
- Portal
`PLATFORM=Emulator npm run watch`
### Hosted Development
The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
To run pure hosted mode, in `webpack.config.js` change index HtmlWebpackPlugin to use hostedExplorer.html and change entry for index to use HostedExplorer.ts.
- Visit: `https://localhost:1234/hostedExplorer.html`
- Local sign in via AAD will NOT work. Connection string only in dev mode. Use the Portal if you need AAD auth.
- The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
### Emulator Development
In a window environment, running `npm run build` will automatically copy the built files from `/dist` over to the default emulator install paths. In a non-windows environment you can specify an alternate endpoint using `EMULATOR_ENDPOINT` and webpack dev server will proxy requests for you.
`PLATFORM=Emulator EMULATOR_ENDPOINT=https://my-vm.azure.com:8081 npm run watch`
- Start the Cosmos Emulator
- Visit: https://localhost:1234/index.html
#### Setting up a Remote Emulator
@@ -55,16 +44,8 @@ The Cosmos emulator currently only runs in Windows environments. You can still d
### Portal Development
The Cosmos Portal that consumes this repo is not currently open source. If you have access to this project, `npm run build` will copy the built files over to the portal where they will be loaded by the portal development environment
You can however load a local running instance of data explorer in the production portal.
1. Turn off browser SSL validation for localhost: chrome://flags/#allow-insecure-localhost OR Install valid SSL certs for localhost (on IE, follow these [instructions](https://www.technipages.com/ie-bypass-problem-with-this-websites-security-certificate) to install the localhost certificate in the right place)
2. Allowlist `https://localhost:1234` domain for CORS in the Azure Cosmos DB portal
3. Start the project in portal mode: `PLATFORM=Portal npm run watch`
4. Load the portal using the following link: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html
Live reload will occur, but data explorer will not properly integrate again with the parent iframe. You will have to manually reload the page.
- Visit: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html
- You may have to manually visit https://localhost:1234/explorer.html first and click through any SSL certificate warnings
### Testing
@@ -88,6 +69,10 @@ Jest and Puppeteer are used for end to end browser based tests and are contained
We generally adhere to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details.
### Architechture
[![](https://mermaid.ink/img/eyJjb2RlIjoiZ3JhcGggTFJcbiAgaG9zdGVkKGh0dHBzOi8vY29zbW9zLmF6dXJlLmNvbSlcbiAgcG9ydGFsKFBvcnRhbClcbiAgZW11bGF0b3IoRW11bGF0b3IpXG4gIGFhZFtBQURdXG4gIHJlc291cmNlVG9rZW5bUmVzb3VyY2UgVG9rZW5dXG4gIGNvbm5lY3Rpb25TdHJpbmdbQ29ubmVjdGlvbiBTdHJpbmddXG4gIHBvcnRhbFRva2VuW0VuY3J5cHRlZCBQb3J0YWwgVG9rZW5dXG4gIG1hc3RlcktleVtNYXN0ZXIgS2V5XVxuICBhcm1bQVJNIFJlc291cmNlIFByb3ZpZGVyXVxuICBkYXRhcGxhbmVbRGF0YSBQbGFuZV1cbiAgcHJveHlbUG9ydGFsIEFQSSBQcm94eV1cbiAgc3FsW1NRTF1cbiAgbW9uZ29bTW9uZ29dXG4gIHRhYmxlc1tUYWJsZXNdXG4gIGNhc3NhbmRyYVtDYXNzYW5kcmFdXG4gIGdyYWZbR3JhcGhdXG5cblxuICBlbXVsYXRvciAtLT4gbWFzdGVyS2V5IC0tLS0-IGRhdGFwbGFuZVxuICBwb3J0YWwgLS0-IGFhZFxuICBob3N0ZWQgLS0-IHBvcnRhbFRva2VuICYgcmVzb3VyY2VUb2tlbiAmIGNvbm5lY3Rpb25TdHJpbmcgJiBhYWRcbiAgYWFkIC0tLT4gYXJtXG4gIGFhZCAtLS0-IGRhdGFwbGFuZVxuICBhYWQgLS0tPiBwcm94eVxuICByZXNvdXJjZVRva2VuIC0tLT4gc3FsIC0tPiBkYXRhcGxhbmVcbiAgcG9ydGFsVG9rZW4gLS0tPiBwcm94eVxuICBwcm94eSAtLT4gZGF0YXBsYW5lXG4gIGNvbm5lY3Rpb25TdHJpbmcgLS0-IHNxbCAmIG1vbmdvICYgY2Fzc2FuZHJhICYgZ3JhZiAmIHRhYmxlc1xuICBzcWwgLS0-IGRhdGFwbGFuZVxuICB0YWJsZXMgLS0-IGRhdGFwbGFuZVxuICBtb25nbyAtLT4gcHJveHlcbiAgY2Fzc2FuZHJhIC0tPiBwcm94eVxuICBncmFmIC0tPiBwcm94eVxuXG5cdFx0IiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggTFJcbiAgaG9zdGVkKGh0dHBzOi8vY29zbW9zLmF6dXJlLmNvbSlcbiAgcG9ydGFsKFBvcnRhbClcbiAgZW11bGF0b3IoRW11bGF0b3IpXG4gIGFhZFtBQURdXG4gIHJlc291cmNlVG9rZW5bUmVzb3VyY2UgVG9rZW5dXG4gIGNvbm5lY3Rpb25TdHJpbmdbQ29ubmVjdGlvbiBTdHJpbmddXG4gIHBvcnRhbFRva2VuW0VuY3J5cHRlZCBQb3J0YWwgVG9rZW5dXG4gIG1hc3RlcktleVtNYXN0ZXIgS2V5XVxuICBhcm1bQVJNIFJlc291cmNlIFByb3ZpZGVyXVxuICBkYXRhcGxhbmVbRGF0YSBQbGFuZV1cbiAgcHJveHlbUG9ydGFsIEFQSSBQcm94eV1cbiAgc3FsW1NRTF1cbiAgbW9uZ29bTW9uZ29dXG4gIHRhYmxlc1tUYWJsZXNdXG4gIGNhc3NhbmRyYVtDYXNzYW5kcmFdXG4gIGdyYWZbR3JhcGhdXG5cblxuICBlbXVsYXRvciAtLT4gbWFzdGVyS2V5IC0tLS0-IGRhdGFwbGFuZVxuICBwb3J0YWwgLS0-IGFhZFxuICBob3N0ZWQgLS0-IHBvcnRhbFRva2VuICYgcmVzb3VyY2VUb2tlbiAmIGNvbm5lY3Rpb25TdHJpbmcgJiBhYWRcbiAgYWFkIC0tLT4gYXJtXG4gIGFhZCAtLS0-IGRhdGFwbGFuZVxuICBhYWQgLS0tPiBwcm94eVxuICByZXNvdXJjZVRva2VuIC0tLT4gc3FsIC0tPiBkYXRhcGxhbmVcbiAgcG9ydGFsVG9rZW4gLS0tPiBwcm94eVxuICBwcm94eSAtLT4gZGF0YXBsYW5lXG4gIGNvbm5lY3Rpb25TdHJpbmcgLS0-IHNxbCAmIG1vbmdvICYgY2Fzc2FuZHJhICYgZ3JhZiAmIHRhYmxlc1xuICBzcWwgLS0-IGRhdGFwbGFuZVxuICB0YWJsZXMgLS0-IGRhdGFwbGFuZVxuICBtb25nbyAtLT4gcHJveHlcbiAgY2Fzc2FuZHJhIC0tPiBwcm94eVxuICBncmFmIC0tPiBwcm94eVxuXG5cdFx0IiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)
# Contributing
Please read the [contribution guidelines](./CONTRIBUTING.md).

View File

@@ -1,3 +1,4 @@
module.exports = {
presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-react", "@babel/preset-typescript"]
presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-react", "@babel/preset-typescript"],
plugins: [["@babel/plugin-proposal-decorators", { legacy: true }]]
};

7
canvas/README.md Normal file
View File

@@ -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

1
canvas/index.js Normal file
View File

@@ -0,0 +1 @@
module.exports = {}

11
canvas/package.json Normal file
View File

@@ -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"
}

View File

@@ -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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,11 @@
@NavMediumSpace: 10px;
@NavLargeSpace: 15px;
.skip-link {
position: fixed;
top: -200px;
}
html {
font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
padding: 0px;

View File

@@ -1,20 +1,12 @@
@import "./Common/Constants";
.main {
width: 100%;
float: left;
transition: all .0s ease-in-out;
-ms-transition: all 0s ease-in-out;
-webkit-transition: all 0s ease-in-out;
-moz-transition: all .0s ease-in-out;
height: 100%;
background-color: white;
border-left: 0px solid white;
}
.resourceTree {
height: 100%;
flex: 0 0 auto;
.main {
height: 100%;
}
}
.resourceTreeScroll {

3909
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,12 @@
"description": "Cosmos Explorer",
"main": "index.js",
"dependencies": {
"@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "3.9.0",
"@azure/cosmos-language-service": "0.0.4",
"@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.1.0",
"@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12",
"@jupyterlab/services": "6.0.0-rc.2",
"@jupyterlab/terminal": "3.0.0-rc.2",
"@microsoft/applicationinsights-web": "2.5.9",
@@ -42,7 +46,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",
@@ -66,7 +70,7 @@
"jquery-ui-dist": "1.12.1",
"knockout": "3.5.1",
"mkdirp": "1.0.4",
"monaco-editor": "0.15.6",
"monaco-editor": "0.18.1",
"object.entries": "1.1.0",
"office-ui-fabric-react": "7.134.1",
"p-retry": "4.2.0",
@@ -83,6 +87,7 @@
"react-notification-system": "0.2.17",
"react-redux": "7.1.3",
"redux": "4.0.4",
"reflect-metadata": "0.1.13",
"rx-jupyter": "5.5.12",
"rxjs": "6.6.3",
"styled-components": "4.3.2",
@@ -115,7 +120,7 @@
"@types/prop-types": "15.5.8",
"@types/puppeteer": "3.0.1",
"@types/q": "1.5.1",
"@types/react": "16.9.49",
"@types/react": "16.9.56",
"@types/react-dom": "16.0.7",
"@types/react-notification-system": "0.2.39",
"@types/react-redux": "7.1.7",

View File

@@ -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'))"
]
}
}

View File

@@ -1,441 +1,437 @@
import { HashMap } from "./HashMap";
export class AuthorizationEndpoints {
public static arm: string = "https://management.core.windows.net/";
public static common: string = "https://login.windows.net/";
}
export class CodeOfConductEndpoints {
public static privacyStatement: string = "https://aka.ms/ms-privacy-policy";
public static codeOfConduct: string = "https://aka.ms/cosmos-code-of-conduct";
public static termsOfUse: string = "https://aka.ms/ms-terms-of-use";
}
export class EndpointsRegex {
public static readonly cassandra = [
"AccountEndpoint=(.*).cassandra.cosmosdb.azure.com",
"HostName=(.*).cassandra.cosmos.azure.com"
];
public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com";
public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com";
public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com";
public static readonly table = "TableEndpoint=https://(.*).table.cosmosdb.azure.com";
}
export class ApiEndpoints {
public static runtimeProxy: string = "/api/RuntimeProxy";
public static guestRuntimeProxy: string = "/api/guest/RuntimeProxy";
}
export class ServerIds {
public static localhost: string = "localhost";
public static blackforest: string = "blackforest";
public static fairfax: string = "fairfax";
public static mooncake: string = "mooncake";
public static productionPortal: string = "prod";
public static dev: string = "dev";
}
export class ArmApiVersions {
public static readonly documentDB: string = "2015-11-06";
public static readonly arcadia: string = "2019-06-01-preview";
public static readonly arcadiaLivy: string = "2019-11-01-preview";
public static readonly arm: string = "2015-11-01";
public static readonly armFeatures: string = "2014-08-01-preview";
public static readonly publicVersion = "2020-04-01";
}
export class ArmResourceTypes {
public static readonly notebookWorkspaces = "Microsoft.DocumentDB/databaseAccounts/notebookWorkspaces";
public static readonly synapseWorkspaces = "Microsoft.Synapse/workspaces";
}
export class BackendDefaults {
public static partitionKeyKind: string = "Hash";
public static singlePartitionStorageInGb: string = "10";
public static multiPartitionStorageInGb: string = "100";
public static maxChangeFeedRetentionDuration: number = 10;
public static partitionKeyVersion = 2;
}
export class ClientDefaults {
public static requestTimeoutMs: number = 60000;
public static portalCacheTimeoutMs: number = 10000;
public static errorNotificationTimeoutMs: number = 5000;
public static copyHelperTimeoutMs: number = 2000;
public static waitForDOMElementMs: number = 500;
public static cacheBustingTimeoutMs: number =
10 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/;
public static databaseThroughputIncreaseFactor: number = 100;
public static readonly arcadiaTokenRefreshInterval: number =
20 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/;
public static readonly arcadiaTokenRefreshIntervalPaddingMs: number = 2000;
}
export class AccountKind {
public static DocumentDB: string = "DocumentDB";
public static MongoDB: string = "MongoDB";
public static Parse: string = "Parse";
public static GlobalDocumentDB: string = "GlobalDocumentDB";
public static Default: string = AccountKind.DocumentDB;
}
export class CorrelationBackend {
public static Url: string = "https://aka.ms/cosmosdbanalytics";
}
export class DefaultAccountExperience {
public static DocumentDB: string = "DocumentDB";
public static Graph: string = "Graph";
public static MongoDB: string = "MongoDB";
public static ApiForMongoDB: string = "Azure Cosmos DB for MongoDB API";
public static Table: string = "Table";
public static Cassandra: string = "Cassandra";
public static Default: string = DefaultAccountExperience.DocumentDB;
}
export class CapabilityNames {
public static EnableTable: string = "EnableTable";
public static EnableGremlin: string = "EnableGremlin";
public static EnableCassandra: string = "EnableCassandra";
public static EnableAutoScale: string = "EnableAutoScale";
public static readonly EnableNotebooks: string = "EnableNotebooks";
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
public static readonly EnableMongo: string = "EnableMongo";
public static readonly EnableServerless: string = "EnableServerless";
}
export class Features {
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";
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";
public static readonly notebookServerUrl = "notebookserverurl";
public static readonly notebookServerToken = "notebookservertoken";
public static readonly notebookBasePath = "notebookbasepath";
public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
public static readonly ttl90Days = "ttl90days";
public static readonly enableRightPanelV2 = "enablerightpanelv2";
public static readonly enableSchema = "enableschema";
public static readonly enableSDKoperations = "enablesdkoperations";
public static readonly showMinRUSurvey = "showminrusurvey";
}
// flight names returned from the portal are always lowercase
export class Flights {
public static readonly SettingsV2 = "settingsv2";
public static readonly MongoIndexEditor = "mongoindexeditor";
}
export class AfecFeatures {
public static readonly Spark = "spark-public-preview";
public static readonly Notebooks = "sparknotebooks-public-preview";
public static readonly StorageAnalytics = "storageanalytics-public-preview";
}
export class Spark {
public static readonly MaxWorkerCount = 10;
public static readonly SKUs: HashMap<string> = new HashMap({
"Cosmos.Spark.D1s": "D1s / 1 core / 4GB RAM",
"Cosmos.Spark.D2s": "D2s / 2 cores / 8GB RAM",
"Cosmos.Spark.D4s": "D4s / 4 cores / 16GB RAM",
"Cosmos.Spark.D8s": "D8s / 8 cores / 32GB RAM",
"Cosmos.Spark.D16s": "D16s / 16 cores / 64GB RAM",
"Cosmos.Spark.D32s": "D32s / 32 cores / 128GB RAM",
"Cosmos.Spark.D64s": "D64s / 64 cores / 256GB RAM"
});
}
export class TagNames {
public static defaultExperience: string = "defaultExperience";
}
export class MongoDBAccounts {
public static protocol: string = "https";
public static defaultPort: string = "10255";
}
export enum MongoBackendEndpointType {
local,
remote
}
// TODO: 435619 Add default endpoints per cloud and use regional only when available
export class CassandraBackend {
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
public static readonly queryApi: string = "api/cassandra";
public static readonly guestQueryApi: string = "api/guest/cassandra";
public static readonly keysApi: string = "api/cassandra/keys";
public static readonly guestKeysApi: string = "api/guest/cassandra/keys";
public static readonly schemaApi: string = "api/cassandra/schema";
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";
public static itemsPerPage: number = 100;
public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions
public static QueryEditorMinHeightRatio: number = 0.1;
public static QueryEditorMaxHeightRatio: number = 0.4;
public static readonly DefaultMaxDegreeOfParallelism = 6;
}
export class SavedQueries {
public static readonly CollectionName: string = "___Query";
public static readonly DatabaseName: string = "___Cosmos";
public static readonly OfferThroughput: number = 400;
public static readonly PartitionKeyProperty: string = "id";
}
export class DocumentsGridMetrics {
public static DocumentsPerPage: number = 100;
public static IndividualRowHeight: number = 34;
public static BufferHeight: number = 28;
public static SplitterMinWidth: number = 200;
public static SplitterMaxWidth: number = 360;
public static DocumentEditorMinWidthRatio: number = 0.2;
public static DocumentEditorMaxWidthRatio: number = 0.4;
}
export class ExplorerMetrics {
public static SplitterMinWidth: number = 240;
public static SplitterMaxWidth: number = 400;
public static CollapsedResourceTreeWidth: number = 36;
}
export class SplitterMetrics {
public static CollapsedPositionLeft: number = ExplorerMetrics.CollapsedResourceTreeWidth;
}
export class Areas {
public static ResourceTree: string = "Resource Tree";
public static ContextualPane: string = "Contextual Pane";
public static Tab: string = "Tab";
public static ShareDialog: string = "Share Access Dialog";
public static Notebook: string = "Notebook";
}
export class HttpHeaders {
public static activityId: string = "x-ms-activity-id";
public static apiType: string = "x-ms-cosmos-apitype";
public static authorization: string = "authorization";
public static collectionIndexTransformationProgress: string =
"x-ms-documentdb-collection-index-transformation-progress";
public static continuation: string = "x-ms-continuation";
public static correlationRequestId: string = "x-ms-correlation-request-id";
public static enableScriptLogging: string = "x-ms-documentdb-script-enable-logging";
public static guestAccessToken: string = "x-ms-encrypted-auth-token";
public static getReadOnlyKey: string = "x-ms-get-read-only-key";
public static connectionString: string = "x-ms-connection-string";
public static msDate: string = "x-ms-date";
public static location: string = "Location";
public static contentType: string = "Content-Type";
public static offerReplacePending: string = "x-ms-offer-replace-pending";
public static user: string = "x-ms-user";
public static populatePartitionStatistics: string = "x-ms-documentdb-populatepartitionstatistics";
public static queryMetrics: string = "x-ms-documentdb-query-metrics";
public static requestCharge: string = "x-ms-request-charge";
public static resourceQuota: string = "x-ms-resource-quota";
public static resourceUsage: string = "x-ms-resource-usage";
public static retryAfterMs: string = "x-ms-retry-after-ms";
public static scriptLogResults: string = "x-ms-documentdb-script-log-results";
public static populateCollectionThroughputInfo = "x-ms-documentdb-populatecollectionthroughputinfo";
public static supportSpatialLegacyCoordinates = "x-ms-documentdb-supportspatiallegacycoordinates";
public static usePolygonsSmallerThanAHemisphere = "x-ms-documentdb-usepolygonssmallerthanahemisphere";
public static autoPilotThroughput = "autoscaleSettings";
public static autoPilotThroughputSDK = "x-ms-cosmos-offer-autopilot-settings";
public static partitionKey: string = "x-ms-documentdb-partitionkey";
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
}
export class ApiType {
// Mapped to hexadecimal values in the backend
public static readonly MongoDB: number = 1;
public static readonly Gremlin: number = 2;
public static readonly Cassandra: number = 4;
public static readonly Table: number = 8;
public static readonly SQL: number = 16;
}
export class HttpStatusCodes {
public static readonly OK: number = 200;
public static readonly Created: number = 201;
public static readonly Accepted: number = 202;
public static readonly NoContent: number = 204;
public static readonly NotModified: number = 304;
public static readonly Unauthorized: number = 401;
public static readonly Forbidden: number = 403;
public static readonly NotFound: number = 404;
public static readonly TooManyRequests: number = 429;
public static readonly Conflict: number = 409;
public static readonly InternalServerError: number = 500;
public static readonly BadGateway: number = 502;
public static readonly ServiceUnavailable: number = 503;
public static readonly GatewayTimeout: number = 504;
public static readonly RetryableStatusCodes: number[] = [
HttpStatusCodes.TooManyRequests,
HttpStatusCodes.InternalServerError, // TODO: Handle all 500s on Portal backend and remove from retries list
HttpStatusCodes.BadGateway,
HttpStatusCodes.ServiceUnavailable,
HttpStatusCodes.GatewayTimeout
];
}
export class Urls {
public static feedbackEmail = "https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Data%20Explorer%20Feedback";
public static autoscaleMigration = "https://aka.ms/cosmos-autoscale-migration";
public static freeTierInformation = "https://aka.ms/cosmos-free-tier";
public static cosmosPricing = "https://aka.ms/azure-cosmos-db-pricing";
}
export class HashRoutePrefixes {
public static databases: string = "/dbs/{db_id}";
public static collections: string = "/dbs/{db_id}/colls/{coll_id}";
public static sprocHash: string = "/sprocs/";
public static sprocs: string = HashRoutePrefixes.collections + HashRoutePrefixes.sprocHash + "{sproc_id}";
public static docs: string = HashRoutePrefixes.collections + "/docs/{doc_id}/";
public static conflicts: string = HashRoutePrefixes.collections + "/conflicts";
public static databasesWithId(databaseId: string): string {
return this.databases.replace("{db_id}", databaseId).replace("/", ""); // strip the first slash since hasher adds it
}
public static collectionsWithIds(databaseId: string, collectionId: string): string {
const transformedDatabasePrefix: string = this.collections.replace("{db_id}", databaseId);
return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it
}
public static sprocWithIds(
databaseId: string,
collectionId: string,
sprocId: string,
stripFirstSlash: boolean = true
): string {
const transformedDatabasePrefix: string = this.sprocs.replace("{db_id}", databaseId);
const transformedSprocRoute: string = transformedDatabasePrefix
.replace("{coll_id}", collectionId)
.replace("{sproc_id}", sprocId);
if (!!stripFirstSlash) {
return transformedSprocRoute.replace("/", ""); // strip the first slash since hasher adds it
}
return transformedSprocRoute;
}
public static conflictsWithIds(databaseId: string, collectionId: string) {
const transformedDatabasePrefix: string = this.conflicts.replace("{db_id}", databaseId);
return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it;
}
public static docsWithIds(databaseId: string, collectionId: string, docId: string) {
const transformedDatabasePrefix: string = this.docs.replace("{db_id}", databaseId);
return transformedDatabasePrefix
.replace("{coll_id}", collectionId)
.replace("{doc_id}", docId)
.replace("/", ""); // strip the first slash since hasher adds it
}
}
export class ConfigurationOverridesValues {
public static IsBsonSchemaV2: string = "true";
}
export class KeyCodes {
public static Space: number = 32;
public static Enter: number = 13;
public static Escape: number = 27;
public static UpArrow: number = 38;
public static DownArrow: number = 40;
public static LeftArrow: number = 37;
public static RightArrow: number = 39;
public static Tab: number = 9;
}
// Normalized per: https://www.w3.org/TR/uievents-key/#named-key-attribute-values
export class NormalizedEventKey {
public static readonly Space = " ";
public static readonly Enter = "Enter";
public static readonly Escape = "Escape";
public static readonly UpArrow = "ArrowUp";
public static readonly DownArrow = "ArrowDown";
public static readonly LeftArrow = "ArrowLeft";
public static readonly RightArrow = "ArrowRight";
}
export class TryCosmosExperience {
public static extendUrl: string = "https://trycosmosdb.azure.com/api/resource/extendportal?userId={0}";
public static deleteUrl: string = "https://trycosmosdb.azure.com/api/resource/deleteportal?userId={0}";
public static collectionsPerAccount: number = 3;
public static maxRU: number = 5000;
public static defaultRU: number = 3000;
}
export class OfferVersions {
public static V1: string = "V1";
public static V2: string = "V2";
}
export enum ConflictOperationType {
Replace = "replace",
Create = "create",
Delete = "delete"
}
export const EmulatorMasterKey =
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
// A variable @MyVariable defined in Constants.less is accessible as StyleConstants.MyVariable
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
export class Notebook {
public static readonly defaultBasePath = "./notebooks";
public static readonly heartbeatDelayMs = 5000;
public static readonly kernelRestartInitialDelayMs = 1000;
public static readonly kernelRestartMaxDelayMs = 20000;
public static readonly autoSaveIntervalMs = 120000;
}
export class SparkLibrary {
public static readonly nameMinLength = 3;
public static readonly nameMaxLength = 63;
}
export class AnalyticalStorageTtl {
public static readonly Days90: number = 7776000;
public static readonly Infinite: number = -1;
public static readonly Disabled: number = 0;
}
export class TerminalQueryParams {
public static readonly Terminal = "terminal";
public static readonly Server = "server";
public static readonly Token = "token";
public static readonly SubscriptionId = "subscriptionId";
public static readonly TerminalEndpoint = "terminalEndpoint";
}
import { HashMap } from "./HashMap";
export class AuthorizationEndpoints {
public static arm: string = "https://management.core.windows.net/";
public static common: string = "https://login.windows.net/";
}
export class CodeOfConductEndpoints {
public static privacyStatement: string = "https://aka.ms/ms-privacy-policy";
public static codeOfConduct: string = "https://aka.ms/cosmos-code-of-conduct";
public static termsOfUse: string = "https://aka.ms/ms-terms-of-use";
}
export class EndpointsRegex {
public static readonly cassandra = [
"AccountEndpoint=(.*).cassandra.cosmosdb.azure.com",
"HostName=(.*).cassandra.cosmos.azure.com"
];
public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com";
public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com";
public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com";
public static readonly table = "TableEndpoint=https://(.*).table.cosmosdb.azure.com";
}
export class ApiEndpoints {
public static runtimeProxy: string = "/api/RuntimeProxy";
public static guestRuntimeProxy: string = "/api/guest/RuntimeProxy";
}
export class ServerIds {
public static localhost: string = "localhost";
public static blackforest: string = "blackforest";
public static fairfax: string = "fairfax";
public static mooncake: string = "mooncake";
public static productionPortal: string = "prod";
public static dev: string = "dev";
}
export class ArmApiVersions {
public static readonly documentDB: string = "2015-11-06";
public static readonly arcadia: string = "2019-06-01-preview";
public static readonly arcadiaLivy: string = "2019-11-01-preview";
public static readonly arm: string = "2015-11-01";
public static readonly armFeatures: string = "2014-08-01-preview";
public static readonly publicVersion = "2020-04-01";
}
export class ArmResourceTypes {
public static readonly notebookWorkspaces = "Microsoft.DocumentDB/databaseAccounts/notebookWorkspaces";
public static readonly synapseWorkspaces = "Microsoft.Synapse/workspaces";
}
export class BackendDefaults {
public static partitionKeyKind: string = "Hash";
public static singlePartitionStorageInGb: string = "10";
public static multiPartitionStorageInGb: string = "100";
public static maxChangeFeedRetentionDuration: number = 10;
public static partitionKeyVersion = 2;
}
export class ClientDefaults {
public static requestTimeoutMs: number = 60000;
public static portalCacheTimeoutMs: number = 10000;
public static errorNotificationTimeoutMs: number = 5000;
public static copyHelperTimeoutMs: number = 2000;
public static waitForDOMElementMs: number = 500;
public static cacheBustingTimeoutMs: number =
10 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/;
public static databaseThroughputIncreaseFactor: number = 100;
public static readonly arcadiaTokenRefreshInterval: number =
20 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/;
public static readonly arcadiaTokenRefreshIntervalPaddingMs: number = 2000;
}
export class AccountKind {
public static DocumentDB: string = "DocumentDB";
public static MongoDB: string = "MongoDB";
public static Parse: string = "Parse";
public static GlobalDocumentDB: string = "GlobalDocumentDB";
public static Default: string = AccountKind.DocumentDB;
}
export class CorrelationBackend {
public static Url: string = "https://aka.ms/cosmosdbanalytics";
}
export class DefaultAccountExperience {
public static DocumentDB: string = "DocumentDB";
public static Graph: string = "Graph";
public static MongoDB: string = "MongoDB";
public static ApiForMongoDB: string = "Azure Cosmos DB for MongoDB API";
public static Table: string = "Table";
public static Cassandra: string = "Cassandra";
public static Default: string = DefaultAccountExperience.DocumentDB;
}
export class CapabilityNames {
public static EnableTable: string = "EnableTable";
public static EnableGremlin: string = "EnableGremlin";
public static EnableCassandra: string = "EnableCassandra";
public static EnableAutoScale: string = "EnableAutoScale";
public static readonly EnableNotebooks: string = "EnableNotebooks";
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
public static readonly EnableMongo: string = "EnableMongo";
public static readonly EnableServerless: string = "EnableServerless";
}
export class Features {
public static readonly cosmosdb = "cosmosdb";
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
public static readonly executeSproc = "dataexplorerexecutesproc";
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
public static readonly enableTtl = "enablettl";
public static readonly enableNotebooks = "enablenotebooks";
public static readonly enableGalleryPublish = "enablegallerypublish";
public static readonly enableLinkInjection = "enablelinkinjection";
public static readonly enableSpark = "enablespark";
public static readonly livyEndpoint = "livyendpoint";
public static readonly notebookServerUrl = "notebookserverurl";
public static readonly notebookServerToken = "notebookservertoken";
public static readonly notebookBasePath = "notebookbasepath";
public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
public static readonly ttl90Days = "ttl90days";
public static readonly enableRightPanelV2 = "enablerightpanelv2";
public static readonly enableSchema = "enableschema";
public static readonly enableSDKoperations = "enablesdkoperations";
public static readonly showMinRUSurvey = "showminrusurvey";
public static readonly selfServeType = "selfservetype";
}
// flight names returned from the portal are always lowercase
export class Flights {
public static readonly SettingsV2 = "settingsv2";
public static readonly MongoIndexEditor = "mongoindexeditor";
public static readonly AutoscaleTest = "autoscaletest";
public static readonly MongoIndexing = "mongoindexing";
}
export class AfecFeatures {
public static readonly Spark = "spark-public-preview";
public static readonly Notebooks = "sparknotebooks-public-preview";
public static readonly StorageAnalytics = "storageanalytics-public-preview";
}
export class Spark {
public static readonly MaxWorkerCount = 10;
public static readonly SKUs: HashMap<string> = new HashMap({
"Cosmos.Spark.D1s": "D1s / 1 core / 4GB RAM",
"Cosmos.Spark.D2s": "D2s / 2 cores / 8GB RAM",
"Cosmos.Spark.D4s": "D4s / 4 cores / 16GB RAM",
"Cosmos.Spark.D8s": "D8s / 8 cores / 32GB RAM",
"Cosmos.Spark.D16s": "D16s / 16 cores / 64GB RAM",
"Cosmos.Spark.D32s": "D32s / 32 cores / 128GB RAM",
"Cosmos.Spark.D64s": "D64s / 64 cores / 256GB RAM"
});
}
export class TagNames {
public static defaultExperience: string = "defaultExperience";
}
export class MongoDBAccounts {
public static protocol: string = "https";
public static defaultPort: string = "10255";
}
export enum MongoBackendEndpointType {
local,
remote
}
// TODO: 435619 Add default endpoints per cloud and use regional only when available
export class CassandraBackend {
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
public static readonly queryApi: string = "api/cassandra";
public static readonly guestQueryApi: string = "api/guest/cassandra";
public static readonly keysApi: string = "api/cassandra/keys";
public static readonly guestKeysApi: string = "api/guest/cassandra/keys";
public static readonly schemaApi: string = "api/cassandra/schema";
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
}
export class Queries {
public static CustomPageOption: string = "custom";
public static UnlimitedPageOption: string = "unlimited";
public static itemsPerPage: number = 100;
public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions
public static QueryEditorMinHeightRatio: number = 0.1;
public static QueryEditorMaxHeightRatio: number = 0.4;
public static readonly DefaultMaxDegreeOfParallelism = 6;
}
export class SavedQueries {
public static readonly CollectionName: string = "___Query";
public static readonly DatabaseName: string = "___Cosmos";
public static readonly OfferThroughput: number = 400;
public static readonly PartitionKeyProperty: string = "id";
}
export class DocumentsGridMetrics {
public static DocumentsPerPage: number = 100;
public static IndividualRowHeight: number = 34;
public static BufferHeight: number = 28;
public static SplitterMinWidth: number = 200;
public static SplitterMaxWidth: number = 360;
public static DocumentEditorMinWidthRatio: number = 0.2;
public static DocumentEditorMaxWidthRatio: number = 0.4;
}
export class ExplorerMetrics {
public static SplitterMinWidth: number = 240;
public static SplitterMaxWidth: number = 400;
public static CollapsedResourceTreeWidth: number = 36;
}
export class SplitterMetrics {
public static CollapsedPositionLeft: number = ExplorerMetrics.CollapsedResourceTreeWidth;
}
export class Areas {
public static ResourceTree: string = "Resource Tree";
public static ContextualPane: string = "Contextual Pane";
public static Tab: string = "Tab";
public static ShareDialog: string = "Share Access Dialog";
public static Notebook: string = "Notebook";
}
export class HttpHeaders {
public static activityId: string = "x-ms-activity-id";
public static apiType: string = "x-ms-cosmos-apitype";
public static authorization: string = "authorization";
public static collectionIndexTransformationProgress: string =
"x-ms-documentdb-collection-index-transformation-progress";
public static continuation: string = "x-ms-continuation";
public static correlationRequestId: string = "x-ms-correlation-request-id";
public static enableScriptLogging: string = "x-ms-documentdb-script-enable-logging";
public static guestAccessToken: string = "x-ms-encrypted-auth-token";
public static getReadOnlyKey: string = "x-ms-get-read-only-key";
public static connectionString: string = "x-ms-connection-string";
public static msDate: string = "x-ms-date";
public static location: string = "Location";
public static contentType: string = "Content-Type";
public static offerReplacePending: string = "x-ms-offer-replace-pending";
public static user: string = "x-ms-user";
public static populatePartitionStatistics: string = "x-ms-documentdb-populatepartitionstatistics";
public static queryMetrics: string = "x-ms-documentdb-query-metrics";
public static requestCharge: string = "x-ms-request-charge";
public static resourceQuota: string = "x-ms-resource-quota";
public static resourceUsage: string = "x-ms-resource-usage";
public static retryAfterMs: string = "x-ms-retry-after-ms";
public static scriptLogResults: string = "x-ms-documentdb-script-log-results";
public static populateCollectionThroughputInfo = "x-ms-documentdb-populatecollectionthroughputinfo";
public static supportSpatialLegacyCoordinates = "x-ms-documentdb-supportspatiallegacycoordinates";
public static usePolygonsSmallerThanAHemisphere = "x-ms-documentdb-usepolygonssmallerthanahemisphere";
public static autoPilotThroughput = "autoscaleSettings";
public static autoPilotThroughputSDK = "x-ms-cosmos-offer-autopilot-settings";
public static partitionKey: string = "x-ms-documentdb-partitionkey";
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
}
export class ApiType {
// Mapped to hexadecimal values in the backend
public static readonly MongoDB: number = 1;
public static readonly Gremlin: number = 2;
public static readonly Cassandra: number = 4;
public static readonly Table: number = 8;
public static readonly SQL: number = 16;
}
export class HttpStatusCodes {
public static readonly OK: number = 200;
public static readonly Created: number = 201;
public static readonly Accepted: number = 202;
public static readonly NoContent: number = 204;
public static readonly NotModified: number = 304;
public static readonly Unauthorized: number = 401;
public static readonly Forbidden: number = 403;
public static readonly NotFound: number = 404;
public static readonly TooManyRequests: number = 429;
public static readonly Conflict: number = 409;
public static readonly InternalServerError: number = 500;
public static readonly BadGateway: number = 502;
public static readonly ServiceUnavailable: number = 503;
public static readonly GatewayTimeout: number = 504;
public static readonly RetryableStatusCodes: number[] = [
HttpStatusCodes.TooManyRequests,
HttpStatusCodes.InternalServerError, // TODO: Handle all 500s on Portal backend and remove from retries list
HttpStatusCodes.BadGateway,
HttpStatusCodes.ServiceUnavailable,
HttpStatusCodes.GatewayTimeout
];
}
export class Urls {
public static feedbackEmail = "https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Data%20Explorer%20Feedback";
public static autoscaleMigration = "https://aka.ms/cosmos-autoscale-migration";
public static freeTierInformation = "https://aka.ms/cosmos-free-tier";
public static cosmosPricing = "https://aka.ms/azure-cosmos-db-pricing";
}
export class HashRoutePrefixes {
public static databases: string = "/dbs/{db_id}";
public static collections: string = "/dbs/{db_id}/colls/{coll_id}";
public static sprocHash: string = "/sprocs/";
public static sprocs: string = HashRoutePrefixes.collections + HashRoutePrefixes.sprocHash + "{sproc_id}";
public static docs: string = HashRoutePrefixes.collections + "/docs/{doc_id}/";
public static conflicts: string = HashRoutePrefixes.collections + "/conflicts";
public static databasesWithId(databaseId: string): string {
return this.databases.replace("{db_id}", databaseId).replace("/", ""); // strip the first slash since hasher adds it
}
public static collectionsWithIds(databaseId: string, collectionId: string): string {
const transformedDatabasePrefix: string = this.collections.replace("{db_id}", databaseId);
return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it
}
public static sprocWithIds(
databaseId: string,
collectionId: string,
sprocId: string,
stripFirstSlash: boolean = true
): string {
const transformedDatabasePrefix: string = this.sprocs.replace("{db_id}", databaseId);
const transformedSprocRoute: string = transformedDatabasePrefix
.replace("{coll_id}", collectionId)
.replace("{sproc_id}", sprocId);
if (!!stripFirstSlash) {
return transformedSprocRoute.replace("/", ""); // strip the first slash since hasher adds it
}
return transformedSprocRoute;
}
public static conflictsWithIds(databaseId: string, collectionId: string) {
const transformedDatabasePrefix: string = this.conflicts.replace("{db_id}", databaseId);
return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it;
}
public static docsWithIds(databaseId: string, collectionId: string, docId: string) {
const transformedDatabasePrefix: string = this.docs.replace("{db_id}", databaseId);
return transformedDatabasePrefix
.replace("{coll_id}", collectionId)
.replace("{doc_id}", docId)
.replace("/", ""); // strip the first slash since hasher adds it
}
}
export class ConfigurationOverridesValues {
public static IsBsonSchemaV2: string = "true";
}
export class KeyCodes {
public static Space: number = 32;
public static Enter: number = 13;
public static Escape: number = 27;
public static UpArrow: number = 38;
public static DownArrow: number = 40;
public static LeftArrow: number = 37;
public static RightArrow: number = 39;
public static Tab: number = 9;
}
// Normalized per: https://www.w3.org/TR/uievents-key/#named-key-attribute-values
export class NormalizedEventKey {
public static readonly Space = " ";
public static readonly Enter = "Enter";
public static readonly Escape = "Escape";
public static readonly UpArrow = "ArrowUp";
public static readonly DownArrow = "ArrowDown";
public static readonly LeftArrow = "ArrowLeft";
public static readonly RightArrow = "ArrowRight";
}
export class TryCosmosExperience {
public static extendUrl: string = "https://trycosmosdb.azure.com/api/resource/extendportal?userId={0}";
public static deleteUrl: string = "https://trycosmosdb.azure.com/api/resource/deleteportal?userId={0}";
public static collectionsPerAccount: number = 3;
public static maxRU: number = 5000;
public static defaultRU: number = 3000;
}
export class OfferVersions {
public static V1: string = "V1";
public static V2: string = "V2";
}
export enum ConflictOperationType {
Replace = "replace",
Create = "create",
Delete = "delete"
}
export const EmulatorMasterKey =
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
// A variable @MyVariable defined in Constants.less is accessible as StyleConstants.MyVariable
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
export class Notebook {
public static readonly defaultBasePath = "./notebooks";
public static readonly heartbeatDelayMs = 5000;
public static readonly kernelRestartInitialDelayMs = 1000;
public static readonly kernelRestartMaxDelayMs = 20000;
public static readonly autoSaveIntervalMs = 120000;
}
export class SparkLibrary {
public static readonly nameMinLength = 3;
public static readonly nameMaxLength = 63;
}
export class AnalyticalStorageTtl {
public static readonly Days90: number = 7776000;
public static readonly Infinite: number = -1;
public static readonly Disabled: number = 0;
}
export class TerminalQueryParams {
public static readonly Terminal = "terminal";
public static readonly Server = "server";
public static readonly Token = "token";
public static readonly SubscriptionId = "subscriptionId";
public static readonly TerminalEndpoint = "terminalEndpoint";
}

View File

@@ -1,182 +0,0 @@
import {
ConflictDefinition,
FeedOptions,
ItemDefinition,
OfferDefinition,
QueryIterator,
Resource
} from "@azure/cosmos";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import Q from "q";
import { configContext, Platform } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts";
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 { OfferUtils } from "../Utils/OfferUtils";
import * as Constants from "./Constants";
import { client } from "./CosmosClient";
import * as HeadersUtility from "./HeadersUtility";
import { sendCachedDataMessage } from "./MessageHandler";
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<QueryIterator<ItemDefinition & Resource>> {
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<any> {
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<any> {
// TODO remove this deferred. Kept it because of timeout code at bottom of function
const deferred = Q.defer<any>();
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<any> {
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<any> {
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<any> {
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<any> {
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<QueryIterator<ConflictDefinition & Resource>> {
const documentsIterator = client()
.database(databaseId)
.container(containerId)
.conflicts.query(query, options);
return Q(documentsIterator);
}

View File

@@ -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<QueryIterator<ItemDefinition & Resource>> {
return DataAccessUtilityBase.queryDocuments(databaseId, containerId, query, options);
}
export function queryConflicts(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
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<any> {
var deferred = Q.defer<any>();
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<ViewModels.QueryResults> {
var deferred = Q.defer<ViewModels.QueryResults>();
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<any> {
var deferred = Q.defer<any>();
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<any> {
var deferred = Q.defer<any>();
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<any> {
var deferred = Q.defer<any>();
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<any> {
var deferred = Q.defer<any>();
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<any> {
var deferred = Q.defer<any>();
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;
}

View File

@@ -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";
};

View File

@@ -1,8 +1,6 @@
export default class EnvironmentUtility {
public static normalizeArmEndpointUri(uri: string): string {
if (uri && uri.slice(-1) !== "/") {
return `${uri}/`;
}
return uri;
export function normalizeArmEndpoint(uri: string): string {
if (uri && uri.slice(-1) !== "/") {
return `${uri}/`;
}
return uri;
}

View File

@@ -21,7 +21,7 @@ export const handleError = (error: string | ARMError | Error, area: string, cons
sendNotificationForError(errorMessage, errorCode);
};
export const getErrorMessage = (error: string | Error): string => {
export const getErrorMessage = (error: string | Error = ""): string => {
const errorMessage = typeof error === "string" ? error : error.message;
return replaceKnownError(errorMessage);
};
@@ -45,10 +45,10 @@ const sendNotificationForError = (errorMessage: string, errorCode: number | stri
const replaceKnownError = (errorMessage: string): string => {
if (
window.dataExplorer?.subscriptionType() === SubscriptionType.Internal &&
errorMessage.indexOf("SharedOffer is Disabled for your account") >= 0
errorMessage?.indexOf("SharedOffer is Disabled for your account") >= 0
) {
return "Database throughput is not supported for internal subscriptions.";
} else if (errorMessage.indexOf("Partition key paths must contain only valid") >= 0) {
} else if (errorMessage?.indexOf("Partition key paths must contain only valid") >= 0) {
return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
}

View File

@@ -0,0 +1,64 @@
import * as OfferUtility from "./OfferUtility";
import { SDKOfferDefinition, Offer } from "../Contracts/DataModels";
import { OfferResponse } from "@azure/cosmos";
describe("parseSDKOfferResponse", () => {
it("manual throughput", () => {
const mockOfferDefinition = {
content: {
offerThroughput: 500,
collectionThroughputInfo: {
minimumRUForCollection: 400,
numPhysicalPartitions: 1
}
},
id: "test"
} as SDKOfferDefinition;
const mockResponse = {
resource: mockOfferDefinition
} as OfferResponse;
const expectedResult: Offer = {
manualThroughput: 500,
autoscaleMaxThroughput: undefined,
minimumThroughput: 400,
id: "test",
offerDefinition: mockOfferDefinition,
offerReplacePending: false
};
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
});
it("autoscale throughput", () => {
const mockOfferDefinition = {
content: {
offerThroughput: 400,
collectionThroughputInfo: {
minimumRUForCollection: 400,
numPhysicalPartitions: 1
},
offerAutopilotSettings: {
maxThroughput: 5000
}
},
id: "test"
} as SDKOfferDefinition;
const mockResponse = {
resource: mockOfferDefinition
} as OfferResponse;
const expectedResult: Offer = {
manualThroughput: undefined,
autoscaleMaxThroughput: 5000,
minimumThroughput: 400,
id: "test",
offerDefinition: mockOfferDefinition,
offerReplacePending: false
};
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
});
});

View File

@@ -0,0 +1,37 @@
import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
import { OfferResponse } from "@azure/cosmos";
import { HttpHeaders } from "./Constants";
export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer | undefined => {
const offerDefinition: SDKOfferDefinition | undefined = offerResponse?.resource;
if (!offerDefinition) {
return undefined;
}
const offerContent = offerDefinition.content;
if (!offerContent) {
return undefined;
}
const minimumThroughput = offerContent.collectionThroughputInfo?.minimumRUForCollection;
const autopilotSettings = offerContent.offerAutopilotSettings;
if (autopilotSettings && autopilotSettings.maxThroughput && minimumThroughput) {
return {
id: offerDefinition.id,
autoscaleMaxThroughput: autopilotSettings.maxThroughput,
manualThroughput: undefined,
minimumThroughput,
offerDefinition,
offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true"
};
}
return {
id: offerDefinition.id,
autoscaleMaxThroughput: undefined,
manualThroughput: offerContent.offerThroughput,
minimumThroughput,
offerDefinition,
offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true"
};
};

View File

@@ -16,7 +16,7 @@ const notificationsPath = () => {
};
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
if (configContext.platform === Platform.Emulator) {
if (configContext.platform === Platform.Emulator || configContext.platform === Platform.Hosted) {
return [];
}

View File

@@ -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<void> {
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<DataModels.Query[]> {
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<ItemDefinition & Resource> = queryDocuments(
SavedQueries.DatabaseName,
SavedQueries.CollectionName,
this.fetchQueriesQuery(),
options
);
const fetchQueries = async (firstItemIndex: number): Promise<ViewModels.QueryResults> =>
await queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex);
return QueryUtils.queryAllPages(fetchQueries)
.then(
(queryIterator: QueryIterator<ItemDefinition & Resource>) => {
const fetchQueries = (firstItemIndex: number): Q.Promise<ViewModels.QueryResults> =>
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<void> {
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 {

View File

@@ -23,10 +23,10 @@ export class Splitter {
public splitterId: string;
public leftSideId: string;
public splitter: HTMLElement;
public leftSide: HTMLElement;
public lastX: number;
public lastWidth: number;
public splitter!: HTMLElement;
public leftSide!: HTMLElement;
public lastX!: number;
public lastWidth!: number;
private isCollapsed: ko.Observable<boolean>;
private bounds: SplitterBounds;
@@ -42,9 +42,10 @@ export class Splitter {
}
public initialize() {
this.splitter = document.getElementById(this.splitterId);
this.leftSide = document.getElementById(this.leftSideId);
if (document.getElementById(this.splitterId) !== null && document.getElementById(this.leftSideId) != null) {
this.splitter = <HTMLElement>document.getElementById(this.splitterId);
this.leftSide = <HTMLElement>document.getElementById(this.leftSideId);
}
const isVerticalSplitter: boolean = this.direction === SplitterDirection.Vertical;
const splitterOptions: JQueryUI.ResizableOptions = {
animate: true,

View File

@@ -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";

View File

@@ -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<unknown> => {
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();
}
};

View File

@@ -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<void> => {
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];
};

View File

@@ -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<void> => {
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();
}
};

View File

@@ -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<ExecuteSprocResult> => {
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();
}
};

View File

@@ -0,0 +1,92 @@
import { AuthType } from "../../AuthType";
import { armRequest } from "../../Utils/arm/request";
import { configContext } from "../../ConfigContext";
import { handleError } from "../ErrorHandlingUtils";
import { userContext } from "../../UserContext";
interface TimeSeriesData {
data: {
timeStamp: string;
total: number;
}[];
metadatavalues: {
name: {
localizedValue: string;
value: string;
};
value: string;
};
}
interface MetricsData {
displayDescription: string;
errorCode: string;
id: string;
name: {
value: string;
localizedValue: string;
};
timeseries: TimeSeriesData[];
type: string;
unit: string;
}
interface MetricsResponse {
cost: number;
interval: string;
namespace: string;
resourceregion: string;
timespan: string;
value: MetricsData[];
}
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
if (window.authType !== AuthType.AAD) {
return undefined;
}
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const filter = `DatabaseName eq '${databaseName}' and CollectionName eq '${containerName}'`;
const metricNames = "DataUsage,IndexUsage";
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/providers/microsoft.insights/metrics`;
try {
const metricsResponse: MetricsResponse = await armRequest({
host: configContext.ARM_ENDPOINT,
path,
method: "GET",
apiVersion: "2018-01-01",
queryParams: {
filter,
metricNames
}
});
if (metricsResponse?.value?.length !== 2) {
return undefined;
}
const dataUsageData: MetricsData = metricsResponse.value[0];
const indexUsagedata: MetricsData = metricsResponse.value[1];
const dataUsageSizeInKb: number = getUsageSizeInKb(dataUsageData);
const indexUsageSizeInKb: number = getUsageSizeInKb(indexUsagedata);
return dataUsageSizeInKb + indexUsageSizeInKb;
} catch (error) {
handleError(error, "getCollectionUsageSize");
throw error;
}
};
const getUsageSizeInKb = (metricsData: MetricsData): number => {
if (metricsData?.errorCode !== "Success") {
throw Error(`Get collection usage size failed: ${metricsData.errorCode}`);
}
const timeSeriesData: TimeSeriesData = metricsData?.timeseries?.[0];
const usageSizeInBytes: number = timeSeriesData?.data?.[0]?.total;
return usageSizeInBytes ? usageSizeInBytes / 1024 : 0;
};

View File

@@ -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<ConflictDefinition & Resource> => {
return client()
.database(databaseId)
.container(containerId)
.conflicts.query(query, options);
};

View File

@@ -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();
});
});

View File

@@ -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<ItemDefinition & Resource> => {
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;
};

View File

@@ -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<QueryResults> => {
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();
}
};

View File

@@ -1,9 +1,6 @@
import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { HttpHeaders } from "../Constants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
import { handleError } from "../ErrorHandlingUtils";
import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
@@ -11,50 +8,22 @@ import { getCassandraTableThroughput } from "../../Utils/arm/generatedClients/20
import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { readOffers } from "./readOffers";
import { readOfferWithSDK } from "./readOfferWithSDK";
import { userContext } from "../../UserContext";
export const readCollectionOffer = async (
params: DataModels.ReadCollectionOfferParams
): Promise<DataModels.OfferWithHeaders> => {
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
let offerId = params.offerId;
if (!offerId) {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
try {
offerId = await getCollectionOfferIdWithARM(params.databaseId, params.collectionId);
} catch (error) {
clearMessage();
if (error.code !== "NotFound") {
throw error;
}
return undefined;
}
} else {
offerId = await getCollectionOfferIdWithSDK(params.collectionResourceId);
if (!offerId) {
clearMessage();
return undefined;
}
}
}
const options: RequestOptions = {
initialHeaders: {
[HttpHeaders.populateCollectionThroughputInfo]: true
}
};
try {
const response = await client()
.offer(offerId)
.read(options);
return (
response && {
...response.resource,
headers: response.headers
}
);
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
return await readCollectionOfferWithARM(params.databaseId, params.collectionId);
}
return await readOfferWithSDK(params.offerId, params.collectionResourceId);
} catch (error) {
handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`);
throw error;
@@ -63,61 +32,92 @@ export const readCollectionOffer = async (
}
};
const getCollectionOfferIdWithARM = async (databaseId: string, collectionId: string): Promise<string> => {
let rpResponse;
const readCollectionOfferWithARM = async (databaseId: string, collectionId: string): Promise<Offer> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
rpResponse = await getSqlContainerThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.MongoDB:
rpResponse = await getMongoDBCollectionThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.Cassandra:
rpResponse = await getCassandraTableThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.Graph:
rpResponse = await getGremlinGraphThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.Table:
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
break;
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
let rpResponse;
try {
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
rpResponse = await getSqlContainerThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.MongoDB:
rpResponse = await getMongoDBCollectionThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.Cassandra:
rpResponse = await getCassandraTableThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.Graph:
rpResponse = await getGremlinGraphThroughput(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId
);
break;
case DefaultAccountExperienceType.Table:
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
break;
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
return undefined;
}
return rpResponse?.name;
};
const resource = rpResponse?.properties?.resource;
if (resource) {
const offerId: string = rpResponse.name;
const minimumThroughput: number =
typeof resource.minimumThroughput === "string"
? parseInt(resource.minimumThroughput)
: resource.minimumThroughput;
const autoscaleSettings = resource.autoscaleSettings;
const getCollectionOfferIdWithSDK = async (collectionResourceId: string): Promise<string> => {
const offers = await readOffers();
const offer = offers.find(offer => offer.resource === collectionResourceId);
return offer?.id;
if (autoscaleSettings) {
return {
id: offerId,
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
manualThroughput: undefined,
minimumThroughput,
offerReplacePending: resource.offerReplacePending === "true"
};
}
return {
id: offerId,
autoscaleMaxThroughput: undefined,
manualThroughput: resource.throughput,
minimumThroughput,
offerReplacePending: resource.offerReplacePending === "true"
};
}
return undefined;
};

View File

@@ -1,45 +0,0 @@
import * as DataModels from "../../Contracts/DataModels";
import * as HeadersUtility from "../HeadersUtility";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContainerDefinition, Resource } from "@azure/cosmos";
import { HttpHeaders } from "../Constants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
interface ResourceWithStatistics {
statistics: DataModels.Statistic[];
}
export const readCollectionQuotaInfo = async (
collection: ViewModels.Collection
): Promise<DataModels.CollectionQuotaInfo> => {
const clearMessage = logConsoleProgress(`Querying containers for database ${collection.id}`);
const options: RequestOptions = {};
options.populateQuotaInfo = true;
options.initialHeaders = options.initialHeaders || {};
options.initialHeaders[HttpHeaders.populatePartitionStatistics] = true;
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.read(options);
const quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers);
const resource = response.resource as ContainerDefinition & Resource & ResourceWithStatistics;
quota["usageSizeInKB"] = resource.statistics.reduce(
(previousValue: number, currentValue: DataModels.Statistic) => previousValue + currentValue.sizeInKB,
0
);
quota["numPartitions"] = resource.statistics.length;
quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617)
return quota;
} catch (error) {
handleError(error, "ReadCollectionQuotaInfo", `Error while querying quota info for container ${collection.id}`);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -1,51 +1,28 @@
import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { HttpHeaders } from "../Constants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { readOffers } from "./readOffers";
import { readOfferWithSDK } from "./readOfferWithSDK";
import { userContext } from "../../UserContext";
export const readDatabaseOffer = async (
params: DataModels.ReadDatabaseOfferParams
): Promise<DataModels.OfferWithHeaders> => {
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
let offerId = params.offerId;
if (!offerId) {
offerId = await (window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
? getDatabaseOfferIdWithARM(params.databaseId)
: getDatabaseOfferIdWithSDK(params.databaseResourceId));
if (!offerId) {
clearMessage();
return undefined;
}
}
const options: RequestOptions = {
initialHeaders: {
[HttpHeaders.populateCollectionThroughputInfo]: true
}
};
try {
const response = await client()
.offer(offerId)
.read(options);
return (
response && {
...response.resource,
headers: response.headers
}
);
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
return await readDatabaseOfferWithARM(params.databaseId);
}
return await readOfferWithSDK(params.offerId, params.databaseResourceId);
} catch (error) {
handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`);
throw error;
@@ -54,13 +31,13 @@ export const readDatabaseOffer = async (
}
};
const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> => {
let rpResponse;
const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
let rpResponse;
try {
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
@@ -78,18 +55,41 @@ const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> =>
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
return rpResponse?.name;
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
return undefined;
}
};
const getDatabaseOfferIdWithSDK = async (databaseResourceId: string): Promise<string> => {
const offers = await readOffers();
const offer = offers.find(offer => offer.resource === databaseResourceId);
return offer?.id;
const resource = rpResponse?.properties?.resource;
if (resource) {
const offerId: string = rpResponse.name;
const minimumThroughput: number =
typeof resource.minimumThroughput === "string"
? parseInt(resource.minimumThroughput)
: resource.minimumThroughput;
const autoscaleSettings = resource.autoscaleSettings;
if (autoscaleSettings) {
return {
id: offerId,
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
manualThroughput: undefined,
minimumThroughput,
offerReplacePending: resource.offerReplacePending === "true"
};
}
return {
id: offerId,
autoscaleMaxThroughput: undefined,
manualThroughput: resource.throughput,
minimumThroughput,
offerReplacePending: resource.offerReplacePending === "true"
};
}
return undefined;
};

View File

@@ -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<Item> => {
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();
}
};

View File

@@ -0,0 +1,29 @@
import { HttpHeaders } from "../Constants";
import { Offer } from "../../Contracts/DataModels";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { parseSDKOfferResponse } from "../OfferUtility";
import { readOffers } from "./readOffers";
export const readOfferWithSDK = async (offerId: string, resourceId: string): Promise<Offer> => {
if (!offerId) {
const offers = await readOffers();
const offer = offers.find(offer => offer.resource === resourceId);
if (!offer) {
return undefined;
}
offerId = offer.id;
}
const options: RequestOptions = {
initialHeaders: {
[HttpHeaders.populateCollectionThroughputInfo]: true
}
};
const response = await client()
.offer(offerId)
.read(options);
return parseSDKOfferResponse(response);
};

View File

@@ -1,9 +1,9 @@
import { Offer } from "../../Contracts/DataModels";
import { SDKOfferDefinition } from "../../Contracts/DataModels";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError, getErrorMessage } from "../ErrorHandlingUtils";
export const readOffers = async (): Promise<Offer[]> => {
export const readOffers = async (): Promise<SDKOfferDefinition[]> => {
const clearMessage = logConsoleProgress(`Querying offers`);
try {

View File

@@ -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<Item> => {
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();
}
};

View File

@@ -1,13 +1,14 @@
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { HttpHeaders } from "../Constants";
import { Offer, UpdateOfferParams } from "../../Contracts/DataModels";
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
import { OfferDefinition } from "@azure/cosmos";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { parseSDKOfferResponse } from "../OfferUtility";
import { readCollectionOffer } from "./readCollectionOffer";
import { readDatabaseOffer } from "./readDatabaseOffer";
import {
@@ -373,21 +374,21 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
};
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
const currentOffer = params.currentOffer;
const newOffer: Offer = {
const sdkOfferDefinition = params.currentOffer.offerDefinition;
const newOffer: SDKOfferDefinition = {
content: {
offerThroughput: undefined,
offerIsRUPerMinuteThroughputEnabled: false
},
_etag: undefined,
_ts: undefined,
_rid: currentOffer._rid,
_self: currentOffer._self,
id: currentOffer.id,
offerResourceId: currentOffer.offerResourceId,
offerVersion: currentOffer.offerVersion,
offerType: currentOffer.offerType,
resource: currentOffer.resource
_rid: sdkOfferDefinition._rid,
_self: sdkOfferDefinition._self,
id: sdkOfferDefinition.id,
offerResourceId: sdkOfferDefinition.offerResourceId,
offerVersion: sdkOfferDefinition.offerVersion,
offerType: sdkOfferDefinition.offerType,
resource: sdkOfferDefinition.resource
};
if (params.autopilotThroughput) {
@@ -415,5 +416,6 @@ const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> =>
.offer(params.currentOffer.id)
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
.replace((newOffer as unknown) as OfferDefinition, options);
return sdkResponse?.resource;
return parseSDKOfferResponse(sdkResponse);
};

View File

@@ -1,25 +0,0 @@
import { updateOfferThroughputBeyondLimit } from "./updateOfferThroughputBeyondLimit";
describe("updateOfferThroughputBeyondLimit", () => {
it("should call fetch", async () => {
window.fetch = jest.fn(() => {
return {
ok: true
};
});
window.dataExplorer = {
logConsoleData: jest.fn(),
deleteInProgressConsoleDataWithId: jest.fn()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
await updateOfferThroughputBeyondLimit({
subscriptionId: "foo",
resourceGroup: "foo",
databaseAccountName: "foo",
databaseName: "foo",
throughput: 1000000000,
offerIsRUPerMinuteThroughputEnabled: false
});
expect(window.fetch).toHaveBeenCalled();
});
});

View File

@@ -1,57 +0,0 @@
import { Platform, configContext } from "../../ConfigContext";
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import { AutoPilotOfferSettings } from "../../Contracts/DataModels";
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { HttpHeaders } from "../Constants";
import { handleError } from "../ErrorHandlingUtils";
interface UpdateOfferThroughputRequest {
subscriptionId: string;
resourceGroup: string;
databaseAccountName: string;
databaseName: string;
collectionName?: string;
throughput: number;
offerIsRUPerMinuteThroughputEnabled: boolean;
offerAutopilotSettings?: AutoPilotOfferSettings;
}
export async function updateOfferThroughputBeyondLimit(request: UpdateOfferThroughputRequest): Promise<void> {
if (configContext.platform !== Platform.Portal) {
throw new Error("Updating throughput beyond specified limit is not supported on this platform");
}
const resourceDescriptionInfo = request.collectionName
? `database ${request.databaseName} and container ${request.collectionName}`
: `database ${request.databaseName}`;
const clearMessage = logConsoleProgress(
`Requesting increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
);
const url = `${configContext.BACKEND_ENDPOINT}/api/offerthroughputrequest/updatebeyondspecifiedlimit`;
const authorizationHeader = getAuthorizationHeader();
const response = await fetch(url, {
method: "POST",
body: JSON.stringify(request),
headers: { [authorizationHeader.header]: authorizationHeader.token, [HttpHeaders.contentType]: "application/json" }
});
if (response.ok) {
logConsoleInfo(
`Successfully requested an increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
);
clearMessage();
return undefined;
}
const error = await response.json();
handleError(
error,
"updateOfferThroughputBeyondLimit",
`Failed to request an increase in throughput for ${request.throughput}`
);
clearMessage();
throw error;
}

View File

@@ -208,12 +208,21 @@ export interface QueryMetrics {
vmExecutionTime: any;
}
export interface Offer extends Resource {
export interface Offer {
id: string;
autoscaleMaxThroughput: number | undefined;
manualThroughput: number | undefined;
minimumThroughput: number | undefined;
offerDefinition?: SDKOfferDefinition;
offerReplacePending: boolean;
}
export interface SDKOfferDefinition extends Resource {
offerVersion?: string;
offerType?: string;
content?: {
offerThroughput: number;
offerIsRUPerMinuteThroughputEnabled: boolean;
offerIsRUPerMinuteThroughputEnabled?: boolean;
collectionThroughputInfo?: OfferThroughputInfo;
offerAutopilotSettings?: AutoPilotOfferSettings;
};
@@ -221,22 +230,6 @@ export interface Offer extends Resource {
offerResourceId?: string;
}
export interface OfferWithHeaders extends Offer {
headers: any;
}
export interface CollectionQuotaInfo {
storedProcedures: number;
triggers: number;
functions: number;
documentsSize: number;
collectionSize: number;
documentsCount: number;
usageSizeInKB: number;
numPartitions: number;
uniqueKeyPolicy?: UniqueKeyPolicy; // TODO: This should ideally not be a part of the collection quota. Remove after refactoring. (#119617)
}
export interface OfferThroughputInfo {
minimumRUForCollection: number;
numPhysicalPartitions: number;
@@ -255,7 +248,6 @@ export interface CreateDatabaseAndCollectionRequest {
collectionId: string;
offerThroughput: number;
databaseLevelThroughput: boolean;
rupmEnabled?: boolean;
partitionKey?: PartitionKey;
indexingPolicy?: IndexingPolicy;
uniqueKeyPolicy?: UniqueKeyPolicy;

View File

@@ -32,7 +32,8 @@ export enum MessageTypes {
GetArcadiaToken,
CreateWorkspace,
CreateSparkPool,
RefreshDatabaseAccount
RefreshDatabaseAccount,
InitTestExplorer
}
export { Versions, ActionContracts, Diagnostics };

View File

@@ -15,6 +15,7 @@ import DocumentId from "../Explorer/Tree/DocumentId";
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import Trigger from "../Explorer/Tree/Trigger";
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
import { SelfServeType } from "../SelfServe/SelfServeUtils";
import { UploadDetails } from "../workers/upload/definitions";
import * as DataModels from "./DataModels";
import { SubscriptionType } from "./SubscriptionType";
@@ -120,7 +121,7 @@ export interface Collection extends CollectionBase {
requestSchema?: () => void;
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
usageSizeInKB: ko.Observable<number>;
offer: ko.Observable<DataModels.Offer>;
conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
@@ -362,7 +363,7 @@ export enum CollectionTabKind {
Gallery = 17,
NotebookViewer = 18,
Schema = 19,
SettingsV2 = 19
SettingsV2 = 20
}
export enum TerminalKind {
@@ -395,6 +396,7 @@ export interface DataExplorerInputsFrame {
isAuthWithresourceToken?: boolean;
defaultCollectionThroughput?: CollectionCreationDefaults;
flights?: readonly string[];
selfServeType?: SelfServeType;
}
export interface CollectionCreationDefaults {

View File

@@ -44,10 +44,6 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true);
});
it("should register settings-tab component", () => {
expect(ko.components.isRegistered("settings-tab")).toBe(true);
});
it("should register settings-tab-v2 component", () => {
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
});

View File

@@ -26,12 +26,11 @@ ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponent
ko.components.register("tabs-manager", TabsManagerKOComponent());
// Collection Tabs
ko.components.register("documents-tab", new TabComponents.MongoDocumentsTabV2());
ko.components.register("documents-tab", new TabComponents.DocumentsTab());
ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab());
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
ko.components.register("settings-tab", new TabComponents.SettingsTab());
ko.components.register("settings-tab-v2", new TabComponents.SettingsTabV2());
ko.components.register("query-tab", new TabComponents.QueryTab());
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());

View File

@@ -42,7 +42,7 @@ interface CollapsiblePanelParams {
* Use the optional "collapseToLeft" parameter to collapse to the left.
*/
class CollapsiblePanelViewModel {
private params: CollapsiblePanelParams;
public params: CollapsiblePanelParams;
private isCollapsed: ko.Observable<boolean>;
public constructor(params: CollapsiblePanelParams) {
@@ -50,7 +50,7 @@ class CollapsiblePanelViewModel {
this.isCollapsed = params.isCollapsed || ko.observable(false);
}
private toggleCollapse(): void {
public toggleCollapse(): void {
this.isCollapsed(!this.isCollapsed());
}
}

View File

@@ -44,12 +44,11 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
onChange?: (_?: React.FormEvent<HTMLElement | HTMLInputElement>, 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" },
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
{ key: "feature.enablecodeofconduct", label: "Enable Code Of Conduct Acknowledgement", value: "true" },
{ key: "feature.selfServeType", label: "Self serve feature", value: "sample" },
{
key: "feature.enableLinkInjection",
label: "Enable Injecting Notebook Viewer Link into the first cell",

View File

@@ -131,12 +131,6 @@ exports[`Feature panel renders all flags 1`] = `
label="Enable change feed policy"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.enablerupm"
label="Enable RUPM"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.dataexplorerexecutesproc"
@@ -163,8 +157,8 @@ exports[`Feature panel renders all flags 1`] = `
/>
<StyledCheckboxBase
checked={false}
key="feature.enablecodeofconduct"
label="Enable Code Of Conduct Acknowledgement"
key="feature.selfServeType"
label="Self serve feature"
onChange={[Function]}
/>
<StyledCheckboxBase

View File

@@ -71,7 +71,7 @@ interface InputTypeaheadParams {
/**
* This function gets called when pressing ENTER on the input box
*/
submitFct?: (inputValue: string, selection: Item) => void;
submitFct?: (inputValue: string | null, selection: Item | null) => void;
/**
* Typehead comes with a Search button that we normally remove.
@@ -88,8 +88,8 @@ interface OnClickItem {
}
interface Cache {
inputValue: string;
selection: Item;
inputValue: string | null;
selection: Item | null;
}
class InputTypeaheadViewModel {
@@ -98,15 +98,12 @@ class InputTypeaheadViewModel {
private params: InputTypeaheadParams;
private cache: Cache;
private inputValue: string;
private selection: Item;
public constructor(params: InputTypeaheadParams) {
this.instanceNumber = InputTypeaheadViewModel.instanceCount++;
this.params = params;
this.params.choices.subscribe(this.initializeTypeahead.bind(this));
this.cache = {
inputValue: null,
selection: null
@@ -161,7 +158,7 @@ class InputTypeaheadViewModel {
}
}
$.typeahead(options);
($ as any).typeahead(options);
}
/**
@@ -177,11 +174,11 @@ class InputTypeaheadViewModel {
* Use ko's "template: afterRender" callback to do that without actually using any template.
* Another way is to call it within setTimeout() in constructor.
*/
private afterRender(): void {
public afterRender(): void {
this.initializeTypeahead();
}
private submit(): void {
public submit(): void {
if (this.params.submitFct) {
this.params.submitFct(this.cache.inputValue, this.cache.selection);
}

View File

@@ -59,10 +59,12 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel {
this.params = params;
this.params.content.subscribe((newValue: string) => {
if (!!this.editor) {
this.editor.getModel().setValue(newValue);
} else {
this.createEditor(newValue, this.configureEditor.bind(this));
if (newValue) {
if (!!this.editor) {
this.editor.getModel().setValue(newValue);
} else {
this.createEditor(newValue, this.configureEditor.bind(this));
}
}
});

View File

@@ -1,6 +1,7 @@
import {
Dropdown,
FocusZone,
FontIcon,
FontWeights,
IDropdownOption,
IPageSpecification,
@@ -16,7 +17,7 @@ import {
Text
} from "office-ui-fabric-react";
import * as React from "react";
import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils";
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
@@ -136,7 +137,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}
public render(): JSX.Element {
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
const tabs: GalleryTabInfo[] = [this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
if (this.props.container?.isGalleryPublishEnabled()) {
tabs.push(
@@ -146,7 +147,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
this.state.isCodeOfConductAccepted
)
);
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
// Displaying code of conduct component on gallery load should not be the default behavior.
@@ -183,6 +184,27 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
);
}
private isEmptyData = (data: IGalleryItem[]): boolean => {
return !data || data.length === 0;
};
private createEmptyTabContent = (iconName: string, line1: string, line2: string): JSX.Element => {
return (
<Stack horizontalAlign="center" tokens={{ childrenGap: 10 }}>
<FontIcon iconName={iconName} style={{ fontSize: 100, color: "lightgray", marginTop: 20 }} />
<Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{line1}</Text>
<Text>{line2}</Text>
</Stack>
);
};
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<GalleryViewerCompone
};
}
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
private createFavoritesTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
return {
tab,
content: this.createSearchBarHeader(this.createCardsTabContent(data))
content: this.isEmptyData(data)
? this.createEmptyTabContent(
"ContactHeart",
"You have not liked anything",
"Like any notebook from Official Samples or Public gallery"
)
: this.createSearchBarHeader(this.createCardsTabContent(data))
};
}
private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
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<GalleryViewerCompone
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
if (!offline) {
try {
let response: IJunoResponse<IPublicGalleryData> | IJunoResponse<IGalleryItem[]>;
if (this.props.container.isCodeOfConductEnabled()) {
response = await this.props.junoClient.fetchPublicNotebooks();
let response: IJunoResponse<IGalleryItem[]> | IJunoResponse<IPublicGalleryData>;
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<GalleryViewerCompone
private deleteItem = async (data: IGalleryItem): Promise<void> => {
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);
});
};

View File

@@ -89,12 +89,12 @@ describe("SettingsComponent", () => {
it("auto pilot helper functions pass on correct value", () => {
const newCollection = { ...collection };
newCollection.offer = ko.observable<DataModels.Offer>({
content: {
offerAutopilotSettings: {
maxThroughput: 10000
}
}
} as DataModels.Offer);
autoscaleMaxThroughput: 10000,
manualThroughput: undefined,
minimumThroughput: 400,
id: "test",
offerReplacePending: false
});
const props = { ...baseProps };
props.settingsTab.collection = newCollection;
@@ -187,21 +187,6 @@ describe("SettingsComponent", () => {
expect(settingsComponentInstance.hasConflictResolution()).toEqual(true);
});
it("isOfferReplacePending", () => {
let settingsComponentInstance = new SettingsComponent(baseProps);
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(undefined);
const newCollection = { ...collection };
newCollection.offer = ko.observable({
headers: { "x-ms-offer-replace-pending": true }
} as DataModels.OfferWithHeaders);
const props = { ...baseProps };
props.settingsTab.collection = newCollection;
settingsComponentInstance = new SettingsComponent(props);
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(true);
});
it("save calls updateCollection, updateMongoDBCollectionThroughRP and updateOffer", async () => {
const wrapper = shallow(<SettingsComponent {...baseProps} />);
wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true, isMongoIndexingPolicySaveable: true });
@@ -246,7 +231,7 @@ describe("SettingsComponent", () => {
it("getUpdatedConflictResolutionPolicy", () => {
const wrapper = shallow(<SettingsComponent {...baseProps} />);
const conflictResolutionPolicyPath = "_ts";
const conflictResolutionPolicyPath = "/_ts";
const conflictResolutionPolicyProcedure = "sample_sproc";
const expectSprocPath =
"/dbs/" + collection.databaseId + "/colls/" + collection.id() + "/sprocs/" + conflictResolutionPolicyProcedure;

View File

@@ -2,28 +2,23 @@ import * as React from "react";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import * as Constants from "../../../Common/Constants";
import * as DataModels from "../../../Contracts/DataModels";
import * as SharedConstants from "../../../Shared/Constants";
import * as ViewModels from "../../../Contracts/ViewModels";
import DiscardIcon from "../../../../images/discard.svg";
import SaveIcon from "../../../../images/save-cosmos.svg";
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import Explorer from "../../Explorer";
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { userContext } from "../../../UserContext";
import { updateOfferThroughputBeyondLimit } from "../../../Common/dataAccess/updateOfferThroughputBeyondLimit";
import SettingsTab from "../../Tabs/SettingsTabV2";
import { throughputUnit } from "./SettingsRenderUtils";
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
import {
MongoIndexingPolicyComponent,
MongoIndexingPolicyComponentProps
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
import {
getMaxRUs,
hasDatabaseSharedThroughput,
GeospatialConfigType,
TtlType,
@@ -49,6 +44,7 @@ import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/genera
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { isEmpty } from "underscore";
interface SettingsV2TabInfo {
tab: SettingsV2TabTypes;
@@ -142,8 +138,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
// Mongo container with system partition key still treat as "Fixed"
this.isFixedContainer =
!this.collection.partitionKey ||
(this.container.isPreferredApiMongoDB() && this.collection.partitionKey.systemKey);
this.container.isPreferredApiMongoDB() &&
(!this.collection.partitionKey || this.collection.partitionKey.systemKey);
this.state = {
throughput: undefined,
@@ -227,7 +223,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
public loadMongoIndexes = async (): Promise<void> => {
if (
this.container.isMongoIndexEditorEnabled() &&
this.container.isPreferredApiMongoDB() &&
this.container.isEnableMongoCapabilityPresent() &&
this.container.databaseAccount()
@@ -275,19 +270,14 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
};
private setAutoPilotStates = (): void => {
const offer = this.collection?.offer && this.collection.offer();
const offerAutopilotSettings = offer?.content?.offerAutopilotSettings;
const autoscaleMaxThroughput = this.collection?.offer()?.autoscaleMaxThroughput;
if (
offerAutopilotSettings &&
offerAutopilotSettings.maxThroughput &&
AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)
) {
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
this.setState({
isAutoPilotSelected: true,
wasAutopilotOriginallySet: true,
autoPilotThroughput: offerAutopilotSettings.maxThroughput,
autoPilotThroughputBaseline: offerAutopilotSettings.maxThroughput
autoPilotThroughput: autoscaleMaxThroughput,
autoPilotThroughputBaseline: autoscaleMaxThroughput
});
}
};
@@ -305,12 +295,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
!!this.collection.conflictResolutionPolicy();
public isOfferReplacePending = (): boolean => {
const offer = this.collection?.offer && this.collection.offer();
return (
offer &&
Object.keys(offer).find(value => value === "headers") &&
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
);
return this.collection?.offer()?.offerReplacePending;
};
public onSaveClick = async (): Promise<void> => {
@@ -448,103 +433,34 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}
if (this.state.isScaleSaveable) {
const newThroughput = this.state.throughput;
const newOffer: DataModels.Offer = { ...this.collection.offer() };
const originalThroughputValue: number = this.state.throughput;
if (newOffer.content) {
newOffer.content.offerThroughput = newThroughput;
} else {
newOffer.content = {
offerThroughput: newThroughput,
offerIsRUPerMinuteThroughputEnabled: false
};
}
const headerOptions: RequestOptions = { initialHeaders: {} };
if (this.state.isAutoPilotSelected) {
newOffer.content.offerAutopilotSettings = {
maxThroughput: this.state.autoPilotThroughput
};
// user has changed from provisioned --> autoscale
if (this.hasProvisioningTypeChanged()) {
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
delete newOffer.content.offerAutopilotSettings;
} else {
delete newOffer.content.offerThroughput;
}
} else {
this.setState({
isAutoPilotSelected: false
});
// user has changed from autoscale --> provisioned
if (this.hasProvisioningTypeChanged()) {
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
} else {
delete newOffer.content.offerAutopilotSettings;
}
}
if (
getMaxRUs(this.collection, this.container) <=
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
this.container
) {
const requestPayload = {
subscriptionId: userContext.subscriptionId,
databaseAccountName: userContext.databaseAccount.name,
resourceGroup: userContext.resourceGroup,
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
throughput: newThroughput,
offerIsRUPerMinuteThroughputEnabled: false
};
await updateOfferThroughputBeyondLimit(requestPayload);
this.collection.offer().content.offerThroughput = originalThroughputValue;
this.setState({
isScaleSaveable: false,
isScaleDiscardable: false,
throughput: originalThroughputValue,
throughputBaseline: originalThroughputValue,
initialNotification: {
description: `Throughput update for ${newThroughput} ${throughputUnit}`
} as DataModels.Notification
});
} else {
const updateOfferParams: DataModels.UpdateOfferParams = {
databaseId: this.collection.databaseId,
collectionId: this.collection.id(),
currentOffer: this.collection.offer(),
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
manualThroughput: this.state.isAutoPilotSelected ? undefined : newThroughput
};
if (this.hasProvisioningTypeChanged()) {
if (this.state.isAutoPilotSelected) {
updateOfferParams.migrateToAutoPilot = true;
} else {
updateOfferParams.migrateToManual = true;
}
}
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
this.collection.offer(updatedOffer);
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
const updateOfferParams: DataModels.UpdateOfferParams = {
databaseId: this.collection.databaseId,
collectionId: this.collection.id(),
currentOffer: this.collection.offer(),
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput
};
if (this.hasProvisioningTypeChanged()) {
if (this.state.isAutoPilotSelected) {
this.setState({
autoPilotThroughput: updatedOffer.content.offerAutopilotSettings.maxThroughput,
autoPilotThroughputBaseline: updatedOffer.content.offerAutopilotSettings.maxThroughput
});
updateOfferParams.migrateToAutoPilot = true;
} else {
this.setState({
throughput: updatedOffer.content.offerThroughput,
throughputBaseline: updatedOffer.content.offerThroughput
});
updateOfferParams.migrateToManual = true;
}
}
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
this.collection.offer(updatedOffer);
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
if (this.state.isAutoPilotSelected) {
this.setState({
autoPilotThroughput: updatedOffer.autoscaleMaxThroughput,
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput
});
} else {
this.setState({
throughput: updatedOffer.manualThroughput,
throughputBaseline: updatedOffer.manualThroughput
});
}
}
this.container.isRefreshingExplorer(false);
this.setBaseline();
@@ -768,7 +684,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
if (policy.mode === DataModels.ConflictResolutionMode.LastWriterWins) {
policy.conflictResolutionPath = this.state.conflictResolutionPolicyPath;
if (policy.conflictResolutionPath?.startsWith("/")) {
if (!policy.conflictResolutionPath?.startsWith("/")) {
policy.conflictResolutionPath = "/" + policy.conflictResolutionPath;
}
}
@@ -809,7 +725,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}
}
const offerThroughput = this.collection?.offer && this.collection.offer()?.content?.offerThroughput;
const offerThroughput = this.collection.offer()?.manualThroughput;
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
? ChangeFeedPolicyState.On
: ChangeFeedPolicyState.Off;
@@ -1000,15 +916,18 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
tab: SettingsV2TabTypes.IndexingPolicyTab,
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
});
} else if (
this.container.isMongoIndexEditorEnabled() &&
this.container.isPreferredApiMongoDB() &&
this.container.isEnableMongoCapabilityPresent()
) {
tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab,
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
});
} else if (this.container.isPreferredApiMongoDB()) {
if (isEmpty(this.container.features())) {
tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab,
content: mongoIndexingPolicyAADError
});
} else if (this.container.isEnableMongoCapabilityPresent()) {
tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab,
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
});
}
}
if (this.hasConflictResolution()) {

View File

@@ -1,9 +1,9 @@
import { shallow } from "enzyme";
import React from "react";
import { IColumn, Text } from "office-ui-fabric-react";
import {
getAutoPilotV3SpendElement,
getEstimatedSpendElement,
getEstimatedAutoscaleSpendElement,
getEstimatedSpendingElement,
manualToAutoscaleDisclaimerElement,
ttlWarning,
indexingPolicynUnsavedWarningMessage,
@@ -19,11 +19,37 @@ import {
mongoIndexingPolicyDisclaimer,
mongoIndexingPolicyAADError,
mongoIndexTransformationRefreshingMessage,
renderMongoIndexTransformationRefreshMessage
renderMongoIndexTransformationRefreshMessage,
ManualEstimatedSpendingDisplayProps,
PriceBreakdown,
getRuPriceBreakdown
} from "./SettingsRenderUtils";
class SettingsRenderUtilsTestComponent extends React.Component {
public render(): JSX.Element {
const estimatedSpendingColumns: IColumn[] = [
{ key: "costType", name: "", fieldName: "costType", minWidth: 100, maxWidth: 200, isResizable: true },
{ key: "hourly", name: "Hourly", fieldName: "hourly", minWidth: 100, maxWidth: 200, isResizable: true },
{ key: "daily", name: "Daily", fieldName: "daily", minWidth: 100, maxWidth: 200, isResizable: true },
{ key: "monthly", name: "Monthly", fieldName: "monthly", minWidth: 100, maxWidth: 200, isResizable: true }
];
const estimatedSpendingItems: ManualEstimatedSpendingDisplayProps[] = [
{
costType: <Text>Current Cost</Text>,
hourly: <Text>$ 1.02</Text>,
daily: <Text>$ 24.48</Text>,
monthly: <Text>$ 744.6</Text>
}
];
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, true)}
{getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)}
{getEstimatedSpendingElement(estimatedSpendingColumns, estimatedSpendingItems, 1000, 2, priceBreakdown, false)}
{manualToAutoscaleDisclaimerElement}
{ttlWarning}
@@ -42,7 +66,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
{updateThroughputDelayedApplyWarningMessage}
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection")}
{getThroughputApplyLongDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
{getToolTipContainer(<span>Sample Text</span>)}
@@ -69,4 +93,14 @@ describe("SettingsUtils functions", () => {
const wrapper = shallow(<SettingsRenderUtilsTestComponent />);
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("$");
});
});

View File

@@ -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,11 +31,42 @@ import {
MessageBarType,
Stack,
Spinner,
SpinnerSize
SpinnerSize,
DetailsList,
IColumn,
SelectionMode,
DetailsListLayoutMode,
IDetailsRowProps,
DetailsRow,
IDetailsColumnStyles
} from "office-ui-fabric-react";
import { isDirtyTypes, isDirty } from "./SettingsUtils";
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } };
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: 14 } };
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
label: {
@@ -104,6 +134,16 @@ export const transparentDetailsRowStyles: Partial<IDetailsRowStyles> = {
}
};
export const transparentDetailsHeaderStyle: Partial<IDetailsColumnStyles> = {
root: {
selectors: {
":hover": {
background: "transparent"
}
}
}
};
export const customDetailsListStyles: Partial<IDetailsListStyles> = {
root: {
selectors: {
@@ -126,10 +166,17 @@ export const separatorStyles: Partial<ISeparatorStyles> = {
]
};
export const messageBarStyles: Partial<IMessageBarStyles> = { root: { marginTop: "5px" } };
export const messageBarStyles: Partial<IMessageBarStyles> = {
root: { marginTop: "5px", backgroundColor: "white" },
text: { fontSize: 14 }
};
export const throughputUnit = "RU/s";
export function onRenderRow(props: IDetailsRowProps): JSX.Element {
return <DetailsRow {...props} styles={transparentDetailsRowStyles} />;
}
export const getAutoPilotV3SpendElement = (
maxAutoPilotThroughputSet: number,
isDatabaseThroughput: boolean,
@@ -165,64 +212,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 (
<Text id="autoscaleSpendElement">
Estimated monthly cost ({currency}) is{" "}
<b>
{currencySign}
{calculateEstimateNumber(monthlyPrice / 10)}
{` - `}
{currencySign}
{calculateEstimateNumber(monthlyPrice)}{" "}
</b>
({"regions: "} {regions}, {throughput / 10} - {throughput} RU/s, {currencySign}
{pricePerRu}/RU)
</Text>
);
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,
rupmEnabled: boolean
numberOfRegions: number,
priceBreakdown: PriceBreakdown,
isAutoscale: boolean
): JSX.Element => {
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, rupmEnabled, 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 (
<Text id="throughputSpendElement">
Estimated cost ({currency}):{" "}
<b>
{currencySign}
{calculateEstimateNumber(hourlyPrice)} hourly {` / `}
{currencySign}
{calculateEstimateNumber(dailyPrice)} daily {` / `}
{currencySign}
{calculateEstimateNumber(monthlyPrice)} monthly{" "}
</b>
({"regions: "} {regions}, {throughput}RU/s, {currencySign}
{pricePerRu}/RU)
</Text>
<Stack {...addMongoIndexStackProps} styles={mediumWidthStackStyles}>
<DetailsList
disableSelectionZone
items={estimatedSpendingItems}
columns={estimatedSpendingColumns}
selectionMode={SelectionMode.none}
layoutMode={DetailsListLayoutMode.justified}
onRenderRow={onRenderRow}
/>
<Text id="throughputSpendElement">
({"regions: "} {numberOfRegions}, {ruRange}
{throughput} RU/s, {priceBreakdown.currencySign}
{priceBreakdown.pricePerRu}/RU)
</Text>
<Text>
<em>{estimatedCostDisclaimer}</em>
</Text>
</Stack>
);
};
@@ -266,6 +310,13 @@ export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
</Text>
);
export const saveThroughputWarningMessage: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below
before saving your changes
</Text>
);
const getCurrentThroughput = (
isAutoscale: boolean,
throughput: number,
@@ -319,14 +370,13 @@ export const getThroughputApplyShortDelayMessage = (
throughput: number,
throughputUnit: string,
databaseName: string,
collectionName: string,
targetThroughput: number
collectionName: string
): JSX.Element => (
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
A request to increase the throughput is currently in progress. This operation will take some time to complete.
<br />
Database: {databaseName}, Container: {collectionName}{" "}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, targetThroughput)}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
</Text>
);

View File

@@ -8,7 +8,7 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}

View File

@@ -6,8 +6,6 @@ import {
IconButton,
Text,
SelectionMode,
IDetailsRowProps,
DetailsRow,
IColumn,
MessageBar,
MessageBarType,
@@ -21,12 +19,11 @@ import {
mongoIndexingPolicyDisclaimer,
mediumWidthStackStyles,
subComponentStackProps,
transparentDetailsRowStyles,
createAndAddMongoIndexStackProps,
separatorStyles,
mongoIndexingPolicyAADError,
indexingPolicynUnsavedWarningMessage,
infoAndToolTipTextStyle
infoAndToolTipTextStyle,
onRenderRow
} from "../../SettingsRenderUtils";
import { MongoIndex } from "../../../../../Utils/arm/generatedClients/2020-04-01/types";
import {
@@ -40,7 +37,6 @@ import {
} from "../../SettingsUtils";
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
import { AuthType } from "../../../../../AuthType";
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
export interface MongoIndexingPolicyComponentProps {
@@ -142,10 +138,6 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
return undefined;
};
private onRenderRow = (props: IDetailsRowProps): JSX.Element => {
return <DetailsRow {...props} styles={transparentDetailsRowStyles} />;
};
private getActionButton = (arrayPosition: number, isCurrentIndex: boolean): JSX.Element => {
return isCurrentIndex ? (
<IconButton
@@ -255,7 +247,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
items={initialIndexes}
columns={this.initialIndexesColumns}
selectionMode={SelectionMode.none}
onRenderRow={this.onRenderRow}
onRenderRow={onRenderRow}
layoutMode={DetailsListLayoutMode.justified}
/>
{this.renderIndexesToBeAdded()}
@@ -281,7 +273,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
items={indexesToBeDropped}
columns={this.indexesToBeDroppedColumns}
selectionMode={SelectionMode.none}
onRenderRow={this.onRenderRow}
onRenderRow={onRenderRow}
layoutMode={DetailsListLayoutMode.justified}
/>
)}
@@ -321,7 +313,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
</Stack>
);
} else {
return window.authType !== AuthType.AAD ? mongoIndexingPolicyAADError : <Spinner size={SpinnerSize.large} />;
return <Spinner size={SpinnerSize.large} />;
}
}
}

View File

@@ -44,7 +44,7 @@ describe("ScaleComponent", () => {
} as DataModels.Notification
};
it("renders with correct intiial notification", () => {
it("renders with correct initial notification", () => {
let wrapper = shallow(<ScaleComponent {...baseProps} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
@@ -54,16 +54,13 @@ describe("ScaleComponent", () => {
const newCollection = { ...collection };
const maxThroughput = 5000;
const targetMaxThroughput = 50000;
newCollection.offer = ko.observable({
content: {
offerAutopilotSettings: {
maxThroughput: maxThroughput,
targetMaxThroughput: targetMaxThroughput
}
},
headers: { "x-ms-offer-replace-pending": true }
} as DataModels.OfferWithHeaders);
manualThroughput: undefined,
autoscaleMaxThroughput: maxThroughput,
minimumThroughput: 400,
id: "offer",
offerReplacePending: true
});
const newProps = {
...baseProps,
initialNotification: undefined as DataModels.Notification,
@@ -73,7 +70,6 @@ describe("ScaleComponent", () => {
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(targetMaxThroughput);
});
it("autoScale disabled", () => {
@@ -109,11 +105,6 @@ describe("ScaleComponent", () => {
expect(scaleComponent.isAutoScaleEnabled()).toEqual(true);
});
it("getMaxRUThroughputInputLimit", () => {
const scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.getMaxRUThroughputInputLimit()).toEqual(40000);
});
it("getThroughputTitle", () => {
let scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
@@ -138,14 +129,8 @@ describe("ScaleComponent", () => {
it("getThroughputWarningMessage", () => {
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
const throughputBeyondMaxRus = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million - 1000;
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
let scaleComponent = new ScaleComponent(newProps);
const scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
newProps.throughput = throughputBeyondMaxRus;
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputDelayedApplyWarningMessage");
});
});

View File

@@ -12,12 +12,11 @@ import {
throughputUnit,
getThroughputApplyLongDelayMessage,
getThroughputApplyShortDelayMessage,
updateThroughputBeyondLimitWarningMessage,
updateThroughputDelayedApplyWarningMessage
updateThroughputBeyondLimitWarningMessage
} from "../SettingsRenderUtils";
import { getMaxRUs, getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils";
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 {
@@ -62,11 +61,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
};
private getStorageCapacityTitle = (): JSX.Element => {
// Mongo container with system partition key still treat as "Fixed"
const isFixed =
!this.props.collection.partitionKey ||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey);
const capacity: string = isFixed ? "Fixed" : "Unlimited";
const capacity: string = this.props.isFixedContainer ? "Fixed" : "Unlimited";
return (
<Stack {...titleAndInputStackProps}>
<Label>Storage capacity</Label>
@@ -75,12 +70,26 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
);
};
public getMaxRUThroughputInputLimit = (): number => {
if (configContext.platform === Platform.Hosted && this.props.collection.partitionKey) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
public getMaxRUs = (): number => {
if (this.props.container?.isTryCosmosDBSubscription()) {
return Constants.TryCosmosExperience.maxRU;
}
return getMaxRUs(this.props.collection, this.props.container);
if (this.props.isFixedContainer) {
return SharedConstants.CollectionCreation.MaxRUPerPartition;
}
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
};
public getMinRUs = (): number => {
if (this.props.container?.isTryCosmosDBSubscription()) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
}
return (
this.props.collection.offer()?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400
);
};
public getThroughputTitle = (): string => {
@@ -88,11 +97,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
return AutoPilotUtils.getAutoPilotHeaderText();
}
const minThroughput: string = getMinRUs(this.props.collection, this.props.container).toLocaleString();
const maxThroughput: string =
this.canThroughputExceedMaximumValue() && !this.props.isFixedContainer
? "unlimited"
: getMaxRUs(this.props.collection, this.props.container).toLocaleString();
const minThroughput: string = this.getMinRUs().toLocaleString();
const maxThroughput: string = !this.props.isFixedContainer ? "unlimited" : this.getMaxRUs().toLocaleString();
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
};
@@ -109,26 +115,15 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
return this.getLongDelayMessage();
}
const offer = this.props.collection?.offer && this.props.collection.offer();
if (
offer &&
Object.keys(offer).find(value => {
return value === "headers";
}) &&
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
) {
const throughput = offer?.content?.offerAutopilotSettings?.maxThroughput;
const targetThroughput =
offer.content?.offerAutopilotSettings?.targetMaxThroughput || offer?.content?.offerThroughput;
const offer = this.props.collection?.offer();
if (offer?.offerReplacePending) {
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
return getThroughputApplyShortDelayMessage(
this.props.isAutoPilotSelected,
throughput,
throughputUnit,
this.props.collection.databaseId,
this.props.collection.id(),
targetThroughput
this.props.collection.id()
);
}
@@ -138,21 +133,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
public getThroughputWarningMessage = (): JSX.Element => {
const throughputExceedsBackendLimits: boolean =
this.canThroughputExceedMaximumValue() &&
getMaxRUs(this.props.collection, this.props.container) <=
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
return updateThroughputBeyondLimitWarningMessage;
}
const throughputExceedsMaxValue: boolean =
!this.isEmulator && this.props.throughput > getMaxRUs(this.props.collection, this.props.container);
if (throughputExceedsMaxValue && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
return updateThroughputDelayedApplyWarningMessage;
}
return undefined;
};
@@ -179,17 +165,20 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
private getThroughputInputComponent = (): JSX.Element => (
<ThroughputInputAutoPilotV3Component
databaseAccount={this.props.container.databaseAccount()}
databaseName={this.props.collection.databaseId}
collectionName={this.props.collection.id()}
serverId={this.props.container.serverId()}
throughput={this.props.throughput}
throughputBaseline={this.props.throughputBaseline}
onThroughputChange={this.props.onThroughputChange}
minimum={getMinRUs(this.props.collection, this.props.container)}
maximum={this.getMaxRUThroughputInputLimit()}
minimum={this.getMinRUs()}
maximum={this.getMaxRUs()}
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
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}
@@ -200,13 +189,41 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
onScaleSaveableChange={this.props.onScaleSaveableChange}
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
getThroughputWarningMessage={this.getThroughputWarningMessage}
usageSizeInKB={this.props.collection.quotaInfo().usageSizeInKB}
usageSizeInKB={this.props.collection.usageSizeInKB()}
/>
);
private isFreeTierAccount(): boolean {
const databaseAccount = this.props.container?.databaseAccount();
return databaseAccount?.properties?.enableFreeTier;
}
private getFreeTierInfoMessage(): JSX.Element {
return (
<Text>
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.
<Link
href="https://docs.microsoft.com/en-us/azure/cosmos-db/understand-your-bill#billing-examples-with-free-tier-accounts"
target="_blank"
>
Learn more.
</Link>
</Text>
);
}
public render(): JSX.Element {
return (
<Stack {...subComponentStackProps}>
{this.isFreeTierAccount() && (
<MessageBar
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
styles={{ text: { fontSize: 14 } }}
>
{this.getFreeTierInfoMessage()}
</MessageBar>
)}
{this.getInitialNotificationElement() && (
<MessageBar messageBarType={MessageBarType.warning}>{this.getInitialNotificationElement()}</MessageBar>
)}

View File

@@ -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<SubSettingsComponentPr
styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)}
/>
{isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && (
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
<MessageBar
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
styles={messageBarStyles}
>
{ttlWarning}
</MessageBar>
)}

View File

@@ -9,6 +9,8 @@ import * as DataModels from "../../../../../Contracts/DataModels";
describe("ThroughputInputAutoPilotV3Component", () => {
const baseProps: ThroughputInputAutoPilotV3Props = {
databaseAccount: {} as DataModels.DatabaseAccount,
databaseName: "test",
collectionName: "test",
serverId: undefined,
wasAutopilotOriginallySet: false,
throughput: 100,
@@ -26,6 +28,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
spendAckVisible: false,
showAsMandatory: true,
isFixed: false,
isFreeTierAccount: false,
label: "label",
infoBubbleText: "infoBubbleText",
canExceedMaximumValue: true,
@@ -54,7 +57,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 +74,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", () => {

View File

@@ -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,8 @@ import {
Label,
Link,
MessageBar,
MessageBarType
FontIcon,
IColumn
} from "office-ui-fabric-react";
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
@@ -32,11 +38,16 @@ 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";
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
export interface ThroughputInputAutoPilotV3Props {
databaseAccount: DataModels.DatabaseAccount;
databaseName: string;
collectionName: string;
serverId: string;
throughput: number;
throughputBaseline: number;
@@ -51,6 +62,7 @@ export interface ThroughputInputAutoPilotV3Props {
spendAckVisible?: boolean;
showAsMandatory?: boolean;
isFixed: boolean;
isFreeTierAccount: boolean;
isEmulator: boolean;
label: string;
infoBubbleText?: string;
@@ -69,6 +81,7 @@ export interface ThroughputInputAutoPilotV3Props {
interface ThroughputInputAutoPilotV3State {
spendAckChecked: boolean;
exceedFreeTierThroughput: boolean;
}
export class ThroughputInputAutoPilotV3Component extends React.Component<
@@ -142,7 +155,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;
@@ -165,34 +180,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,
false
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: <Text>Current Cost</Text>,
minPerMonth: (
<Text>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)}
</Text>
),
maxPerMonth: (
<Text>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}
</Text>
)
}
];
if (newThroughput) {
const newPrices: PriceBreakdown = getRuPriceBreakdown(
newThroughput,
serverId,
numberOfRegions,
isMultimaster,
true
);
estimatedSpendingItems.unshift({
costType: (
<Text>
<b>Updated Cost</b>
</Text>
),
minPerMonth: (
<Text>
<b>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)}
</b>
</Text>
),
maxPerMonth: (
<Text>
<b>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}
</b>
</Text>
)
});
}
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: <Text>Current Cost</Text>,
hourly: (
<Text>
{prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}
</Text>
),
daily: (
<Text>
{prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}
</Text>
),
monthly: (
<Text>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}
</Text>
)
}
];
if (newThroughput) {
const newPrices: PriceBreakdown = getRuPriceBreakdown(
newThroughput,
serverId,
numberOfRegions,
isMultimaster,
false
);
estimatedSpendingItems.unshift({
costType: (
<Text>
<b>Updated Cost</b>
</Text>
),
hourly: (
<Text>
<b>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}
</b>
</Text>
),
daily: (
<Text>
<b>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}
</b>
</Text>
),
monthly: (
<Text>
<b>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}
</b>
</Text>
)
});
}
return getEstimatedSpendingElement(
estimatedSpendingColumns,
estimatedSpendingItems,
newThroughput ?? throughput,
numberOfRegions,
prices,
false
);
};
private getAutoPilotUsageCost = (): JSX.Element => {
if (!this.props.maxAutoPilotThroughput) {
return <></>;
@@ -208,7 +432,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
const newThroughput = getSanitizedInputValue(newValue, this.autoPilotInputMaxValue);
const newThroughput = getSanitizedInputValue(newValue);
this.props.onMaxAutoPilotThroughputChange(newThroughput);
};
@@ -216,10 +440,11 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
const newThroughput = getSanitizedInputValue(newValue, this.throughputInputMaxValue);
const newThroughput = getSanitizedInputValue(newValue);
if (this.overrideWithAutoPilotSettings()) {
this.props.onMaxAutoPilotThroughputChange(newThroughput);
} else {
this.setState({ exceedFreeTierThroughput: this.props.isFreeTierAccount && newThroughput > 400 });
this.props.onThroughputChange(newThroughput);
}
};
@@ -227,7 +452,19 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
private onChoiceGroupChange = (
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
): void => this.props.onAutoPilotSelected(option.key === "true");
): void => {
this.props.onAutoPilotSelected(option.key === "true");
TelemetryProcessor.trace(Action.ToggleAutoscaleSetting, ActionModifiers.Mark, {
changedSelectedValueTo:
option.key === "true" ? ActionModifiers.ToggleAutoscaleOn : ActionModifiers.ToggleAutoscaleOff,
subscriptionId: userContext.subscriptionId,
databaseAccountName: this.props.databaseAccount?.name,
databaseName: this.props.databaseName,
collectionName: this.props.collectionName,
apiKind: userContext.defaultExperience,
dataExplorerArea: "Scale Tab V2"
});
};
private minRUperGBSurvey = (): JSX.Element => {
const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`;
@@ -263,7 +500,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
/>
</Label>
{this.overrideWithProvisionedThroughputSettings() && (
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
<MessageBar
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
styles={messageBarStyles}
>
{manualToAutoscaleDisclaimerElement}
</MessageBar>
)}
@@ -319,6 +559,12 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
private renderThroughputInput = (): JSX.Element => (
<Stack {...titleAndInputStackProps}>
<Text>
Estimate your required throughput with
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
{` capacity calculator`} <FontIcon iconName="NavigateExternalInline" />
</Link>
</Text>
<TextField
required
type="number"
@@ -334,8 +580,21 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
}
onChange={this.onThroughputChange}
/>
{this.state.exceedFreeTierThroughput && (
<MessageBar
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
styles={messageBarStyles}
>
{
"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."
}
</MessageBar>
)}
{this.props.getThroughputWarningMessage() && (
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
<MessageBar
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
styles={messageBarStyles}
>
{this.props.getThroughputWarningMessage()}
</MessageBar>
)}
@@ -350,13 +609,32 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
onChange={this.onSpendAckChecked}
/>
)}
<br />
{this.props.isFixed && <p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>}
</Stack>
);
private renderWarningMessage = (): JSX.Element => {
let warningMessage: JSX.Element;
if (this.IsComponentDirty().isDiscardable) {
warningMessage = saveThroughputWarningMessage;
}
return (
<>
{warningMessage && (
<MessageBar messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}>
{warningMessage}
</MessageBar>
)}
</>
);
};
public render(): JSX.Element {
return (
<Stack {...checkBoxAndInputStackProps}>
{this.renderWarningMessage()}
{this.renderThroughputModeChoices()}
{this.props.isAutoPilotSelected ? this.renderAutoPilotInput() : this.renderThroughputInput()}

View File

@@ -8,6 +8,26 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
}
}
>
<StyledMessageBarBase
messageBarIconProps={
Object {
"className": "messageBarWarningIcon",
"iconName": "WarningSolid",
}
}
>
<Text
styles={
Object {
"root": Object {
"fontSize": 14,
},
}
}
>
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below before saving your changes
</Text>
</StyledMessageBarBase>
<Stack>
<StyledLabelBase
id="settingsV2RadioButtonLabelId"
@@ -19,7 +39,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -30,12 +50,21 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
/>
</StyledLabelBase>
<StyledMessageBarBase
messageBarType={5}
messageBarIconProps={
Object {
"className": "messageBarInfoIcon",
"iconName": "InfoSolid",
}
}
styles={
Object {
"root": Object {
"backgroundColor": "white",
"marginTop": "5px",
},
"text": Object {
"fontSize": 14,
},
}
}
>
@@ -44,7 +73,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -156,7 +185,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -214,6 +243,19 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
}
}
>
<Text>
Estimate your required throughput with
<StyledLinkBase
href="https://cosmos.azure.com/capacitycalculator/"
target="_blank"
>
capacity calculator
<Component
iconName="NavigateExternalInline"
/>
</StyledLinkBase>
</Text>
<StyledTextFieldBase
disabled={false}
id="throughputInput"
@@ -239,38 +281,142 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
type="number"
value="100"
/>
<Text
id="throughputSpendElement"
<Stack
styles={
Object {
"root": Object {
"width": 600,
},
}
}
tokens={
Object {
"childrenGap": 10,
}
}
>
Estimated cost (
USD
):
<b>
$
0.0080
hourly
/
$
0.19
daily
/
$
5.84
monthly
<StyledWithViewportComponent
columns={
Array [
Object {
"fieldName": "costType",
"isResizable": true,
"key": "costType",
"maxWidth": 200,
"minWidth": 100,
"name": "",
"styles": Object {
"root": Object {
"selectors": Object {
":hover": Object {
"background": "transparent",
},
},
},
},
},
Object {
"fieldName": "hourly",
"isResizable": true,
"key": "hourly",
"maxWidth": 200,
"minWidth": 100,
"name": "Hourly",
"styles": Object {
"root": Object {
"selectors": Object {
":hover": Object {
"background": "transparent",
},
},
},
},
},
Object {
"fieldName": "daily",
"isResizable": true,
"key": "daily",
"maxWidth": 200,
"minWidth": 100,
"name": "Daily",
"styles": Object {
"root": Object {
"selectors": Object {
":hover": Object {
"background": "transparent",
},
},
},
},
},
Object {
"fieldName": "monthly",
"isResizable": true,
"key": "monthly",
"maxWidth": 200,
"minWidth": 100,
"name": "Monthly",
"styles": Object {
"root": Object {
"selectors": Object {
":hover": Object {
"background": "transparent",
},
},
},
},
},
]
}
disableSelectionZone={true}
items={
Array [
Object {
"costType": <Text>
Current Cost
</Text>,
"daily": <Text>
$
0.19
</Text>,
"hourly": <Text>
$
0.0080
</Text>,
"monthly": <Text>
$
5.84
</Text>,
},
]
}
layoutMode={1}
onRenderRow={[Function]}
selectionMode={0}
/>
<Text
id="throughputSpendElement"
>
(
regions:
</b>
(
regions:
1
,
100
RU/s,
$
0.00008
/RU)
</Text>
1
,
100
RU/s,
$
0.00008
/RU)
</Text>
<Text>
<em>
*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account
</em>
</Text>
</Stack>
<StyledCheckboxBase
checked={false}
id="spendAckCheckBox"
@@ -288,6 +434,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
}
}
/>
<br />
</Stack>
</Stack>
`;
@@ -311,7 +458,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -369,6 +516,19 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
}
}
>
<Text>
Estimate your required throughput with
<StyledLinkBase
href="https://cosmos.azure.com/capacitycalculator/"
target="_blank"
>
capacity calculator
<Component
iconName="NavigateExternalInline"
/>
</StyledLinkBase>
</Text>
<StyledTextFieldBase
disabled={false}
id="throughputInput"
@@ -394,38 +554,143 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
type="number"
value="100"
/>
<Text
id="throughputSpendElement"
<Stack
styles={
Object {
"root": Object {
"width": 600,
},
}
}
tokens={
Object {
"childrenGap": 10,
}
}
>
Estimated cost (
USD
):
<b>
$
0.0080
hourly
/
$
0.19
daily
/
$
5.84
monthly
<StyledWithViewportComponent
columns={
Array [
Object {
"fieldName": "costType",
"isResizable": true,
"key": "costType",
"maxWidth": 200,
"minWidth": 100,
"name": "",
"styles": Object {
"root": Object {
"selectors": Object {
":hover": Object {
"background": "transparent",
},
},
},
},
},
Object {
"fieldName": "hourly",
"isResizable": true,
"key": "hourly",
"maxWidth": 200,
"minWidth": 100,
"name": "Hourly",
"styles": Object {
"root": Object {
"selectors": Object {
":hover": Object {
"background": "transparent",
},
},
},
},
},
Object {
"fieldName": "daily",
"isResizable": true,
"key": "daily",
"maxWidth": 200,
"minWidth": 100,
"name": "Daily",
"styles": Object {
"root": Object {
"selectors": Object {
":hover": Object {
"background": "transparent",
},
},
},
},
},
Object {
"fieldName": "monthly",
"isResizable": true,
"key": "monthly",
"maxWidth": 200,
"minWidth": 100,
"name": "Monthly",
"styles": Object {
"root": Object {
"selectors": Object {
":hover": Object {
"background": "transparent",
},
},
},
},
},
]
}
disableSelectionZone={true}
items={
Array [
Object {
"costType": <Text>
Current Cost
</Text>,
"daily": <Text>
$
0.19
</Text>,
"hourly": <Text>
$
0.0080
</Text>,
"monthly": <Text>
$
5.84
</Text>,
},
]
}
layoutMode={1}
onRenderRow={[Function]}
selectionMode={0}
/>
<Text
id="throughputSpendElement"
>
(
regions:
</b>
(
regions:
1
,
100
RU/s,
$
0.00008
/RU)
</Text>
1
,
100
RU/s,
$
0.00008
/RU)
</Text>
<Text>
<em>
*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account
</em>
</Text>
</Stack>
<br />
</Stack>
</Stack>
`;

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ScaleComponent renders with correct intiial notification 1`] = `
exports[`ScaleComponent renders with correct initial notification 1`] = `
<Stack
tokens={
Object {
@@ -16,7 +16,7 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -40,6 +40,8 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
>
<ThroughputInputAutoPilotV3Component
canExceedMaximumValue={true}
collectionName="test"
databaseName="test"
getThroughputWarningMessage={[Function]}
isAutoPilotSelected={false}
isEmulator={false}
@@ -48,7 +50,7 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
label="Throughput (6,000 - unlimited RU/s)"
maxAutoPilotThroughput={4000}
maxAutoPilotThroughputBaseline={4000}
maximum={40000}
maximum={1000000}
minimum={6000}
onAutoPilotSelected={[Function]}
onMaxAutoPilotThroughputChange={[Function]}
@@ -58,6 +60,7 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
spendAckChecked={false}
throughput={1000}
throughputBaseline={1000}
usageSizeInKB={100}
wasAutopilotOriginallySet={true}
/>
<Stack

View File

@@ -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,
},
}
}

View File

@@ -1,7 +1,5 @@
import { collection, container } from "./TestUtils";
import { collection } from "./TestUtils";
import {
getMaxRUs,
getMinRUs,
getMongoIndexType,
getMongoNotification,
getSanitizedInputValue,
@@ -23,16 +21,6 @@ import * as ViewModels from "../../../Contracts/ViewModels";
import ko from "knockout";
describe("SettingsUtils", () => {
it("getMaxRUs", () => {
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
expect(getMaxRUs(collection, container)).toEqual(40000);
});
it("getMinRUs", () => {
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
expect(getMinRUs(collection, container)).toEqual(6000);
});
it("hasDatabaseSharedThroughput", () => {
expect(hasDatabaseSharedThroughput(collection)).toEqual(undefined);

View File

@@ -1,10 +1,6 @@
import * as ViewModels from "../../../Contracts/ViewModels";
import * as DataModels from "../../../Contracts/DataModels";
import * as Constants from "../../../Common/Constants";
import * as SharedConstants from "../../../Shared/Constants";
import * as PricingUtils from "../../../Utils/PricingUtils";
import Explorer from "../../Explorer";
import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
const zeroValue = 0;
@@ -71,57 +67,6 @@ export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection):
return database?.isDatabaseShared() && !collection.offer();
};
export const getMaxRUs = (collection: ViewModels.Collection, container: Explorer): number => {
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription() || false;
if (isTryCosmosDBSubscription) {
return Constants.TryCosmosExperience.maxRU;
}
const numPartitionsFromOffer: number =
collection?.offer && collection.offer()?.content?.collectionThroughputInfo?.numPhysicalPartitions;
const numPartitionsFromQuotaInfo: number = collection?.quotaInfo()?.numPartitions;
const numPartitions = numPartitionsFromOffer ?? numPartitionsFromQuotaInfo ?? 1;
return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions;
};
export const getMinRUs = (collection: ViewModels.Collection, container: Explorer): number => {
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription();
if (isTryCosmosDBSubscription) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
}
const offerContent = collection?.offer && collection.offer()?.content;
if (offerContent?.offerAutopilotSettings) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
}
const collectionThroughputInfo: DataModels.OfferThroughputInfo = offerContent?.collectionThroughputInfo;
if (collectionThroughputInfo?.minimumRUForCollection > 0) {
return collectionThroughputInfo.minimumRUForCollection;
}
const numPartitions = collectionThroughputInfo?.numPhysicalPartitions ?? collection.quotaInfo()?.numPartitions;
if (!numPartitions || numPartitions === 1) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
}
const baseRU = SharedConstants.CollectionCreation.DefaultCollectionRUs400;
const quotaInKb = collection.quotaInfo().collectionSize;
const quotaInGb = PricingUtils.usageInGB(quotaInKb);
const perPartitionGBQuota: number = Math.max(10, quotaInGb / numPartitions);
const baseRUbyPartitions: number = ((numPartitions * perPartitionGBQuota) / 10) * 100;
return Math.max(baseRU, baseRUbyPartitions);
};
export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => {
// Backend can contain different casing as it does case-insensitive comparisson
if (!modeFromBackend) {
@@ -156,13 +101,13 @@ export const parseConflictResolutionProcedure = (procedureFromBackEnd: string):
return procedureFromBackEnd;
};
export const getSanitizedInputValue = (newValueString: string, max: number): number => {
export const getSanitizedInputValue = (newValueString: string, max?: number): number => {
const newValue = parseInt(newValueString);
if (isNaN(newValue)) {
return zeroValue;
}
// make sure new value does not exceed the maximum throughput
return Math.min(newValue, max);
return max ? Math.min(newValue, max) : newValue;
};
export const isDirty = (current: isDirtyTypes, baseline: isDirtyTypes): boolean => {

View File

@@ -18,17 +18,14 @@ export const collection = ({
excludedPaths: []
}),
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
quotaInfo: ko.observable<DataModels.CollectionQuotaInfo>({} as DataModels.CollectionQuotaInfo),
usageSizeInKB: ko.observable(100),
offer: ko.observable<DataModels.Offer>({
content: {
offerThroughput: 10000,
offerIsRUPerMinuteThroughputEnabled: false,
collectionThroughputInfo: {
minimumRUForCollection: 6000,
numPhysicalPartitions: 4
} as DataModels.OfferThroughputInfo
}
} as DataModels.Offer),
autoscaleMaxThroughput: undefined,
manualThroughput: 10000,
minimumThroughput: 6000,
id: "offer",
offerReplacePending: false
}),
conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>(
{} as DataModels.ConflictResolutionPolicy
),

View File

@@ -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],
@@ -133,8 +135,6 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
@@ -593,6 +593,7 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
@@ -622,8 +623,6 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
@@ -669,6 +668,7 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
@@ -735,7 +735,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function],
},
"arcadiaToken": [Function],
"armEndpoint": [Function],
"browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
@@ -947,7 +946,7 @@ exports[`SettingsComponent renders 1`] = `
"hasWriteAccess": [Function],
"isAccountReady": [Function],
"isAuthWithResourceToken": [Function],
"isCodeOfConductEnabled": [Function],
"isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
@@ -956,7 +955,7 @@ exports[`SettingsComponent renders 1`] = `
"isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function],
"isMongoIndexEditorEnabled": [Function],
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function],
@@ -973,7 +972,6 @@ exports[`SettingsComponent renders 1`] = `
"isRightPanelV2Enabled": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSettingsV2Enabled": [Function],
"isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function],
@@ -1030,7 +1028,6 @@ exports[`SettingsComponent renders 1`] = `
"onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function],
"parentFrameDataExplorerVersion": [Function],
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
@@ -1056,7 +1053,6 @@ exports[`SettingsComponent renders 1`] = `
"titleLabel": "Select Columns",
"visible": [Function],
},
"quotaId": [Function],
"refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function],
"refreshTreeTitle": [Function],
@@ -1124,6 +1120,14 @@ exports[`SettingsComponent renders 1`] = `
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function],
"settingsPane": SettingsPane {
"container": [Circular],
@@ -1185,11 +1189,9 @@ exports[`SettingsComponent renders 1`] = `
},
"direction": "vertical",
"isCollapsed": [Function],
"leftSide": null,
"leftSideId": "resourcetree",
"onResizeStart": [Function],
"onResizeStop": [Function],
"splitter": null,
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
@@ -1302,9 +1304,9 @@ exports[`SettingsComponent renders 1`] = `
"version": 2,
},
"partitionKeyProperty": "partitionKey",
"quotaInfo": [Function],
"readSettings": [Function],
"uniqueKeyPolicy": Object {},
"usageSizeInKB": [Function],
}
}
container={
@@ -1336,6 +1338,7 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
@@ -1385,6 +1388,7 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
@@ -1414,8 +1418,6 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
@@ -1874,6 +1876,7 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
@@ -1903,8 +1906,6 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
@@ -1950,6 +1951,7 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
@@ -2016,7 +2018,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function],
},
"arcadiaToken": [Function],
"armEndpoint": [Function],
"browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
@@ -2228,7 +2229,7 @@ exports[`SettingsComponent renders 1`] = `
"hasWriteAccess": [Function],
"isAccountReady": [Function],
"isAuthWithResourceToken": [Function],
"isCodeOfConductEnabled": [Function],
"isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
@@ -2237,7 +2238,7 @@ exports[`SettingsComponent renders 1`] = `
"isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function],
"isMongoIndexEditorEnabled": [Function],
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function],
@@ -2254,7 +2255,6 @@ exports[`SettingsComponent renders 1`] = `
"isRightPanelV2Enabled": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSettingsV2Enabled": [Function],
"isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function],
@@ -2311,7 +2311,6 @@ exports[`SettingsComponent renders 1`] = `
"onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function],
"parentFrameDataExplorerVersion": [Function],
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
@@ -2337,7 +2336,6 @@ exports[`SettingsComponent renders 1`] = `
"titleLabel": "Select Columns",
"visible": [Function],
},
"quotaId": [Function],
"refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function],
"refreshTreeTitle": [Function],
@@ -2405,6 +2403,14 @@ exports[`SettingsComponent renders 1`] = `
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function],
"settingsPane": SettingsPane {
"container": [Circular],
@@ -2466,11 +2472,9 @@ exports[`SettingsComponent renders 1`] = `
},
"direction": "vertical",
"isCollapsed": [Function],
"leftSide": null,
"leftSideId": "resourcetree",
"onResizeStart": [Function],
"onResizeStop": [Function],
"splitter": null,
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
@@ -2630,6 +2634,7 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
@@ -2679,6 +2684,7 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
@@ -2708,8 +2714,6 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
@@ -3168,6 +3172,7 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
@@ -3197,8 +3202,6 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
@@ -3244,6 +3247,7 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
@@ -3310,7 +3314,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function],
},
"arcadiaToken": [Function],
"armEndpoint": [Function],
"browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
@@ -3522,7 +3525,7 @@ exports[`SettingsComponent renders 1`] = `
"hasWriteAccess": [Function],
"isAccountReady": [Function],
"isAuthWithResourceToken": [Function],
"isCodeOfConductEnabled": [Function],
"isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
@@ -3531,7 +3534,7 @@ exports[`SettingsComponent renders 1`] = `
"isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function],
"isMongoIndexEditorEnabled": [Function],
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function],
@@ -3548,7 +3551,6 @@ exports[`SettingsComponent renders 1`] = `
"isRightPanelV2Enabled": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSettingsV2Enabled": [Function],
"isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function],
@@ -3605,7 +3607,6 @@ exports[`SettingsComponent renders 1`] = `
"onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function],
"parentFrameDataExplorerVersion": [Function],
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
@@ -3631,7 +3632,6 @@ exports[`SettingsComponent renders 1`] = `
"titleLabel": "Select Columns",
"visible": [Function],
},
"quotaId": [Function],
"refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function],
"refreshTreeTitle": [Function],
@@ -3699,6 +3699,14 @@ exports[`SettingsComponent renders 1`] = `
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function],
"settingsPane": SettingsPane {
"container": [Circular],
@@ -3760,11 +3768,9 @@ exports[`SettingsComponent renders 1`] = `
},
"direction": "vertical",
"isCollapsed": [Function],
"leftSide": null,
"leftSideId": "resourcetree",
"onResizeStart": [Function],
"onResizeStop": [Function],
"splitter": null,
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
@@ -3877,9 +3883,9 @@ exports[`SettingsComponent renders 1`] = `
"version": 2,
},
"partitionKeyProperty": "partitionKey",
"quotaInfo": [Function],
"readSettings": [Function],
"uniqueKeyPolicy": Object {},
"usageSizeInKB": [Function],
}
}
container={
@@ -3911,6 +3917,7 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
@@ -3960,6 +3967,7 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
@@ -3989,8 +3997,6 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
@@ -4449,6 +4455,7 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
@@ -4478,8 +4485,6 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
@@ -4525,6 +4530,7 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
@@ -4591,7 +4597,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function],
},
"arcadiaToken": [Function],
"armEndpoint": [Function],
"browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
@@ -4803,7 +4808,7 @@ exports[`SettingsComponent renders 1`] = `
"hasWriteAccess": [Function],
"isAccountReady": [Function],
"isAuthWithResourceToken": [Function],
"isCodeOfConductEnabled": [Function],
"isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
@@ -4812,7 +4817,7 @@ exports[`SettingsComponent renders 1`] = `
"isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function],
"isMongoIndexEditorEnabled": [Function],
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function],
@@ -4829,7 +4834,6 @@ exports[`SettingsComponent renders 1`] = `
"isRightPanelV2Enabled": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSettingsV2Enabled": [Function],
"isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function],
@@ -4886,7 +4890,6 @@ exports[`SettingsComponent renders 1`] = `
"onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function],
"parentFrameDataExplorerVersion": [Function],
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
@@ -4912,7 +4915,6 @@ exports[`SettingsComponent renders 1`] = `
"titleLabel": "Select Columns",
"visible": [Function],
},
"quotaId": [Function],
"refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function],
"refreshTreeTitle": [Function],
@@ -4980,6 +4982,14 @@ exports[`SettingsComponent renders 1`] = `
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function],
"settingsPane": SettingsPane {
"container": [Circular],
@@ -5041,11 +5051,9 @@ exports[`SettingsComponent renders 1`] = `
},
"direction": "vertical",
"isCollapsed": [Function],
"leftSide": null,
"leftSideId": "resourcetree",
"onResizeStart": [Function],
"onResizeStop": [Function],
"splitter": null,
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {

View File

@@ -60,72 +60,106 @@ exports[`SettingsUtils functions render 1`] = `
</StyledLinkBase>
.
</Text>
<Text
id="throughputSpendElement"
<Stack
styles={
Object {
"root": Object {
"width": 600,
},
}
}
tokens={
Object {
"childrenGap": 10,
}
}
>
Estimated cost (
RMB
):
<b>
¥
1.29
hourly
/
¥
31.06
daily
/
¥
944.60
monthly
<StyledWithViewportComponent
columns={
Array [
Object {
"fieldName": "costType",
"isResizable": true,
"key": "costType",
"maxWidth": 200,
"minWidth": 100,
"name": "",
},
Object {
"fieldName": "hourly",
"isResizable": true,
"key": "hourly",
"maxWidth": 200,
"minWidth": 100,
"name": "Hourly",
},
Object {
"fieldName": "daily",
"isResizable": true,
"key": "daily",
"maxWidth": 200,
"minWidth": 100,
"name": "Daily",
},
Object {
"fieldName": "monthly",
"isResizable": true,
"key": "monthly",
"maxWidth": 200,
"minWidth": 100,
"name": "Monthly",
},
]
}
disableSelectionZone={true}
items={
Array [
Object {
"costType": <Text>
Current Cost
</Text>,
"daily": <Text>
$ 24.48
</Text>,
"hourly": <Text>
$ 1.02
</Text>,
"monthly": <Text>
$ 744.6
</Text>,
},
]
}
layoutMode={1}
onRenderRow={[Function]}
selectionMode={0}
/>
<Text
id="throughputSpendElement"
>
(
regions:
</b>
(
regions:
2
,
1000
RU/s,
¥
0.00051
/RU)
</Text>
<Text
id="autoscaleSpendElement"
>
Estimated monthly cost (
RMB
) is
<b>
2
,
1000
RU/s,
¥
111.69
-
¥
1116.90
</b>
(
regions:
2
,
100
-
1000
RU/s,
¥
0.000765
/RU)
</Text>
0.00051
/RU)
</Text>
<Text>
<em>
*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account
</em>
</Text>
</Stack>
<Text
id="manualToAutoscaleDisclaimerElement"
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -142,7 +176,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -161,7 +195,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -173,7 +207,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -185,7 +219,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -196,7 +230,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -215,7 +249,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -227,14 +261,14 @@ exports[`SettingsUtils functions render 1`] = `
, Container:
sampleCollection
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
, Current manual throughput: 1000 RU/s
</Text>
<Text
id="throughputApplyLongDelayMessage"
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -252,7 +286,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -265,7 +299,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -276,7 +310,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -295,7 +329,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -337,7 +371,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -352,7 +386,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}
@@ -368,7 +402,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"fontSize": 12,
"fontSize": 14,
},
}
}

View File

@@ -1,9 +1,9 @@
import React from "react";
import { shallow } from "enzyme";
import { SmartUiComponent, Descriptor, InputType } from "./SmartUiComponent";
import { SmartUiComponent, SmartUiDescriptor, UiType } from "./SmartUiComponent";
describe("SmartUiComponent", () => {
const exampleData: Descriptor = {
const exampleData: SmartUiDescriptor = {
root: {
id: "root",
info: {
@@ -24,7 +24,7 @@ describe("SmartUiComponent", () => {
max: 500,
step: 10,
defaultValue: 400,
inputType: "spin"
uiType: UiType.Spinner
}
},
{
@@ -37,7 +37,21 @@ describe("SmartUiComponent", () => {
max: 500,
step: 10,
defaultValue: 400,
inputType: "slider"
uiType: UiType.Slider
}
},
{
id: "throughput3",
input: {
label: "Throughput (invalid)",
dataFieldName: "throughput3",
type: "boolean",
min: 400,
max: 500,
step: 10,
defaultValue: 400,
uiType: UiType.Spinner,
errorMessage: "label, truelabel and falselabel are required for boolean input 'throughput3'"
}
},
{
@@ -64,11 +78,11 @@ describe("SmartUiComponent", () => {
input: {
label: "Database",
dataFieldName: "database",
type: "enum",
type: "object",
choices: [
{ label: "Database 1", key: "db1", value: "database1" },
{ label: "Database 2", key: "db2", value: "database2" },
{ label: "Database 3", key: "db3", value: "database3" }
{ label: "Database 1", key: "db1" },
{ label: "Database 2", key: "db2" },
{ label: "Database 3", key: "db3" }
],
defaultKey: "db2"
}
@@ -77,12 +91,11 @@ describe("SmartUiComponent", () => {
}
};
const exampleCallbacks = (newValues: Map<string, InputType>): void => {
console.log("New values:", newValues);
};
it("should render", () => {
const wrapper = shallow(<SmartUiComponent descriptor={exampleData} onChange={exampleCallbacks} />);
it("should render", async () => {
const wrapper = shallow(
<SmartUiComponent descriptor={exampleData} currentValues={new Map()} onInputChange={undefined} />
);
await new Promise(resolve => setTimeout(resolve, 0));
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -5,11 +5,9 @@ import { SpinButton } from "office-ui-fabric-react/lib/SpinButton";
import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
import { TextField } from "office-ui-fabric-react/lib/TextField";
import { Text } from "office-ui-fabric-react/lib/Text";
import { InputType } from "../../Tables/Constants";
import { RadioSwitchComponent } from "../RadioSwitchComponent/RadioSwitchComponent";
import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack";
import { Link, MessageBar, MessageBarType } from "office-ui-fabric-react";
import * as InputUtils from "./InputUtils";
import "./SmartUiComponent.less";
@@ -21,45 +19,16 @@ import "./SmartUiComponent.less";
* - a descriptor of the UX.
*/
export type InputTypeValue = "number" | "string" | "boolean" | "enum";
export type InputTypeValue = "number" | "string" | "boolean" | "object";
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export type EnumItem = { label: string; key: string; value: any };
export type InputType = number | string | boolean | EnumItem;
interface BaseInput {
label: string;
dataFieldName: string;
type: InputTypeValue;
placeholder?: string;
export enum UiType {
Spinner = "Spinner",
Slider = "Slider"
}
/**
* For now, this only supports integers
*/
export interface NumberInput extends BaseInput {
min?: number;
max?: number;
step: number;
defaultValue: number;
inputType: "spin" | "slider";
}
export type ChoiceItem = { label: string; key: string };
export interface BooleanInput extends BaseInput {
trueLabel: string;
falseLabel: string;
defaultValue: boolean;
}
export interface StringInput extends BaseInput {
defaultValue?: string;
}
export interface EnumInput extends BaseInput {
choices: EnumItem[];
defaultKey: string;
}
export type InputType = number | string | boolean | ChoiceItem;
export interface Info {
message: string;
@@ -69,28 +38,62 @@ export interface Info {
};
}
export type AnyInput = NumberInput | BooleanInput | StringInput | EnumInput;
interface BaseInput {
label: string;
dataFieldName: string;
type: InputTypeValue;
placeholder?: string;
errorMessage?: string;
}
export interface Node {
/**
* For now, this only supports integers
*/
interface NumberInput extends BaseInput {
min: number;
max: number;
step: number;
defaultValue?: number;
uiType: UiType;
}
interface BooleanInput extends BaseInput {
trueLabel: string;
falseLabel: string;
defaultValue?: boolean;
}
interface StringInput extends BaseInput {
defaultValue?: string;
}
interface ChoiceInput extends BaseInput {
choices: ChoiceItem[];
defaultKey?: string;
}
type AnyInput = NumberInput | BooleanInput | StringInput | ChoiceInput;
interface Node {
id: string;
info?: Info;
input?: AnyInput;
children?: Node[];
}
export interface Descriptor {
export interface SmartUiDescriptor {
root: Node;
}
/************************** Component implementation starts here ************************************* */
export interface SmartUiComponentProps {
descriptor: Descriptor;
onChange: (newValues: Map<string, InputType>) => void;
descriptor: SmartUiDescriptor;
currentValues: Map<string, InputType>;
onInputChange: (input: AnyInput, newValue: InputType) => void;
}
interface SmartUiComponentState {
currentValues: Map<string, InputType>;
errors: Map<string, string>;
}
@@ -104,7 +107,6 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
constructor(props: SmartUiComponentProps) {
super(props);
this.state = {
currentValues: new Map(),
errors: new Map()
};
}
@@ -113,42 +115,37 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return (
<MessageBar>
{info.message}
<Link href={info.link.href} target="_blank">
{info.link.text}
</Link>
{info.link && (
<Link href={info.link.href} target="_blank">
{info.link.text}
</Link>
)}
</MessageBar>
);
}
private onInputChange = (newValue: string | number | boolean, dataFieldName: string) => {
const { currentValues } = this.state;
currentValues.set(dataFieldName, newValue);
this.setState({ currentValues }, () => this.props.onChange(this.state.currentValues));
};
private renderStringInput(input: StringInput): JSX.Element {
private renderTextInput(input: StringInput): JSX.Element {
const value = this.props.currentValues.get(input.dataFieldName) as string;
return (
<div className="stringInputContainer">
<div>
<TextField
id={`${input.dataFieldName}-input`}
label={input.label}
type="text"
value={input.defaultValue}
placeholder={input.placeholder}
onChange={(_, newValue) => this.onInputChange(newValue, input.dataFieldName)}
styles={{
subComponentStyles: {
label: {
root: {
...SmartUiComponent.labelStyle,
fontWeight: 600
}
<TextField
id={`${input.dataFieldName}-textBox-input`}
label={input.label}
type="text"
value={value}
placeholder={input.placeholder}
onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
styles={{
subComponentStyles: {
label: {
root: {
...SmartUiComponent.labelStyle,
fontWeight: 600
}
}
}}
/>
</div>
}
}}
/>
</div>
);
}
@@ -159,10 +156,11 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
this.setState({ errors });
}
private onValidate = (value: string, min: number, max: number, dataFieldName: string): string => {
private onValidate = (input: AnyInput, value: string, min: number, max: number): string => {
const newValue = InputUtils.onValidateValueChange(value, min, max);
const dataFieldName = input.dataFieldName;
if (newValue) {
this.onInputChange(newValue, dataFieldName);
this.props.onInputChange(input, newValue);
this.clearError(dataFieldName);
return newValue.toString();
} else {
@@ -173,20 +171,22 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return undefined;
};
private onIncrement = (value: string, step: number, max: number, dataFieldName: string): string => {
private onIncrement = (input: AnyInput, value: string, step: number, max: number): string => {
const newValue = InputUtils.onIncrementValue(value, step, max);
const dataFieldName = input.dataFieldName;
if (newValue) {
this.onInputChange(newValue, dataFieldName);
this.props.onInputChange(input, newValue);
this.clearError(dataFieldName);
return newValue.toString();
}
return undefined;
};
private onDecrement = (value: string, step: number, min: number, dataFieldName: string): string => {
private onDecrement = (input: AnyInput, value: string, step: number, min: number): string => {
const newValue = InputUtils.onDecrementValue(value, step, min);
const dataFieldName = input.dataFieldName;
if (newValue) {
this.onInputChange(newValue, dataFieldName);
this.props.onInputChange(input, newValue);
this.clearError(dataFieldName);
return newValue.toString();
}
@@ -194,18 +194,26 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
};
private renderNumberInput(input: NumberInput): JSX.Element {
const { label, min, max, defaultValue, dataFieldName, step } = input;
const props = { label, min, max, ariaLabel: label, step };
const { label, min, max, dataFieldName, step } = input;
const props = {
label: label,
min: min,
max: max,
ariaLabel: label,
step: step
};
if (input.inputType === "spin") {
const value = this.props.currentValues.get(dataFieldName) as number;
if (input.uiType === UiType.Spinner) {
return (
<div>
<>
<SpinButton
{...props}
defaultValue={defaultValue.toString()}
onValidate={newValue => this.onValidate(newValue, min, max, dataFieldName)}
onIncrement={newValue => this.onIncrement(newValue, step, max, dataFieldName)}
onDecrement={newValue => this.onDecrement(newValue, step, min, dataFieldName)}
id={`${input.dataFieldName}-spinner-input`}
value={value?.toString()}
onValidate={newValue => this.onValidate(input, newValue, props.min, props.max)}
onIncrement={newValue => this.onIncrement(input, newValue, props.step, props.max)}
onDecrement={newValue => this.onDecrement(input, newValue, props.step, props.min)}
labelPosition={Position.top}
styles={{
label: {
@@ -217,34 +225,35 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
{this.state.errors.has(dataFieldName) && (
<MessageBar messageBarType={MessageBarType.error}>Error: {this.state.errors.get(dataFieldName)}</MessageBar>
)}
</>
);
} else if (input.uiType === UiType.Slider) {
return (
<div id={`${input.dataFieldName}-slider-input`}>
<Slider
{...props}
value={value}
onChange={newValue => this.props.onInputChange(input, newValue)}
styles={{
titleLabel: {
...SmartUiComponent.labelStyle,
fontWeight: 600
},
valueLabel: SmartUiComponent.labelStyle
}}
/>
</div>
);
} else if (input.inputType === "slider") {
return (
<Slider
// showValue={true}
// valueFormat={}
{...props}
defaultValue={defaultValue}
onChange={newValue => this.onInputChange(newValue, dataFieldName)}
styles={{
titleLabel: {
...SmartUiComponent.labelStyle,
fontWeight: 600
},
valueLabel: SmartUiComponent.labelStyle
}}
/>
);
} else {
return <>Unsupported number input type {input.inputType}</>;
return <>Unsupported number UI type {input.uiType}</>;
}
}
private renderBooleanInput(input: BooleanInput): JSX.Element {
const { dataFieldName } = input;
const value = this.props.currentValues.get(input.dataFieldName) as boolean;
const selectedKey = value || input.defaultValue ? "true" : "false";
return (
<div>
<div id={`${input.dataFieldName}-radioSwitch-input`}>
<div className="inputLabelContainer">
<Text variant="small" nowrap className="inputLabel">
{input.label}
@@ -255,41 +264,33 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
{
label: input.falseLabel,
key: "false",
onSelect: () => this.onInputChange(false, dataFieldName)
onSelect: () => this.props.onInputChange(input, false)
},
{
label: input.trueLabel,
key: "true",
onSelect: () => this.onInputChange(true, dataFieldName)
onSelect: () => this.props.onInputChange(input, true)
}
]}
selectedKey={
(this.state.currentValues.has(dataFieldName)
? (this.state.currentValues.get(dataFieldName) as boolean)
: input.defaultValue)
? "true"
: "false"
}
selectedKey={selectedKey}
/>
</div>
);
}
private renderEnumInput(input: EnumInput): JSX.Element {
const { label, defaultKey, dataFieldName, choices, placeholder } = input;
private renderChoiceInput(input: ChoiceInput): JSX.Element {
const { label, defaultKey: defaultKey, dataFieldName, choices, placeholder } = input;
const value = this.props.currentValues.get(dataFieldName) as string;
return (
<Dropdown
id={`${input.dataFieldName}-dropown-input`}
label={label}
selectedKey={
this.state.currentValues.has(dataFieldName)
? (this.state.currentValues.get(dataFieldName) as string)
: defaultKey
}
onChange={(_, item: IDropdownOption) => this.onInputChange(item.key.toString(), dataFieldName)}
selectedKey={value ? value : defaultKey}
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
placeholder={placeholder}
options={choices.map(c => ({
key: c.key,
text: c.value
text: c.label
}))}
styles={{
label: {
@@ -302,34 +303,48 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
);
}
private renderError(input: AnyInput): JSX.Element {
return <MessageBar messageBarType={MessageBarType.error}>Error: {input.errorMessage}</MessageBar>;
}
private renderInput(input: AnyInput): JSX.Element {
if (input.errorMessage) {
return this.renderError(input);
}
switch (input.type) {
case "string":
return this.renderStringInput(input as StringInput);
return this.renderTextInput(input as StringInput);
case "number":
return this.renderNumberInput(input as NumberInput);
case "boolean":
return this.renderBooleanInput(input as BooleanInput);
case "enum":
return this.renderEnumInput(input as EnumInput);
case "object":
return this.renderChoiceInput(input as ChoiceInput);
default:
throw new Error(`Unknown input type: ${input.type}`);
}
}
private renderNode(node: Node): JSX.Element {
const containerStackTokens: IStackTokens = { childrenGap: 10 };
const containerStackTokens: IStackTokens = { childrenGap: 15 };
return (
<Stack tokens={containerStackTokens} className="widgetRendererContainer">
{node.info && this.renderInfo(node.info)}
{node.input && this.renderInput(node.input)}
<Stack.Item>
{node.info && this.renderInfo(node.info as Info)}
{node.input && this.renderInput(node.input)}
</Stack.Item>
{node.children && node.children.map(child => <div key={child.id}>{this.renderNode(child)}</div>)}
</Stack>
);
}
render(): JSX.Element {
return <>{this.renderNode(this.props.descriptor.root)}</>;
const containerStackTokens: IStackTokens = { childrenGap: 20 };
return (
<Stack tokens={containerStackTokens} styles={{ root: { width: 400, padding: 10 } }}>
{this.renderNode(this.props.descriptor.root)}
</Stack>
);
}
}

View File

@@ -1,24 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SmartUiComponent should render 1`] = `
<Fragment>
<Stack
styles={
Object {
"root": Object {
"padding": 10,
"width": 400,
},
}
}
tokens={
Object {
"childrenGap": 20,
}
}
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
"childrenGap": 15,
}
}
>
<StyledMessageBarBase>
Start at $24/mo per database
<StyledLinkBase
href="https://aka.ms/azure-cosmos-db-pricing"
target="_blank"
>
More Details
</StyledLinkBase>
</StyledMessageBarBase>
<StackItem>
<StyledMessageBarBase>
Start at $24/mo per database
<StyledLinkBase
href="https://aka.ms/azure-cosmos-db-pricing"
target="_blank"
>
More Details
</StyledLinkBase>
</StyledMessageBarBase>
</StackItem>
<div
key="throughput"
>
@@ -26,11 +42,11 @@ exports[`SmartUiComponent should render 1`] = `
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
"childrenGap": 15,
}
}
>
<div>
<StackItem>
<CustomizedSpinButton
ariaLabel="Throughput (input)"
decrementButtonIcon={
@@ -38,8 +54,8 @@ exports[`SmartUiComponent should render 1`] = `
"iconName": "ChevronDownSmall",
}
}
defaultValue="400"
disabled={false}
id="throughput-spinner-input"
incrementButtonIcon={
Object {
"iconName": "ChevronUpSmall",
@@ -64,7 +80,7 @@ exports[`SmartUiComponent should render 1`] = `
}
}
/>
</div>
</StackItem>
</Stack>
</div>
<div
@@ -74,34 +90,60 @@ exports[`SmartUiComponent should render 1`] = `
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
"childrenGap": 15,
}
}
>
<StyledSliderBase
ariaLabel="Throughput (Slider)"
defaultValue={400}
label="Throughput (Slider)"
max={500}
min={400}
onChange={[Function]}
step={10}
styles={
Object {
"titleLabel": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
"valueLabel": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
}
<StackItem>
<div
id="throughput2-slider-input"
>
<StyledSliderBase
ariaLabel="Throughput (Slider)"
label="Throughput (Slider)"
max={500}
min={400}
onChange={[Function]}
step={10}
styles={
Object {
"titleLabel": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
"valueLabel": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
}
}
/>
</div>
</StackItem>
</Stack>
</div>
<div
key="throughput3"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 15,
}
/>
}
>
<StackItem>
<StyledMessageBarBase
messageBarType={1}
>
Error:
label, truelabel and falselabel are required for boolean input 'throughput3'
</StyledMessageBarBase>
</StackItem>
</Stack>
</div>
<div
@@ -111,16 +153,16 @@ exports[`SmartUiComponent should render 1`] = `
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
"childrenGap": 15,
}
}
>
<div
className="stringInputContainer"
>
<div>
<StackItem>
<div
className="stringInputContainer"
>
<StyledTextFieldBase
id="containerId-input"
id="containerId-textBox-input"
label="Container id"
onChange={[Function]}
styles={
@@ -140,7 +182,7 @@ exports[`SmartUiComponent should render 1`] = `
type="text"
/>
</div>
</div>
</StackItem>
</Stack>
</div>
<div
@@ -150,40 +192,44 @@ exports[`SmartUiComponent should render 1`] = `
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
"childrenGap": 15,
}
}
>
<div>
<StackItem>
<div
className="inputLabelContainer"
id="analyticalStore-radioSwitch-input"
>
<Text
className="inputLabel"
nowrap={true}
variant="small"
<div
className="inputLabelContainer"
>
Analytical Store
</Text>
<Text
className="inputLabel"
nowrap={true}
variant="small"
>
Analytical Store
</Text>
</div>
<RadioSwitchComponent
choices={
Array [
Object {
"key": "false",
"label": "Disabled",
"onSelect": [Function],
},
Object {
"key": "true",
"label": "Enabled",
"onSelect": [Function],
},
]
}
selectedKey="true"
/>
</div>
<RadioSwitchComponent
choices={
Array [
Object {
"key": "false",
"label": "Disabled",
"onSelect": [Function],
},
Object {
"key": "true",
"label": "Enabled",
"onSelect": [Function],
},
]
}
selectedKey="true"
/>
</div>
</StackItem>
</Stack>
</div>
<div
@@ -193,48 +239,51 @@ exports[`SmartUiComponent should render 1`] = `
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
"childrenGap": 15,
}
}
>
<StyledWithResponsiveMode
label="Database"
onChange={[Function]}
options={
Array [
Object {
"key": "db1",
"text": "database1",
},
Object {
"key": "db2",
"text": "database2",
},
Object {
"key": "db3",
"text": "database3",
},
]
}
selectedKey="db2"
styles={
Object {
"dropdown": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
"label": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
<StackItem>
<StyledWithResponsiveMode
id="database-dropown-input"
label="Database"
onChange={[Function]}
options={
Array [
Object {
"key": "db1",
"text": "Database 1",
},
Object {
"key": "db2",
"text": "Database 2",
},
Object {
"key": "db3",
"text": "Database 3",
},
]
}
}
/>
selectedKey="db2"
styles={
Object {
"dropdown": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
"label": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
}
}
/>
</StackItem>
</Stack>
</div>
</Stack>
</Fragment>
</Stack>
`;

View File

@@ -5,6 +5,9 @@ import ThroughputInputComponentAutoscaleV3 from "./ThroughputInputComponentAutos
import { KeyCodes } from "../../../Common/Constants";
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
import { userContext } from "../../../UserContext";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
/**
* Throughput Input:
*
@@ -129,6 +132,8 @@ export interface ThroughputInputParams {
showAutoPilot?: ko.Observable<boolean>;
overrideWithAutoPilotSettings: ko.Observable<boolean>;
overrideWithProvisionedThroughputSettings: ko.Observable<boolean>;
freeTierExceedThroughputTooltip?: ko.Observable<string>;
freeTierExceedThroughputWarning?: ko.Observable<string>;
}
export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
@@ -165,6 +170,10 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
public overrideWithProvisionedThroughputSettings: ko.Observable<boolean>;
public isManualThroughputInputFieldRequired: ko.Computed<boolean>;
public isAutoscaleThroughputInputFieldRequired: ko.Computed<boolean>;
public freeTierExceedThroughputTooltip: ko.Observable<string>;
public freeTierExceedThroughputWarning: ko.Observable<string>;
public showFreeTierExceedThroughputTooltip: ko.Computed<boolean>;
public showFreeTierExceedThroughputWarning: ko.Computed<boolean>;
public constructor(options: ThroughputInputParams) {
super();
@@ -195,6 +204,16 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
this.label = options.label || ko.observable<string>();
this.showAutoPilot = options.showAutoPilot !== undefined ? options.showAutoPilot : ko.observable<boolean>(true);
this.isAutoPilotSelected = options.isAutoPilotSelected || ko.observable<boolean>(false);
this.isAutoPilotSelected.subscribe(value => {
TelemetryProcessor.trace(Action.ToggleAutoscaleSetting, ActionModifiers.Mark, {
changedSelectedValueTo: value ? ActionModifiers.ToggleAutoscaleOn : ActionModifiers.ToggleAutoscaleOff,
databaseAccountName: userContext.databaseAccount?.name,
subscriptionId: userContext.subscriptionId,
apiKind: userContext.defaultExperience,
dataExplorerArea: "Scale Tab V1"
});
});
this.throughputAutoPilotRadioId = options.throughputAutoPilotRadioId;
this.throughputProvisionedRadioId = options.throughputProvisionedRadioId;
this.throughputModeRadioName = options.throughputModeRadioName;
@@ -219,6 +238,16 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
this.isAutoscaleThroughputInputFieldRequired = ko.pureComputed(
() => this.isEnabled() && this.isAutoPilotSelected()
);
this.freeTierExceedThroughputTooltip = options.freeTierExceedThroughputTooltip || ko.observable<string>();
this.freeTierExceedThroughputWarning = options.freeTierExceedThroughputWarning || ko.observable<string>();
this.showFreeTierExceedThroughputTooltip = ko.pureComputed<boolean>(
() => !!this.freeTierExceedThroughputTooltip() && this.value() > 400
);
this.showFreeTierExceedThroughputWarning = ko.pureComputed<boolean>(
() => !!this.freeTierExceedThroughputWarning() && this.value() > 400
);
}
public decreaseThroughput() {

View File

@@ -126,6 +126,20 @@
</div>
<div data-bind="visible: !isAutoPilotSelected()">
<p>
<span
>Estimate your required throughput with
<a target="_blank" href="https://cosmos.azure.com/capacitycalculator/">capacity calculator</a></span
>
</p>
<div class="inputTooltip">
<span
data-bind="text: freeTierExceedThroughputTooltip, visible: showFreeTierExceedThroughputTooltip"
class="inputTooltipText"
></span>
</div>
<div data-bind="setTemplateReady: true">
<input
data-bind="
@@ -148,6 +162,11 @@
/>
</div>
<div class="freeTierInlineWarning" data-bind="visible: showFreeTierExceedThroughputWarning">
<span class="freeTierWarningIcon"><img src="/warning.svg" alt="Warning"/></span>
<span class="freeTierWarningMessage" data-bind="text: freeTierExceedThroughputWarning"></span>
</div>
<p data-bind="visible: costsVisible">
<span data-bind="html: requestUnitsUsageCost"></span>
</p>

View File

@@ -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";

View File

@@ -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);
}
})
);
}
}

View File

@@ -18,7 +18,7 @@ import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPa
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
import EnvironmentUtility from "../Common/EnvironmentUtility";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
import GraphStylingPane from "./Panes/GraphStylingPane";
import hasher from "hasher";
import NewVertexPane from "./Panes/NewVertexPane";
@@ -88,6 +88,9 @@ import { stringToBlob } from "../Utils/BlobUtils";
import { IChoiceGroupProps } from "office-ui-fabric-react";
import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils";
import { SubscriptionType } from "../Contracts/SubscriptionType";
import { SelfServeLoadingComponentAdapter } from "../SelfServe/SelfServeLoadingComponentAdapter";
import { SelfServeType } from "../SelfServe/SelfServeUtils";
import { SelfServeComponentAdapter } from "../SelfServe/SelfServeComponentAdapter";
BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@@ -121,7 +124,6 @@ export default class Explorer {
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
public subscriptionType: ko.Observable<SubscriptionType>;
public quotaId: ko.Observable<string>;
public defaultExperience: ko.Observable<string>;
public isPreferredApiDocumentDB: ko.Computed<boolean>;
public isPreferredApiCassandra: ko.Computed<boolean>;
@@ -132,15 +134,14 @@ export default class Explorer {
public isEnableMongoCapabilityPresent: ko.Computed<boolean>;
public isServerlessEnabled: ko.Computed<boolean>;
public isAccountReady: ko.Observable<boolean>;
public selfServeType: ko.Observable<SelfServeType>;
public canSaveQueries: ko.Computed<boolean>;
public features: ko.Observable<any>;
public serverId: ko.Observable<string>;
public armEndpoint: ko.Observable<string>;
public isTryCosmosDBSubscription: ko.Observable<boolean>;
public queriesClient: QueriesClient;
public tableDataClient: TableDataClient;
public splitter: Splitter;
public parentFrameDataExplorerVersion: ko.Observable<string> = ko.observable<string>("");
public mostRecentActivity: MostRecentActivity.MostRecentActivity;
// Notification Console
@@ -159,6 +160,7 @@ export default class Explorer {
public selectedNode: ko.Observable<ViewModels.TreeNode>;
public isRefreshingExplorer: ko.Observable<boolean>;
private resourceTree: ResourceTreeAdapter;
private selfServeComponentAdapter: SelfServeComponentAdapter;
// Resource Token
public resourceTokenDatabaseId: ko.Observable<string>;
@@ -204,16 +206,15 @@ export default class Explorer {
// features
public isGalleryPublishEnabled: ko.Computed<boolean>;
public isCodeOfConductEnabled: ko.Computed<boolean>;
public isLinkInjectionEnabled: ko.Computed<boolean>;
public isSettingsV2Enabled: ko.Observable<boolean>;
public isMongoIndexEditorEnabled: ko.Observable<boolean>;
public isGitHubPaneEnabled: ko.Observable<boolean>;
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
public isRightPanelV2Enabled: ko.Computed<boolean>;
public isMongoIndexingEnabled: ko.Observable<boolean>;
public canExceedMaximumValue: ko.Computed<boolean>;
public isAutoscaleDefaultEnabled: ko.Observable<boolean>;
public shouldShowShareDialogContents: ko.Observable<boolean>;
public shareAccessData: ko.Observable<AdHocAccessData>;
@@ -263,6 +264,7 @@ export default class Explorer {
private _dialogProps: ko.Observable<DialogProps>;
private addSynapseLinkDialog: DialogComponentAdapter;
private _addSynapseLinkDialogProps: ko.Observable<DialogProps>;
private selfServeLoadingComponentAdapter: SelfServeLoadingComponentAdapter;
private static readonly MaxNbDatabasesToAutoExpand = 5;
@@ -281,7 +283,6 @@ export default class Explorer {
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType);
this.quotaId = ko.observable<string>("");
let firstInitialization = true;
this.isRefreshingExplorer = ko.observable<boolean>(true);
this.isRefreshingExplorer.subscribe((isRefreshing: boolean) => {
@@ -299,6 +300,7 @@ export default class Explorer {
}
});
this.isAccountReady = ko.observable<boolean>(false);
this.selfServeType = ko.observable<SelfServeType>(undefined);
this._isInitializingNotebooks = false;
this._isInitializingSparkConnectionInfo = false;
this.arcadiaToken = ko.observable<string>();
@@ -321,9 +323,9 @@ export default class Explorer {
if (isAccountReady) {
this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true);
RouteHandler.getInstance().initHandler();
this.notebookWorkspaceManager = new NotebookWorkspaceManager(this.armEndpoint());
this.notebookWorkspaceManager = new NotebookWorkspaceManager();
this.arcadiaWorkspaces = ko.observableArray();
this._arcadiaManager = new ArcadiaResourceManager(this.armEndpoint());
this._arcadiaManager = new ArcadiaResourceManager();
this._isAfecFeatureRegistered(Constants.AfecFeatures.StorageAnalytics).then(isRegistered =>
this.hasStorageAnalyticsAfecFeature(isRegistered)
);
@@ -373,7 +375,6 @@ export default class Explorer {
this.features = ko.observable();
this.serverId = ko.observable<string>();
this.armEndpoint = ko.observable<string>(undefined);
this.queriesClient = new QueriesClient(this);
this.isTryCosmosDBSubscription = ko.observable<boolean>(false);
@@ -406,15 +407,11 @@ export default class Explorer {
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
);
this.isCodeOfConductEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableCodeOfConduct)
);
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
);
this.isSettingsV2Enabled = ko.observable(false);
this.isMongoIndexEditorEnabled = ko.observable(false);
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
this.isMongoIndexingEnabled = ko.observable<boolean>(false);
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
@@ -425,6 +422,8 @@ export default class Explorer {
this.isSchemaEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema));
this.isNotificationConsoleExpanded = ko.observable<boolean>(false);
this.isAutoscaleDefaultEnabled = ko.observable<boolean>(false);
this.databases = ko.observableArray<ViewModels.Database>();
this.canSaveQueries = ko.computed<boolean>(() => {
const savedQueriesDatabase: ViewModels.Database = _.find(
@@ -706,6 +705,7 @@ export default class Explorer {
});
this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this);
this.selfServeComponentAdapter = new SelfServeComponentAdapter(this);
this.loadQueryPane = new LoadQueryPane({
id: "loadquerypane",
@@ -881,6 +881,7 @@ export default class Explorer {
});
this.commandBarComponentAdapter = new CommandBarComponentAdapter(this);
this.selfServeLoadingComponentAdapter = new SelfServeLoadingComponentAdapter();
this.notificationConsoleComponentAdapter = new NotificationConsoleComponentAdapter(this);
this._initSettings();
@@ -1020,9 +1021,7 @@ export default class Explorer {
this.isSynapseLinkUpdating(true);
this._closeSynapseLinkModalDialog();
const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(
this.databaseAccount().id
);
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(this.databaseAccount().id);
try {
const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync(
@@ -1734,6 +1733,7 @@ export default class Explorer {
case MessageTypes.SendNotification:
case MessageTypes.ClearNotification:
case MessageTypes.LoadingStatus:
case MessageTypes.InitTestExplorer:
return true;
}
}
@@ -1761,61 +1761,59 @@ export default class Explorer {
inputs.extensionEndpoint = configContext.PROXY_PATH;
}
const initPromise: Q.Promise<void> = inputs ? this.initDataExplorerWithFrameInputs(inputs) : Q();
this.initDataExplorerWithFrameInputs(inputs);
initPromise.then(() => {
const openAction: ActionContracts.DataExplorerAction = message.openAction;
if (!!openAction) {
if (this.isRefreshingExplorer()) {
const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => {
handleOpenAction(openAction, this.nonSystemDatabases(), this);
subscription.dispose();
});
} else {
const openAction: ActionContracts.DataExplorerAction = message.openAction;
if (!!openAction) {
if (this.isRefreshingExplorer()) {
const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => {
handleOpenAction(openAction, this.nonSystemDatabases(), this);
}
subscription.dispose();
});
} else {
handleOpenAction(openAction, this.nonSystemDatabases(), this);
}
if (message.actionType === ActionContracts.ActionType.TransmitCachedData) {
handleCachedDataMessage(message);
return;
}
if (message.type) {
switch (message.type) {
case MessageTypes.UpdateLocationHash:
if (!message.locationHash) {
break;
}
hasher.replaceHash(message.locationHash);
RouteHandler.getInstance().parseHash(message.locationHash);
break;
case MessageTypes.SendNotification:
if (!message.message) {
break;
}
NotificationConsoleUtils.logConsoleMessage(
message.consoleDataType || ConsoleDataType.Info,
message.message,
message.id
);
break;
case MessageTypes.ClearNotification:
if (!message.id) {
break;
}
NotificationConsoleUtils.clearInProgressMessageWithId(message.id);
break;
case MessageTypes.LoadingStatus:
if (!message.text) {
break;
}
this._setLoadingStatusText(message.text, message.title);
break;
}
return;
}
if (message.actionType === ActionContracts.ActionType.TransmitCachedData) {
handleCachedDataMessage(message);
return;
}
if (message.type) {
switch (message.type) {
case MessageTypes.UpdateLocationHash:
if (!message.locationHash) {
break;
}
hasher.replaceHash(message.locationHash);
RouteHandler.getInstance().parseHash(message.locationHash);
break;
case MessageTypes.SendNotification:
if (!message.message) {
break;
}
NotificationConsoleUtils.logConsoleMessage(
message.consoleDataType || ConsoleDataType.Info,
message.message,
message.id
);
break;
case MessageTypes.ClearNotification:
if (!message.id) {
break;
}
NotificationConsoleUtils.clearInProgressMessageWithId(message.id);
break;
case MessageTypes.LoadingStatus:
if (!message.text) {
break;
}
this._setLoadingStatusText(message.text, message.title);
break;
}
return;
}
this.splashScreenAdapter.forceRender();
});
this.splashScreenAdapter.forceRender();
}
public findSelectedDatabase(): ViewModels.Database {
@@ -1855,8 +1853,28 @@ export default class Explorer {
return false;
}
public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): Q.Promise<void> {
public setSelfServeType(inputs: ViewModels.DataExplorerInputsFrame): void {
const selfServeFeature = inputs.features[Constants.Features.selfServeType];
if (selfServeFeature) {
// self serve type received from query string
const selfServeType = SelfServeType[selfServeFeature?.toLowerCase() as keyof typeof SelfServeType];
this.selfServeType(selfServeType ? selfServeType : SelfServeType.invalid);
} else if (inputs.selfServeType) {
// self serve type received from portal
this.selfServeType(inputs.selfServeType);
} else {
this.selfServeType(SelfServeType.none);
}
}
public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): void {
if (inputs != null) {
// In development mode, save the iframe message from the portal in session storage.
// This allows webpack hot reload to funciton properly
if (process.env.NODE_ENV === "development") {
sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs));
}
const authorizationToken = inputs.authorizationToken || "";
const masterKey = inputs.masterKey || "";
const databaseAccount = inputs.databaseAccount || null;
@@ -1865,25 +1883,19 @@ export default class Explorer {
}
this.features(inputs.features);
this.serverId(inputs.serverId);
this.armEndpoint(EnvironmentUtility.normalizeArmEndpointUri(inputs.csmEndpoint || configContext.ARM_ENDPOINT));
this.databaseAccount(databaseAccount);
this.subscriptionType(inputs.subscriptionType);
this.quotaId(inputs.quotaId);
this.hasWriteAccess(inputs.hasWriteAccess);
this.flight(inputs.addCollectionDefaultFlight);
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription);
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken);
this.setFeatureFlagsFromFlights(inputs.flights);
if (!!inputs.dataExplorerVersion) {
this.parentFrameDataExplorerVersion(inputs.dataExplorerVersion);
}
this.setSelfServeType(inputs);
this._importExplorerConfigComplete = true;
updateConfigContext({
BACKEND_ENDPOINT: inputs.extensionEndpoint || "",
ARM_ENDPOINT: this.armEndpoint()
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT)
});
updateUserContext({
@@ -1892,7 +1904,8 @@ export default class Explorer {
databaseAccount,
resourceGroup: inputs.resourceGroup,
subscriptionId: inputs.subscriptionId,
subscriptionType: inputs.subscriptionType
subscriptionType: inputs.subscriptionType,
quotaId: inputs.quotaId
});
TelemetryProcessor.traceSuccess(
Action.LoadDatabaseAccount,
@@ -1906,20 +1919,17 @@ export default class Explorer {
this.isAccountReady(true);
}
return Q();
}
public setFeatureFlagsFromFlights(flights: readonly string[]): void {
if (!flights) {
return;
}
if (flights.indexOf(Constants.Flights.SettingsV2) !== -1) {
this.isSettingsV2Enabled(true);
if (flights.indexOf(Constants.Flights.AutoscaleTest) !== -1) {
this.isAutoscaleDefaultEnabled(true);
}
if (flights.indexOf(Constants.Flights.MongoIndexEditor) !== -1) {
this.isMongoIndexEditorEnabled(true);
if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) {
this.isMongoIndexingEnabled(true);
}
}
@@ -2287,7 +2297,6 @@ export default class Explorer {
name,
content,
parentDomElement,
this.isCodeOfConductEnabled(),
this.isLinkInjectionEnabled()
);
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
@@ -2379,11 +2388,13 @@ export default class Explorer {
this.tabsManager.activateTab(notebookTab);
} else {
const options: NotebookTabOptions = {
account: userContext.databaseAccount,
tabKind: ViewModels.CollectionTabKind.NotebookV2,
node: null,
title: notebookContentItem.name,
tabPath: notebookContentItem.path,
collection: null,
masterKey: userContext.masterKey || "",
hashLocation: "notebooks",
isActive: ko.observable(false),
isTabsContentExpanded: ko.observable(true),
@@ -2576,7 +2587,7 @@ export default class Explorer {
public _refreshSparkEnabledStateForAccount = async (): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const armEndpoint = this.armEndpoint();
const armEndpoint = configContext.ARM_ENDPOINT;
const authType = window.authType as AuthType;
if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
// explorer is not aware of the database account yet
@@ -2585,7 +2596,7 @@ export default class Explorer {
}
const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${Constants.AfecFeatures.Spark}`;
const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(featureUri);
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri);
try {
const sparkNotebooksFeature: DataModels.AfecFeature = await resourceProviderClient.getAsync(
featureUri,
@@ -2605,7 +2616,7 @@ export default class Explorer {
public _isAfecFeatureRegistered = async (featureName: string): Promise<boolean> => {
const subscriptionId = userContext.subscriptionId;
const armEndpoint = this.armEndpoint();
const armEndpoint = configContext.ARM_ENDPOINT;
const authType = window.authType as AuthType;
if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
// explorer is not aware of the database account yet
@@ -2613,7 +2624,7 @@ export default class Explorer {
}
const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${featureName}`;
const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(featureUri);
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri);
try {
const featureStatus: DataModels.AfecFeature = await resourceProviderClient.getAsync(
featureUri,
@@ -3038,4 +3049,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;
});
}
}

View File

@@ -11,7 +11,9 @@ export class ArraysByKeyCache<T> {
public constructor(maxNbElements: number) {
this.maxNbElements = maxNbElements;
this.clear();
this.keyQueue = [];
this.cache = {};
this.totalElements = 0;
}
public clear(): void {
@@ -58,7 +60,7 @@ export class ArraysByKeyCache<T> {
* @param startIndex
* @param pageSize
*/
public retrieve(key: string, startIndex: number, pageSize: number): T[] {
public retrieve(key: string, startIndex: number, pageSize: number): T[] | null {
if (!this.cache.hasOwnProperty(key)) {
return null;
}
@@ -77,8 +79,10 @@ export class ArraysByKeyCache<T> {
private reduceCacheSize(): void {
// remove an key and its array
const oldKey = this.keyQueue.shift();
this.totalElements -= this.cache[oldKey].length;
delete this.cache[oldKey];
if (oldKey) {
this.totalElements -= this.cache[oldKey].length;
delete this.cache[oldKey];
}
}
/**

View File

@@ -413,13 +413,13 @@ export class GraphData<V extends GremlinVertex, E extends GremlinEdge> {
* @param node
* @param prop
*/
public static getNodePropValue(node: D3Node, prop: string): string | number | boolean {
public static getNodePropValue(node: D3Node, prop: string): undefined | string | number | boolean {
if (node.hasOwnProperty(prop)) {
return (node as any)[prop];
}
// This is DocDB specific
if (node.hasOwnProperty("properties") && node.properties.hasOwnProperty(prop)) {
if (node.properties && node.properties.hasOwnProperty(prop)) {
return node.properties[prop][0]["value"];
}
@@ -496,7 +496,7 @@ export class GraphData<V extends GremlinVertex, E extends GremlinEdge> {
* Get list of children ids of a given vertex
* @param vertex
*/
private static getChildrenId(vertex: GremlinVertex): string[] {
public static getChildrenId(vertex: GremlinVertex): string[] {
const ids = <any>{}; // HashSet
if (vertex.hasOwnProperty("outE")) {
let outE = vertex.outE;

View File

@@ -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) => {

View File

@@ -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<GraphExplorerProps, GraphExpl
/**
* Execute DocDB query and get all results
*/
public executeNonPagedDocDbQuery(query: string): Q.Promise<DataModels.DocumentId[]> {
// 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<ItemDefinition & Resource>) => {
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<DataModels.DocumentId[]> {
try {
// TODO maxItemCount: this reduces throttling, but won't cap the # of results
const iterator: QueryIterator<ItemDefinition & Resource> = 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<GraphExplorerProps, GraphExpl
/**
* User executes query
*/
public submitQuery(query: string): void {
public async submitQuery(query: string): Promise<void> {
// Clear any progress indicator
this.executeCounter = 0;
this.setState({
@@ -882,24 +890,22 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// Remember query
this.pushToLatestQueryFragments(query);
let backendPromise;
if (query.toLocaleLowerCase() === "g.V()".toLocaleLowerCase()) {
backendPromise = this.executeDocDbGVQuery();
} else {
backendPromise = this.executeGremlinQuery(query);
}
backendPromise.then(
(result: UserQueryResult) => (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<GraphExplorerProps, GraphExpl
/**
* Update possible vertices to display in UI
*/
private updatePossibleVertices(): Q.Promise<PossibleVertex[]> {
private updatePossibleVertices(): Promise<PossibleVertex[]> {
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<GraphExplorerProps, GraphExpl
);
}
private executeDocDbGVQuery(): Q.Promise<UserQueryResult> {
private async executeDocDbGVQuery(): Promise<UserQueryResult> {
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<ItemDefinition & Resource>) => {
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<ItemDefinition & Resource> = 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<UserQueryResult> {
private async loadMoreRootNodes(): Promise<UserQueryResult> {
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<UserQueryResult> {

View File

@@ -8,7 +8,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
let mockExplorer: Explorer;
describe("Enable Azure Synapse Link Button", () => {
const enableAzureSynapseLinkBtnLabel = "Enable Azure Synapse Link (Preview)";
const enableAzureSynapseLinkBtnLabel = "Enable Azure Synapse Link";
beforeAll(() => {
mockExplorer = {} as Explorer;

View File

@@ -269,7 +269,7 @@ export class CommandBarComponentButtonFactory {
return null;
}
const label = "Enable Azure Synapse Link (Preview)";
const label = "Enable Azure Synapse Link";
return {
iconSrc: SynapseIcon,
iconAlt: label,

View File

@@ -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 {

View File

@@ -5,14 +5,14 @@
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";
import InfoIcon from "../../../../images/info_color.svg";
import ErrorRedIcon from "../../../../images/error_red.svg";
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
import ClearIcon from "../../../../images/Clear.svg";
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png";
import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
@@ -53,16 +53,21 @@ export class NotificationConsoleComponent extends React.Component<
NotificationConsoleComponentState
> {
private static readonly transitionDurationMs = 200;
private static readonly FilterOptions = ["All", "In Progress", "Info", "Error"];
private headerTimeoutId: number;
private prevHeaderStatus: string;
private consoleHeaderElement: HTMLElement;
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 | null;
private consoleHeaderElement?: HTMLElement;
constructor(props: NotificationConsoleComponentProps) {
super(props);
this.state = {
headerStatus: "",
selectedFilter: NotificationConsoleComponent.FilterOptions[0],
selectedFilter: NotificationConsoleComponent.FilterOptions[0].key || "",
isExpanded: props.isConsoleExpanded
};
this.prevHeaderStatus = null;
@@ -94,6 +99,10 @@ export class NotificationConsoleComponent extends React.Component<
}
}
public setElememntRef = (element: HTMLElement) => {
this.consoleHeaderElement = element;
};
public render(): JSX.Element {
const numInProgress = this.props.consoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.InProgress)
.length;
@@ -105,7 +114,7 @@ export class NotificationConsoleComponent extends React.Component<
<div className="notificationConsoleContainer">
<div
className="notificationConsoleHeader"
ref={(element: HTMLElement) => (this.consoleHeaderElement = element)}
ref={this.setElememntRef}
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.expandCollapseConsole()}
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
tabIndex={0}
@@ -150,20 +159,15 @@ export class NotificationConsoleComponent extends React.Component<
>
<div className="notificationConsoleContents">
<div className="notificationConsoleControls">
<label id="consoleFilterLabel">Filter</label>
<select
aria-labelledby="consoleFilterLabel"
<Dropdown
label="Filter:"
role="combobox"
aria-label={this.state.selectedFilter}
value={this.state.selectedFilter}
selectedKey={this.state.selectedFilter}
options={NotificationConsoleComponent.FilterOptions}
onChange={this.onFilterSelected.bind(this)}
>
{NotificationConsoleComponent.FilterOptions.map((value: string) => (
<option value={value} key={value}>
{value}
</option>
))}
</select>
aria-labelledby="consoleFilterLabel"
aria-label={this.state.selectedFilter}
/>
<span className="consoleSplitter" />
<span
className="clearNotificationsButton"
@@ -220,12 +224,12 @@ export class NotificationConsoleComponent extends React.Component<
));
}
private onFilterSelected(event: React.ChangeEvent<HTMLSelectElement>): void {
this.setState({ selectedFilter: event.target.value });
}
private onFilterSelected = (event: React.ChangeEvent<HTMLSelectElement>, option: IDropdownOption): void => {
this.setState({ selectedFilter: String(option.key) });
};
private getFilteredConsoleData(): ConsoleData[] {
let filterType: ConsoleDataType = null;
let filterType: ConsoleDataType | null = null;
switch (this.state.selectedFilter) {
case "All":
@@ -272,7 +276,7 @@ export class NotificationConsoleComponent extends React.Component<
private onConsoleWasExpanded = (): void => {
this.props.onConsoleExpandedChange(this.state.isExpanded);
if (this.state.isExpanded) {
if (this.state.isExpanded && this.consoleHeaderElement) {
this.consoleHeaderElement.focus();
}
};

View File

@@ -110,43 +110,34 @@ exports[`NotificationConsoleComponent renders the console (expanded) 1`] = `
<div
className="notificationConsoleControls"
>
<label
id="consoleFilterLabel"
>
Filter
</label>
<select
<StyledWithResponsiveMode
aria-label="All"
aria-labelledby="consoleFilterLabel"
label="Filter:"
onChange={[Function]}
options={
Array [
Object {
"key": "All",
"text": "All",
},
Object {
"key": "In Progress",
"text": "In progress",
},
Object {
"key": "Info",
"text": "Info",
},
Object {
"key": "Error",
"text": "Error",
},
]
}
role="combobox"
value="All"
>
<option
key="All"
value="All"
>
All
</option>
<option
key="In Progress"
value="In Progress"
>
In Progress
</option>
<option
key="Info"
value="Info"
>
Info
</option>
<option
key="Error"
value="Error"
>
Error
</option>
</select>
selectedKey="All"
/>
<span
className="consoleSplitter"
/>

View File

@@ -1,23 +0,0 @@
.mongoQueryComponent {
margin-left: 10px;
input {
margin-top: 0;
}
label {
padding: 0;
margin-bottom: 0;
}
label:before {
top: 2px;
left: 2px;
height: 16px;
width: 16px;
}
.queryInput {
border: 1px solid black;
margin: 5px;
}
}

View File

@@ -1,224 +0,0 @@
import * as React from "react";
import { Dispatch } from "redux";
import MonacoEditor from "@nteract/monaco-editor";
import { PrimaryButton } from "office-ui-fabric-react";
import { ChoiceGroup, IChoiceGroupOption } from "office-ui-fabric-react/lib/ChoiceGroup";
import Outputs from "@nteract/stateful-components/lib/outputs";
import { KernelOutputError, StreamText } from "@nteract/outputs";
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
import { actions, selectors, AppState, ContentRef, KernelRef } from "@nteract/core";
import loadTransform from "../NotebookComponent/loadTransform";
import { connect } from "react-redux";
import Immutable from "immutable";
import "./MongoQueryComponent.less";
interface MongoQueryComponentPureProps {
contentRef: ContentRef;
kernelRef: KernelRef;
databaseId: string;
collectionId: string;
}
interface MongoQueryComponentDispatchProps {
runCell: (contentRef: ContentRef, cellId: string) => void;
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void;
onChange: (text: string, id: string, contentRef: ContentRef) => void;
save: (contentRef: ContentRef) => void;
}
type OutputType = "rich" | "json";
interface MongoQueryComponentState {
outputType: OutputType;
selectedId: string;
}
const options: IChoiceGroupOption[] = [
{ key: "rich", text: "Rich Output" },
{ key: "json", text: "Json Output" }
];
interface MongoKernelJsonOutput {
results: any;
}
interface MongoDocument {
id: string;
}
type MongoQueryComponentProps = MongoQueryComponentPureProps & StateProps & MongoQueryComponentDispatchProps;
export class MongoQueryComponent extends React.Component<MongoQueryComponentProps, MongoQueryComponentState> {
constructor(props: MongoQueryComponentProps) {
super(props);
this.state = {
outputType: "json",
selectedId: undefined
};
}
componentDidMount(): void {
loadTransform(this.props);
}
private onExecute = () => {
this.props.runCell(this.props.contentRef, this.props.firstCellId);
this.props.save(this.props.contentRef);
};
/**
*
* @param databaseId
* @param collectionId
* @param query e.g. { "lastName": { $in: ["Andersen"] } }
*/
private createFilterQuery(databaseId: string, collectionId: string, query: string): string {
const newCommand = `{ "command": "filter", "database": "${databaseId}", "collection": "${collectionId}", "filter": ${JSON.stringify(query)}, "outputType": "${this.state.outputType}" }`;
return newCommand;
}
private onOutputTypeChange = (e: React.FormEvent<HTMLElement | HTMLInputElement>, option: IChoiceGroupOption): void => {
const outputType = option.key as OutputType;
this.setState({ outputType }, () => this.onInputChange(this.props.inputValue));
};
private onInputChange = (text: string) => {
this.props.onChange(this.createFilterQuery(this.props.databaseId, this.props.collectionId, text),
this.props.firstCellId, this.props.contentRef);
};
render(): JSX.Element {
const { firstCellId: id, contentRef, outputDocuments } = this.props;
if (!id) {
return <></>;
}
return (
<div className="mongoQueryComponent">
<div className="queryInput">
<MonacoEditor id={this.props.firstCellId} contentRef={this.props.contentRef} theme={""}
language="json" onChange={this.onInputChange}
value={this.props.inputValue} />
</div>
<PrimaryButton text="Apply" onClick={this.onExecute} disabled={!this.props.firstCellId} />
<ChoiceGroup
selectedKey={this.state.outputType}
options={options}
onChange={this.onOutputTypeChange}
label="Output Type"
styles={{ input: { marginTop: 0 }, root: { marginTop: 0 } }}
/>
<hr />
<div style={ { display: "flex" } }>
<ul>
{outputDocuments && outputDocuments.map(d => (
<li key={d.id}>
<a onClick={() => this.setState({ selectedId: id })}>{d.id}</a>
</li>
))}
</ul>
<div style={{ width: "100%" }} >
<MonacoEditor id={""} contentRef={""} theme={""} language="json" onChange={() => {}}
value={JSON.stringify(outputDocuments.find(doc => doc.id ===this.state.selectedId)) ?? ""} />
</div>
</div>
<hr />
<Outputs id={id} contentRef={contentRef}>
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
<KernelOutputError />
<StreamText />
</Outputs>
</div>
);
}
}
interface StateProps {
firstCellId: string;
inputValue: string;
outputDocuments: MongoDocument[];
}
interface InitialProps {
contentRef: string;
}
// Redux
const makeMapStateToProps = (state: AppState, initialProps: InitialProps) => {
const { contentRef } = initialProps;
const mapStateToProps = (state: AppState) => {
let firstCellId;
let inputValue = "";
let outputDocuments = [];
const content = selectors.content(state, { contentRef });
if (content?.type === "notebook") {
const cellOrder = selectors.notebook.cellOrder(content.model);
if (cellOrder.size > 0) {
firstCellId = cellOrder.first() as string;
const cell = selectors.notebook.cellById(content.model, { id: firstCellId });
// Parse to extract filter and output type
const cellValue = cell.get("source", "");
if (cellValue) {
try {
const filterValue = JSON.parse(cellValue).filter;
if (filterValue) {
inputValue = filterValue;
}
} catch(e) {
console.error("Could not parse", e);
}
}
const outputs = cell.get("outputs", Immutable.List());
// Extract "application/json" mime-type
let jsonOutput: MongoKernelJsonOutput;
for (const output of outputs) {
if (Object.prototype.hasOwnProperty.call(output.data, "application/json")) {
jsonOutput = output.data["application/json"];
break;
}
}
outputDocuments = jsonOutput?.results ?? [];
}
}
return {
firstCellId,
inputValue,
outputDocuments
};
};
return mapStateToProps;
};
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: MongoQueryComponentProps) => {
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
return dispatch(
actions.addTransform({
mediaType: transform.MIMETYPE,
component: transform
})
);
},
runCell: (contentRef: ContentRef, cellId: string) => {
return dispatch(
actions.executeCell({
contentRef,
id: cellId
})
);
},
onChange: (text: string, id: string, contentRef: ContentRef) => {
dispatch(actions.updateCellSource({ id, contentRef, value: text }));
},
save: (contentRef: ContentRef) => {
dispatch(actions.save({ contentRef }));
}
};
};
return mapDispatchToProps;
};
export default connect(makeMapStateToProps, makeMapDispatchToProps)(MongoQueryComponent);

View File

@@ -1,49 +0,0 @@
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import {
NotebookComponentBootstrapper,
NotebookComponentBootstrapperOptions
} from "../NotebookComponent/NotebookComponentBootstrapper";
import MongoQueryComponent from "../MongoQueryComponent/MongoQueryComponent";
import { actions, createContentRef, createKernelRef, KernelRef } from "@nteract/core";
import { Provider } from "react-redux";
export class MongoQueryComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
public parameters: unknown;
private kernelRef: KernelRef;
constructor(options: NotebookComponentBootstrapperOptions, private databaseId: string, private collectionId: string) {
super(options);
if (!this.contentRef) {
this.contentRef = createContentRef();
this.kernelRef = createKernelRef();
// Request fetching notebook content
this.getStore().dispatch(
actions.fetchContent({
filepath: "mongo.ipynb",
params: {},
kernelRef: this.kernelRef,
contentRef: this.contentRef
})
);
}
}
public renderComponent(): JSX.Element {
const props = {
contentRef: this.contentRef,
kernelRef: this.kernelRef,
databaseId: this.databaseId,
collectionId: this.collectionId
};
return (
<Provider store={this.getStore()}>
<MongoQueryComponent {...props} />;
</Provider>
);
}
}

View File

@@ -1,18 +1,17 @@
import { Observable, of } from "rxjs";
import { AjaxResponse } from "rxjs/ajax";
import { ServerConfig } from "rx-jupyter";
import { AjaxRequest, AjaxResponse } from "rxjs/ajax";
let fakeAjaxResponse: AjaxResponse = {
originalEvent: undefined,
originalEvent: <Event>(<unknown>undefined),
xhr: new XMLHttpRequest(),
request: null,
request: <AjaxRequest>(<unknown>null),
status: 200,
response: {},
responseText: null,
responseText: "",
responseType: "json"
};
export const sessions = {
create: (serverConfig: ServerConfig, body: object): Observable<AjaxResponse> => of(fakeAjaxResponse),
create: (serverConfig: unknown, body: object): Observable<AjaxResponse> => of(fakeAjaxResponse),
__setResponse: (response: AjaxResponse) => {
fakeAjaxResponse = response;
},

Some files were not shown because too many files have changed in this diff Show More