Compare commits

...

51 Commits

Author SHA1 Message Date
Tanuj Mittal
9aeb349d74 Sandbox HTML and JavaScript outputs in iFrame 2021-04-02 17:27:17 -07:00
Hardikkumar Nai
d8fe4ed77f Fix lint file system util (#481)
Co-authored-by: Steve Faulkner <471400+southpolesteve@users.noreply.github.com>
2021-03-31 22:28:16 -05:00
Sunil Kumar Yadav
75ea475217 [WIP]Cleanup/removed knockout database confirmation panel (#546)
* complete delete database component ui in react

* fixed functional issue and added feedback input

* test cases for deleteDatabaseConfirmationPanel

* Removed Q and fixed PR change request

* removed knockout database confirmation panel and references

* delete deleteDatabaseConfirmationPane.html

* remove test

Co-authored-by: Steve Faulkner <471400+southpolesteve@users.noreply.github.com>
2021-03-31 19:58:38 -05:00
Steve Faulkner
dc20aa96d2 Remove accidentally checked in screenshots 2021-03-31 19:49:45 -05:00
Hardikkumar Nai
5307f6bb5b Migrate Notebook Upload File to React (#581)
Co-authored-by: Steve Faulkner <471400+southpolesteve@users.noreply.github.com>
2021-03-31 19:25:45 -05:00
Sunil Kumar Yadav
c68e84a4b9 Migration/delete database confirmation in react (#542)
Co-authored-by: Steve Faulkner <471400+southpolesteve@users.noreply.github.com>
Co-authored-by: Steve Faulkner <stfaul@microsoft.com>
2021-03-31 17:44:07 -05:00
Hardikkumar Nai
6a69d3a77b Move upload items panel to react (#558)
Co-authored-by: Steve Faulkner <471400+southpolesteve@users.noreply.github.com>
2021-03-31 15:43:05 -05:00
Hardikkumar Nai
458cca8e01 Move setting pane to react (#543)
Co-authored-by: Steve Faulkner <471400+southpolesteve@users.noreply.github.com>
2021-03-31 15:22:52 -05:00
Sunil Kumar Yadav
69ac4e218d Migration/execute sproc params pane in react (#576)
Co-authored-by: Steve Faulkner <471400+southpolesteve@users.noreply.github.com>
2021-03-31 14:43:55 -05:00
Steve Faulkner
b1a904a98f Remove TelemetrtData Type Restriction (#595) 2021-03-30 16:35:45 -05:00
Jordi Bunster
813dbfee5b Defensively set feature flags from window.parent (#594)
This doesn't really fix the fact that portal feature flags are not
being set, but it does re-enable feature flags in hosted mode.
2021-03-30 14:01:30 -07:00
Jordi Bunster
a9ed187213 Bugfix: this was not really used at all (#596) 2021-03-30 13:59:04 -07:00
Srinath Narayanan
6cdac3c53b Added support for self serve telemetry + Localization fixes (#580)
* initial telemetry commit

* Added localization changes

* moved telemetrymessage types to selfservetypes

* fixed compile errors

* fixed failing test

* changed translation file format

* Addressed PR comments

* modified test
2021-03-30 10:11:43 -07:00
Steve Faulkner
63e13cdabe Remove Explorer.nonSystemDatabases (#538)
* Remove Explorer.nonSystemDatabases

* Fix tests
2021-03-30 09:31:21 -05:00
Steve Faulkner
c9eb61351a Remove unused inline-css package (#593) 2021-03-29 21:56:25 -05:00
Hardikkumar Nai
343e82c102 Fix lint query utils (#487)
* Fix Lint errors in QueryUtils

* Format

* Simplify diff

Co-authored-by: Your Name <you@example.com>
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
Co-authored-by: Steve Faulkner <471400+southpolesteve@users.noreply.github.com>
Co-authored-by: Jordi Bunster <jbunster@microsoft.com>
2021-03-29 21:26:41 -05:00
dependabot[bot]
fad3a08fdf Bump node-fetch from 2.6.0 to 2.6.1 in /utils/deployment-status (#587)
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-29 21:22:31 -05:00
vaidankarswapnil
72c2d8592b Fix lint for Database Account Utils (#583) 2021-03-29 17:13:35 -05:00
Jordi Bunster
6b73560122 Create dependabot.yml 2021-03-29 15:09:18 -07:00
Steve Faulkner
9108c01e62 Drop IE11 Support (#476) 2021-03-29 13:31:52 -05:00
Jordi Bunster
4c2f22c2b1 When switching between tabs, editors remain open (#586)
I misunderstood the purpose of a ko.foreach() loop in my changes
back in #534. This re-introduces the loop.
2021-03-29 12:38:41 -05:00
Jordi Bunster
f82b0b442e Update README.md 2021-03-27 11:18:59 -07:00
Armando Trejo Oliver
a66f042c10 Fix table api query projections (#584)
When building queries with projections, the resulting query does not include the "id" property as part of the projection. The "id" property is used by the results grid to display as the RowKey so the result is queries wih projections do not show the RowKey.

This change fixes this by including "id" as part of the projections.
2021-03-26 12:18:04 -07:00
Jordi Bunster
8cc04bab87 MongoDocumentsTab.html was not used here (#582) 2021-03-25 15:24:43 -07:00
victor-meng
ca7cd139ba Use window instead of window.parent in extractFeatures function (#577) 2021-03-23 17:21:41 -07:00
Jordi Bunster
f33ec09040 Remove Explorer.features (#563)
In addition this makes the URL-passed feature flags type safe
2021-03-22 19:04:06 +00:00
Jordi Bunster
b1aeab6b84 Knockout tab changes
This reduces the work involved in rendering knockout tabs, which will make moving them to React under a feature flag a bit easier.
2021-03-22 10:13:44 -07:00
Steve Faulkner
8bf976026f Pass masterkey in connection string mode (#572) 2021-03-19 18:37:13 -05:00
Steve Faulkner
c7ba5de90d Fix Test Explorer AAD Authority (#571) 2021-03-19 14:45:58 -05:00
Steve Faulkner
ddf59d6b24 Fix SplashScreen/Tabs Visible bug (#570) 2021-03-19 12:53:34 -05:00
Tanuj Mittal
316fe7e8bb Reset cell status after execution is canceled (#564) 2021-03-19 22:55:54 +05:30
Steve Faulkner
ee8d2070bf Remove Explorer.isRefreshing (#539) 2021-03-19 12:13:52 -05:00
Armando Trejo Oliver
e97a1643fb Make ready message backwards compatible (#569) 2021-03-19 08:38:57 -07:00
Srinath Narayanan
049e3c36d8 Dedicated gateway portal changes (#568)
* Portal changes for DedicatedGateway

Changes to support creation and deletion of DedicatedGateway resource.

Tested locally with various scenarios.

* Portal changes for DedicatedGateway. CR feedback

* Stylecop changes

* Removing TODO comments

* exposed baselineValues

* added getOnSaveNotification

* disable UI when onSave is taking place

* minro edits

* made polling optional

* added optional polling

* added default

* Added portal notifications

* merged more changes

* minor edits

* added label for description

* Added correlationids and polling of refresh

* Added correlationids and polling of refresh

* minor edit

* added label tooltip

* removed ClassInfo decorator

* Added dynamic decription

* added info and warninf types for description

* more changes to promise retry

* promise retry changes

* compile errors fixed

* New changes

* added operationstatus link

* merged sqlxEdits

* undid sqlx changes

* added completed notification

* passed retryInterval in notif options

* more changes

* added polling on landing on the page

* edits for error display

* added keys blade link

* added link generation

* added link to blade

* Modified info and description

* fixed format errors

* Second cut of the Portal

* OnChange for Number of instances

* added keys for texts

* fixed lint errors

* Added support for undefined dynamic description

* fixed failing test

* disable save/discard buttons

* fixed sqlx errors

* Dedicated Gateway changes to add the keys blade

* Change connectionStringText

* Change connectionStringText

* Text changes

* Added UI improvements

* Code review feedback

* undid package lock changes

* updated package.json

* undid package reverts

Co-authored-by: Balaji Sridharan <fnbalaji@microsoft.com>
Co-authored-by: fnbalaji <75445927+fnbalaji@users.noreply.github.com>
2021-03-19 00:54:13 -07:00
Srinath Narayanan
159c297e8d removed change to package.json (#567) 2021-03-19 00:05:15 -07:00
Srinath Narayanan
4e09e4c7fa Revert "Dedicated Gateway Portal Changes (#540)" (#566)
This reverts commit 909a9fa522.
2021-03-19 00:01:15 -07:00
Armando Trejo Oliver
19880203ec Fix ready message (format) (#565)
* Fix ready message sent to parent frame

* format
2021-03-18 21:40:31 -07:00
artrejo
f929a638d6 Fix ready message sent to parent frame 2021-03-18 20:41:43 -07:00
Steve Faulkner
3cccbdfe81 Fix Lint errors in URLUtility (#462) 2021-03-18 22:19:35 -05:00
victor-meng
65c859c835 Move add collection pane to React (#486)
* Move add collection pane to React

* Add feature flag

* fix unit tests

* FIx merge conflicts and address comments

* Resolve merge conflicts

* Address comments

* Fix e2e test failure

* Update test snapshots

* Update test snapshots
2021-03-18 20:06:13 -05:00
Steve Faulkner
c6090e2663 Update Puppeteer (#562) 2021-03-18 17:53:15 -05:00
Steve Faulkner
c43e24061c [Tables] Check for undefined before compare (#561) 2021-03-18 16:16:10 -05:00
fnbalaji
909a9fa522 Dedicated Gateway Portal Changes (#540)
* Portal changes for DedicatedGateway

Changes to support creation and deletion of DedicatedGateway resource.

Tested locally with various scenarios.

* Portal changes for DedicatedGateway. CR feedback

* Stylecop changes

* Removing TODO comments

* exposed baselineValues

* added getOnSaveNotification

* disable UI when onSave is taking place

* minro edits

* made polling optional

* added optional polling

* added default

* Added portal notifications

* merged more changes

* minor edits

* added label for description

* Added correlationids and polling of refresh

* Added correlationids and polling of refresh

* minor edit

* added label tooltip

* removed ClassInfo decorator

* Added dynamic decription

* added info and warninf types for description

* more changes to promise retry

* promise retry changes

* compile errors fixed

* New changes

* added operationstatus link

* merged sqlxEdits

* undid sqlx changes

* added completed notification

* passed retryInterval in notif options

* more changes

* added polling on landing on the page

* edits for error display

* added keys blade link

* added link generation

* added link to blade

* Modified info and description

* fixed format errors

* Second cut of the Portal

* OnChange for Number of instances

* added keys for texts

* fixed lint errors

* Added support for undefined dynamic description

* fixed failing test

* disable save/discard buttons

* fixed sqlx errors

* Dedicated Gateway changes to add the keys blade

* Change connectionStringText

* Change connectionStringText

* Text changes

* Added UI improvements

* Code review feedback

* undid package lock changes

Co-authored-by: Srinath Narayanan <srnara@microsoft.com>
2021-03-18 14:00:28 -07:00
Srinath Narayanan
be4e490a64 Added SelfServe UI updates (#559)
* Added SelfServe UI modifications

* fixed lint error

* addressed PR comments
2021-03-18 13:40:48 -07:00
Steve Faulkner
9db0975f7f Remove Explorer.isTryCosmos (#556) 2021-03-17 16:02:20 -05:00
Steve Faulkner
a2e3be9680 Remove Explorer.serverId (#555) 2021-03-17 15:24:21 -05:00
Srinath Narayanan
eab9b0ce9c mongo index policy editor bug fix (#550) 2021-03-17 11:52:16 -07:00
Steve Faulkner
d9d88c1517 Remove isAuthWithResourceToken from Explorer (#553)
* Remove isAuthWithResourceToken from Explorer

* Update test

* Remove ifs
2021-03-17 10:41:15 -05:00
Ken Dale
e10ab08d5c Small README.md update (#554) 2021-03-17 10:14:32 -05:00
Tanuj Mittal
3eda8029ba Change default Sort order for Gallery to MostRecent & fix gallery card height for empty tags (#545) 2021-03-17 01:40:38 +00:00
Steve Faulkner
6582d3be37 Fix Mongo AAD bug where collections are not load (#552) 2021-03-16 18:04:26 -05:00
186 changed files with 23008 additions and 7895 deletions

View File

@@ -24,7 +24,6 @@ src/Common/ObjectCache.test.ts
src/Common/ObjectCache.ts src/Common/ObjectCache.ts
src/Common/QueriesClient.ts src/Common/QueriesClient.ts
src/Common/Splitter.ts src/Common/Splitter.ts
src/Common/UrlUtility.ts
src/Config.ts src/Config.ts
src/Contracts/ActionContracts.ts src/Contracts/ActionContracts.ts
src/Contracts/DataModels.ts src/Contracts/DataModels.ts
@@ -98,7 +97,6 @@ src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts
src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.ts src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.ts
src/Explorer/Menus/ContextMenu.ts src/Explorer/Menus/ContextMenu.ts
src/Explorer/MostRecentActivity/MostRecentActivity.ts src/Explorer/MostRecentActivity/MostRecentActivity.ts
src/Explorer/Notebook/FileSystemUtil.ts
src/Explorer/Notebook/NotebookClientV2.ts src/Explorer/Notebook/NotebookClientV2.ts
src/Explorer/Notebook/NotebookComponent/NotebookContentProvider.ts src/Explorer/Notebook/NotebookComponent/NotebookContentProvider.ts
src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts
@@ -127,15 +125,12 @@ src/Explorer/Panes/DeleteCollectionConfirmationPane.test.ts
src/Explorer/Panes/DeleteCollectionConfirmationPane.ts src/Explorer/Panes/DeleteCollectionConfirmationPane.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts
src/Explorer/Panes/ExecuteSprocParamsPane.ts
src/Explorer/Panes/GraphStylingPane.ts src/Explorer/Panes/GraphStylingPane.ts
src/Explorer/Panes/LoadQueryPane.ts src/Explorer/Panes/LoadQueryPane.ts
src/Explorer/Panes/NewVertexPane.ts src/Explorer/Panes/NewVertexPane.ts
src/Explorer/Panes/PaneComponents.ts src/Explorer/Panes/PaneComponents.ts
src/Explorer/Panes/RenewAdHocAccessPane.ts src/Explorer/Panes/RenewAdHocAccessPane.ts
src/Explorer/Panes/SaveQueryPane.ts src/Explorer/Panes/SaveQueryPane.ts
src/Explorer/Panes/SettingsPane.test.ts
src/Explorer/Panes/SettingsPane.ts
src/Explorer/Panes/SetupNotebooksPane.ts src/Explorer/Panes/SetupNotebooksPane.ts
src/Explorer/Panes/StringInputPane.ts src/Explorer/Panes/StringInputPane.ts
src/Explorer/Panes/SwitchDirectoryPane.ts src/Explorer/Panes/SwitchDirectoryPane.ts
@@ -148,8 +143,6 @@ src/Explorer/Panes/Tables/TableEntityPane.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts
src/Explorer/Panes/UploadFilePane.ts
src/Explorer/Panes/UploadItemsPane.ts
src/Explorer/SplashScreen/SplashScreen.test.ts src/Explorer/SplashScreen/SplashScreen.test.ts
src/Explorer/Tables/Constants.ts src/Explorer/Tables/Constants.ts
src/Explorer/Tables/DataTable/CacheBase.ts src/Explorer/Tables/DataTable/CacheBase.ts
@@ -256,11 +249,8 @@ src/Terminal/NotebookAppContracts.d.ts
src/Terminal/index.ts src/Terminal/index.ts
src/TokenProviders/PortalTokenProvider.ts src/TokenProviders/PortalTokenProvider.ts
src/TokenProviders/TokenProviderFactory.ts src/TokenProviders/TokenProviderFactory.ts
src/Utils/DatabaseAccountUtils.test.ts
src/Utils/DatabaseAccountUtils.ts
src/Utils/PricingUtils.test.ts src/Utils/PricingUtils.test.ts
src/Utils/QueryUtils.test.ts src/Utils/QueryUtils.test.ts
src/Utils/QueryUtils.ts
src/applyExplorerBindings.ts src/applyExplorerBindings.ts
src/global.d.ts src/global.d.ts
src/setupTests.ts src/setupTests.ts

9
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"

View File

@@ -18,7 +18,6 @@ Run `npm start` to start the development server and automatically rebuild on cha
### Hosted Development (https://cosmos.azure.com) ### Hosted Development (https://cosmos.azure.com)
- Visit: `https://localhost:1234/hostedExplorer.html` - 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. - 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 ### Emulator Development
@@ -69,7 +68,7 @@ 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. 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 ### Architecture
[![](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) [![](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)

View File

@@ -7,5 +7,6 @@ module.exports = {
defaultViewport: null, defaultViewport: null,
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
args: ["--disable-web-security"], args: ["--disable-web-security"],
exitOnPageError: false,
}, },
}; };

View File

@@ -67,7 +67,8 @@ module.exports = {
// A map from regular expressions to module names that allow to stub out resources with a single module // A map from regular expressions to module names that allow to stub out resources with a single module
moduleNameMapper: { moduleNameMapper: {
"^.*[.](svg|png|gif|less)$": "<rootDir>/mockModule", "^.*[.](svg|png|gif|less|css)$": "<rootDir>/mockModule",
"@nteract/stateful-components/(.*)$": "<rootDir>/mockModule",
"worker-loader": "<rootDir>/mockModule", "worker-loader": "<rootDir>/mockModule",
"office-ui-fabric-react/lib/(.*)$": "office-ui-fabric-react/lib-commonjs/$1", // https://github.com/OfficeDev/office-ui-fabric-react/wiki/Fabric-6-Release-Notes "office-ui-fabric-react/lib/(.*)$": "office-ui-fabric-react/lib-commonjs/$1", // https://github.com/OfficeDev/office-ui-fabric-react/wiki/Fabric-6-Release-Notes
"^dnd-core$": "dnd-core/dist/cjs", "^dnd-core$": "dnd-core/dist/cjs",

View File

@@ -718,7 +718,7 @@ execute-sproc-params-pane {
} }
} }
stored-procedure-tab { .stored-procedure-tab {
@ToggleHeight: 30px; @ToggleHeight: 30px;
@ToggleWidth: 180px; @ToggleWidth: 180px;

3102
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -43,10 +43,9 @@
"@types/mkdirp": "1.0.1", "@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7", "@types/node-fetch": "2.5.7",
"@uifabric/react-cards": "0.109.110", "@uifabric/react-cards": "0.109.110",
"@uifabric/react-hooks": "7.14.0",
"@uifabric/styling": "7.13.7", "@uifabric/styling": "7.13.7",
"abort-controller": "3.0.0",
"applicationinsights": "1.8.0", "applicationinsights": "1.8.0",
"babel-polyfill": "6.26.0",
"bootstrap": "3.4.1", "bootstrap": "3.4.1",
"canvas": "file:./canvas", "canvas": "file:./canvas",
"clean-webpack-plugin": "0.1.19", "clean-webpack-plugin": "0.1.19",
@@ -60,8 +59,6 @@
"date-fns": "1.29.0", "date-fns": "1.29.0",
"dayjs": "1.8.19", "dayjs": "1.8.19",
"dotenv": "8.2.0", "dotenv": "8.2.0",
"es6-object-assign": "1.1.0",
"es6-symbol": "3.1.3",
"eslint-plugin-jest": "23.13.2", "eslint-plugin-jest": "23.13.2",
"eslint-plugin-react": "7.20.0", "eslint-plugin-react": "7.20.0",
"hasher": "1.2.0", "hasher": "1.2.0",
@@ -79,12 +76,9 @@
"monaco-editor": "0.18.1", "monaco-editor": "0.18.1",
"ms": "2.1.3", "ms": "2.1.3",
"msal": "1.4.4", "msal": "1.4.4",
"object.entries": "1.1.0", "office-ui-fabric-react": "7.164.2",
"office-ui-fabric-react": "7.134.1",
"p-retry": "4.2.0", "p-retry": "4.2.0",
"plotly.js-cartesian-dist-min": "1.52.3", "plotly.js-cartesian-dist-min": "1.52.3",
"promise-polyfill": "8.1.0",
"promise.prototype.finally": "3.1.0",
"q": "1.5.1", "q": "1.5.1",
"react": "16.13.1", "react": "16.13.1",
"react-animate-height": "2.0.8", "react-animate-height": "2.0.8",
@@ -101,13 +95,9 @@
"rxjs": "6.6.3", "rxjs": "6.6.3",
"styled-components": "4.3.2", "styled-components": "4.3.2",
"swr": "0.4.0", "swr": "0.4.0",
"text-encoding": "0.7.0", "terser-webpack-plugin": "3.1.0",
"underscore": "1.9.1", "underscore": "1.9.1",
"url-polyfill": "1.1.7", "utility-types": "3.10.0"
"utility-types": "3.10.0",
"webcrypto-liner": "1.1.4",
"webfontloader": "1.6.28",
"whatwg-fetch": "3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.9.0", "@babel/core": "7.9.0",
@@ -121,15 +111,15 @@
"@types/d3": "5.9.2", "@types/d3": "5.9.2",
"@types/enzyme": "3.10.7", "@types/enzyme": "3.10.7",
"@types/enzyme-adapter-react-16": "1.0.6", "@types/enzyme-adapter-react-16": "1.0.6",
"@types/expect-puppeteer": "4.4.3", "@types/expect-puppeteer": "4.4.5",
"@types/hasher": "0.0.31", "@types/hasher": "0.0.31",
"@types/jest": "26.0.20", "@types/jest": "26.0.20",
"@types/jest-environment-puppeteer": "4.3.2", "@types/jest-environment-puppeteer": "4.4.1",
"@types/memoize-one": "4.1.1", "@types/memoize-one": "4.1.1",
"@types/node": "12.11.1", "@types/node": "12.11.1",
"@types/promise.prototype.finally": "2.0.3", "@types/promise.prototype.finally": "2.0.3",
"@types/prop-types": "15.5.8", "@types/prop-types": "15.5.8",
"@types/puppeteer": "3.0.1", "@types/puppeteer": "5.4.3",
"@types/q": "1.5.1", "@types/q": "1.5.1",
"@types/react": "17.0.0", "@types/react": "17.0.0",
"@types/react-dom": "17.0.0", "@types/react-dom": "17.0.0",
@@ -137,9 +127,7 @@
"@types/react-redux": "7.1.7", "@types/react-redux": "7.1.7",
"@types/sinon": "2.3.3", "@types/sinon": "2.3.3",
"@types/styled-components": "5.1.1", "@types/styled-components": "5.1.1",
"@types/text-encoding": "0.0.33",
"@types/underscore": "1.7.36", "@types/underscore": "1.7.36",
"@types/webfontloader": "1.6.29",
"@typescript-eslint/eslint-plugin": "4.0.1", "@typescript-eslint/eslint-plugin": "4.0.1",
"@typescript-eslint/parser": "4.0.1", "@typescript-eslint/parser": "4.0.1",
"axe-puppeteer": "1.1.0", "axe-puppeteer": "1.1.0",
@@ -164,7 +152,6 @@
"html-loader": "0.5.5", "html-loader": "0.5.5",
"html-loader-jest": "0.2.1", "html-loader-jest": "0.2.1",
"html-webpack-plugin": "3.2.0", "html-webpack-plugin": "3.2.0",
"inline-css": "2.2.5",
"jest": "25.5.4", "jest": "25.5.4",
"jest-canvas-mock": "2.1.0", "jest-canvas-mock": "2.1.0",
"jest-puppeteer": "4.4.0", "jest-puppeteer": "4.4.0",
@@ -176,12 +163,11 @@
"monaco-editor-webpack-plugin": "1.7.0", "monaco-editor-webpack-plugin": "1.7.0",
"node-fetch": "2.6.1", "node-fetch": "2.6.1",
"prettier": "2.2.1", "prettier": "2.2.1",
"puppeteer": "4.0.0", "puppeteer": "8.0.0",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"rimraf": "3.0.0", "rimraf": "3.0.0",
"sinon": "3.2.1", "sinon": "3.2.1",
"style-loader": "0.23.0", "style-loader": "0.23.0",
"terser-webpack-plugin": "3.0.5",
"ts-loader": "6.2.2", "ts-loader": "6.2.2",
"tslint": "5.11.0", "tslint": "5.11.0",
"tslint-microsoft-contrib": "6.0.0", "tslint-microsoft-contrib": "6.0.0",

View File

@@ -98,30 +98,6 @@ export class CapabilityNames {
public static readonly EnableServerless: string = "EnableServerless"; 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 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 enableDatabaseSettingsTabV1 = "enabledbsettingsv1";
public static readonly selfServeType = "selfservetype";
public static readonly enableKOPanel = "enablekopanel";
}
// flight names returned from the portal are always lowercase // flight names returned from the portal are always lowercase
export class Flights { export class Flights {
public static readonly SettingsV2 = "settingsv2"; public static readonly SettingsV2 = "settingsv2";

View File

@@ -1,8 +1,8 @@
import { MessageTypes } from "../Contracts/ExplorerContracts";
import Q from "q"; import Q from "q";
import * as _ from "underscore"; import * as _ from "underscore";
import * as Constants from "./Constants"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { getDataExplorerWindow } from "../Utils/WindowUtils"; import { getDataExplorerWindow } from "../Utils/WindowUtils";
import * as Constants from "./Constants";
export interface CachedDataPromise<T> { export interface CachedDataPromise<T> {
deferred: Q.Deferred<T>; deferred: Q.Deferred<T>;
@@ -61,6 +61,21 @@ export function sendMessage(data: any): void {
} }
} }
export function sendReadyMessage(): void {
if (canSendMessage()) {
// We try to find data explorer window first, then fallback to current window
const portalChildWindow = getDataExplorerWindow(window) || window;
portalChildWindow.parent.postMessage(
{
signature: "pcIframe",
kind: "ready",
data: "ready",
},
portalChildWindow.document.referrer
);
}
}
export function canSendMessage(): boolean { export function canSendMessage(): boolean {
return window.parent !== window; return window.parent !== window;
} }

View File

@@ -6,7 +6,7 @@ import Explorer from "../Explorer/Explorer";
import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { QueryUtils } from "../Utils/QueryUtils"; import * as QueryUtils from "../Utils/QueryUtils";
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage"; import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage";

View File

@@ -0,0 +1,24 @@
import { useId } from "@uifabric/react-hooks";
import { ITooltipHostStyles, TooltipHost } from "office-ui-fabric-react/lib/Tooltip";
import * as React from "react";
import InfoBubble from "../../../images/info-bubble.svg";
const calloutProps = { gapSpace: 0 };
const hostStyles: Partial<ITooltipHostStyles> = { root: { display: "inline-block" } };
export interface TooltipProps {
children: string;
}
export const Tooltip: React.FunctionComponent = ({ children }: TooltipProps) => {
const tooltipId = useId("tooltip");
return children ? (
<span>
<TooltipHost content={children} id={tooltipId} calloutProps={calloutProps} styles={hostStyles}>
<img className="infoImg" src={InfoBubble} alt="More information" />
</TooltipHost>
</span>
) : (
<></>
);
};

View File

@@ -0,0 +1,75 @@
import { Image, Stack, TextField } from "office-ui-fabric-react";
import React, { ChangeEvent, FunctionComponent, KeyboardEvent, useRef, useState } from "react";
import FolderIcon from "../../../images/folder_16x16.svg";
import * as Constants from "../../Common/Constants";
import { Tooltip } from "../Tooltip";
interface UploadProps {
label: string;
accept?: string;
tooltip?: string;
multiple?: boolean;
tabIndex?: number;
onUpload: (event: ChangeEvent<HTMLInputElement>) => void;
}
export const Upload: FunctionComponent<UploadProps> = ({
label,
accept,
tooltip,
multiple,
tabIndex,
...props
}: UploadProps) => {
const [selectedFilesTitle, setSelectedFilesTitle] = useState<string[]>([]);
const fileRef = useRef<HTMLInputElement>();
const onImportLinkKeyPress = (event: KeyboardEvent<HTMLAnchorElement>): void => {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
onImportLinkClick();
}
};
const onImportLinkClick = (): void => {
fileRef?.current?.click();
};
const onUpload = (event: ChangeEvent<HTMLInputElement>): void => {
const { files } = event.target;
const newFileList = [];
for (let i = 0; i < files.length; i++) {
newFileList.push(files.item(i).name);
}
if (newFileList) {
setSelectedFilesTitle(newFileList);
props.onUpload(event);
}
};
const title = label + " to upload";
return (
<div>
<span className="renewUploadItemsHeader">{label}</span>
<Tooltip>{tooltip}</Tooltip>
<Stack horizontal>
<TextField styles={{ fieldGroup: { width: 300 } }} readOnly value={selectedFilesTitle.toString()} />
<input
type="file"
id="importFileInput"
style={{ display: "none" }}
ref={fileRef}
accept={accept}
tabIndex={tabIndex}
multiple={multiple}
title="Upload Icon"
onChange={onUpload}
role="button"
/>
<a href="#" id="fileImportLinkNotebook" onClick={onImportLinkClick} onKeyPress={onImportLinkKeyPress}>
<Image className="fileImportImg" src={FolderIcon} alt={title} title={title} />
</a>
</Stack>
</div>
);
};

View File

@@ -1,55 +1,61 @@
export default class UrlUtility { interface Result {
public static parseDocumentsPath(resourcePath: string): any { type?: string;
if (typeof resourcePath !== "string") { objectBody?: {
return {}; id: string;
} self: string;
};
if (resourcePath.length === 0) { }
return {};
} export function parseDocumentsPath(resourcePath: string): Result {
if (typeof resourcePath !== "string") {
if (resourcePath[resourcePath.length - 1] !== "/") { return {};
resourcePath = resourcePath + "/"; }
}
if (resourcePath.length === 0) {
if (resourcePath[0] !== "/") { return {};
resourcePath = "/" + resourcePath; }
}
if (resourcePath[resourcePath.length - 1] !== "/") {
var id: string; resourcePath = resourcePath + "/";
var type: string; }
var pathParts = resourcePath.split("/");
if (resourcePath[0] !== "/") {
if (pathParts.length % 2 === 0) { resourcePath = "/" + resourcePath;
id = pathParts[pathParts.length - 2]; }
type = pathParts[pathParts.length - 3];
} else { let id: string;
id = pathParts[pathParts.length - 3]; let type: string;
type = pathParts[pathParts.length - 2]; const pathParts = resourcePath.split("/");
}
if (pathParts.length % 2 === 0) {
var result = { id = pathParts[pathParts.length - 2];
type: type, type = pathParts[pathParts.length - 3];
objectBody: { } else {
id: id, id = pathParts[pathParts.length - 3];
self: resourcePath, type = pathParts[pathParts.length - 2];
}, }
};
const result = {
return result; type: type,
} objectBody: {
id: id,
public static createUri(baseUri: string, relativeUri: string): string { self: resourcePath,
if (!baseUri) { },
throw new Error("baseUri is null or empty"); };
}
return result;
var slashAtEndOfUriRegex = /\/$/, }
slashAtStartOfUriRegEx = /^\//;
export function createUri(baseUri: string, relativeUri: string): string {
var normalizedBaseUri = baseUri.replace(slashAtEndOfUriRegex, "") + "/", if (!baseUri) {
normalizedRelativeUri = (relativeUri && relativeUri.replace(slashAtStartOfUriRegEx, "")) || ""; throw new Error("baseUri is null or empty");
}
return normalizedBaseUri + normalizedRelativeUri;
} const slashAtEndOfUriRegex = /\/$/,
slashAtStartOfUriRegEx = /^\//;
const normalizedBaseUri = baseUri.replace(slashAtEndOfUriRegex, "") + "/",
normalizedRelativeUri = (relativeUri && relativeUri.replace(slashAtStartOfUriRegEx, "")) || "";
return normalizedBaseUri + normalizedRelativeUri;
} }

View File

@@ -88,7 +88,6 @@ export interface Database extends TreeNode {
loadCollections(): Promise<void>; loadCollections(): Promise<void>;
findCollectionWithId(collectionId: string): Collection; findCollectionWithId(collectionId: string): Collection;
openAddCollection(database: Database, event: MouseEvent): void; openAddCollection(database: Database, event: MouseEvent): void;
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
onSettingsClick: () => void; onSettingsClick: () => void;
loadOffer(): Promise<void>; loadOffer(): Promise<void>;
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>; getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
@@ -376,7 +375,6 @@ export interface DataExplorerInputsFrame {
masterKey?: string; masterKey?: string;
hasWriteAccess?: boolean; hasWriteAccess?: boolean;
authorizationToken?: string; authorizationToken?: string;
features: { [key: string]: string };
csmEndpoint?: string; csmEndpoint?: string;
dnsSuffix?: string; dnsSuffix?: string;
serverId?: string; serverId?: string;
@@ -390,7 +388,6 @@ export interface DataExplorerInputsFrame {
sharedThroughputMaximum?: number; sharedThroughputMaximum?: number;
sharedThroughputDefault?: number; sharedThroughputDefault?: number;
dataExplorerVersion?: string; dataExplorerVersion?: string;
isAuthWithresourceToken?: boolean;
defaultCollectionThroughput?: CollectionCreationDefaults; defaultCollectionThroughput?: CollectionCreationDefaults;
flights?: readonly string[]; flights?: readonly string[];
} }

View File

@@ -1,5 +1,10 @@
import * as Plotly from "plotly.js-cartesian-dist-min";
import dayjs from "dayjs"; import dayjs from "dayjs";
import * as Plotly from "plotly.js-cartesian-dist-min";
import { StyleConstants } from "../../Common/Constants";
import { sendCachedDataMessage, sendReadyMessage } from "../../Common/MessageHandler";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import "./Heatmap.less";
import { import {
ChartSettings, ChartSettings,
DataPayload, DataPayload,
@@ -11,11 +16,6 @@ import {
PartitionTimeStampToData, PartitionTimeStampToData,
PortalTheme, PortalTheme,
} from "./HeatmapDatatypes"; } from "./HeatmapDatatypes";
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import { sendCachedDataMessage, sendMessage } from "../../Common/MessageHandler";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { StyleConstants } from "../../Common/Constants";
import "./Heatmap.less";
export class Heatmap { export class Heatmap {
public static readonly elementId: string = "heatmap"; public static readonly elementId: string = "heatmap";
@@ -266,4 +266,4 @@ export function handleMessage(event: MessageEvent) {
} }
window.addEventListener("message", handleMessage, false); window.addEventListener("message", handleMessage, false);
sendMessage("ready"); sendReadyMessage();

View File

@@ -77,10 +77,6 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("delete-collection-confirmation-pane")).toBe(true); expect(ko.components.isRegistered("delete-collection-confirmation-pane")).toBe(true);
}); });
it("should register delete-database-confirmation-pane component", () => {
expect(ko.components.isRegistered("delete-database-confirmation-pane")).toBe(true);
});
it("should register save-query-pane component", () => { it("should register save-query-pane component", () => {
expect(ko.components.isRegistered("save-query-pane")).toBe(true); expect(ko.components.isRegistered("save-query-pane")).toBe(true);
}); });
@@ -97,10 +93,6 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("graph-styling-pane")).toBe(true); expect(ko.components.isRegistered("graph-styling-pane")).toBe(true);
}); });
it("should register upload-file-pane component", () => {
expect(ko.components.isRegistered("upload-file-pane")).toBe(true);
});
it("should register string-input-pane component", () => { it("should register string-input-pane component", () => {
expect(ko.components.isRegistered("string-input-pane")).toBe(true); expect(ko.components.isRegistered("string-input-pane")).toBe(true);
}); });

View File

@@ -1,16 +1,30 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as PaneComponents from "./Panes/PaneComponents";
import * as TabComponents from "./Tabs/TabComponents";
import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent"; import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent";
import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent"; import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent";
import { EditorComponent } from "./Controls/Editor/EditorComponent"; import { EditorComponent } from "./Controls/Editor/EditorComponent";
import { ErrorDisplayComponent } from "./Controls/ErrorDisplayComponent/ErrorDisplayComponent"; import { ErrorDisplayComponent } from "./Controls/ErrorDisplayComponent/ErrorDisplayComponent";
import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent";
import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahead"; import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahead";
import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent"; import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent";
import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent";
import { TabsManagerKOComponent } from "./Tabs/TabsManager";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3"; import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent";
import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent";
import * as PaneComponents from "./Panes/PaneComponents";
import ConflictsTab from "./Tabs/ConflictsTab";
import DatabaseSettingsTab from "./Tabs/DatabaseSettingsTab";
import DocumentsTab from "./Tabs/DocumentsTab";
import GalleryTab from "./Tabs/GalleryTab";
import GraphTab from "./Tabs/GraphTab";
import MongoShellTab from "./Tabs/MongoShellTab";
import NotebookTabV2 from "./Tabs/NotebookV2Tab";
import NotebookViewerTab from "./Tabs/NotebookViewerTab";
import QueryTab from "./Tabs/QueryTab";
import QueryTablesTab from "./Tabs/QueryTablesTab";
import { DatabaseSettingsTabV2, SettingsTabV2 } from "./Tabs/SettingsTabV2";
import StoredProcedureTab from "./Tabs/StoredProcedureTab";
import TabsManagerTemplate from "./Tabs/TabsManager.html";
import TerminalTab from "./Tabs/TerminalTab";
import TriggerTab from "./Tabs/TriggerTab";
import UserDefinedFunctionTab from "./Tabs/UserDefinedFunctionTab";
ko.components.register("input-typeahead", new InputTypeaheadComponent()); ko.components.register("input-typeahead", new InputTypeaheadComponent());
ko.components.register("new-vertex-form", NewVertexComponent); ko.components.register("new-vertex-form", NewVertexComponent);
@@ -21,28 +35,27 @@ ko.components.register("json-editor", new JsonEditorComponent());
ko.components.register("diff-editor", new DiffEditorComponent()); ko.components.register("diff-editor", new DiffEditorComponent());
ko.components.register("dynamic-list", DynamicListComponent); ko.components.register("dynamic-list", DynamicListComponent);
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3); ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
ko.components.register("tabs-manager", TabsManagerKOComponent()); ko.components.register("tabs-manager", { template: TabsManagerTemplate });
// Collection Tabs // Collection Tabs
ko.components.register("documents-tab", new TabComponents.DocumentsTab()); [
ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab()); DocumentsTab,
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab()); StoredProcedureTab,
ko.components.register("trigger-tab", new TabComponents.TriggerTab()); TriggerTab,
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab()); UserDefinedFunctionTab,
ko.components.register("collection-settings-tab-v2", new TabComponents.SettingsTabV2()); SettingsTabV2,
ko.components.register("query-tab", new TabComponents.QueryTab()); QueryTab,
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab()); QueryTablesTab,
ko.components.register("graph-tab", new TabComponents.GraphTab()); GraphTab,
ko.components.register("mongo-shell-tab", new TabComponents.MongoShellTab()); MongoShellTab,
ko.components.register("conflicts-tab", new TabComponents.ConflictsTab()); ConflictsTab,
ko.components.register("notebookv2-tab", new TabComponents.NotebookV2Tab()); NotebookTabV2,
ko.components.register("terminal-tab", new TabComponents.TerminalTab()); TerminalTab,
ko.components.register("gallery-tab", new TabComponents.GalleryTab()); GalleryTab,
ko.components.register("notebook-viewer-tab", new TabComponents.NotebookViewerTab()); NotebookViewerTab,
DatabaseSettingsTab,
// Database Tabs DatabaseSettingsTabV2,
ko.components.register("database-settings-tab", new TabComponents.DatabaseSettingsTab()); ].forEach(({ component: { name, template } }) => ko.components.register(name, { template }));
ko.components.register("database-settings-tab-v2", new TabComponents.SettingsTabV2());
// Panes // Panes
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent()); ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
@@ -51,10 +64,7 @@ ko.components.register(
"delete-collection-confirmation-pane", "delete-collection-confirmation-pane",
new PaneComponents.DeleteCollectionConfirmationPaneComponent() new PaneComponents.DeleteCollectionConfirmationPaneComponent()
); );
ko.components.register(
"delete-database-confirmation-pane",
new PaneComponents.DeleteDatabaseConfirmationPaneComponent()
);
ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent()); ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent());
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent()); ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent()); ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());
@@ -62,13 +72,9 @@ ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEnt
ko.components.register("table-column-options-pane", new PaneComponents.TableColumnOptionsPaneComponent()); ko.components.register("table-column-options-pane", new PaneComponents.TableColumnOptionsPaneComponent());
ko.components.register("table-query-select-pane", new PaneComponents.TableQuerySelectPaneComponent()); ko.components.register("table-query-select-pane", new PaneComponents.TableQuerySelectPaneComponent());
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent()); ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());
ko.components.register("settings-pane", new PaneComponents.SettingsPaneComponent());
ko.components.register("execute-sproc-params-pane", new PaneComponents.ExecuteSprocParamsComponent());
ko.components.register("upload-items-pane", new PaneComponents.UploadItemsPaneComponent());
ko.components.register("load-query-pane", new PaneComponents.LoadQueryPaneComponent()); ko.components.register("load-query-pane", new PaneComponents.LoadQueryPaneComponent());
ko.components.register("save-query-pane", new PaneComponents.SaveQueryPaneComponent()); ko.components.register("save-query-pane", new PaneComponents.SaveQueryPaneComponent());
ko.components.register("browse-queries-pane", new PaneComponents.BrowseQueriesPaneComponent()); ko.components.register("browse-queries-pane", new PaneComponents.BrowseQueriesPaneComponent());
ko.components.register("upload-file-pane", new PaneComponents.UploadFilePaneComponent());
ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent()); ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent());
ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent()); ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent());
ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent()); ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent());

View File

@@ -1,23 +1,22 @@
import * as ko from "knockout";
import * as ViewModels from "../Contracts/ViewModels";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import AddCollectionIcon from "../../images/AddCollection.svg"; import AddCollectionIcon from "../../images/AddCollection.svg";
import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg"; import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
import AddStoredProcedureIcon from "../../images/AddStoredProcedure.svg"; import AddStoredProcedureIcon from "../../images/AddStoredProcedure.svg";
import AddTriggerIcon from "../../images/AddTrigger.svg";
import AddUdfIcon from "../../images/AddUdf.svg";
import DeleteCollectionIcon from "../../images/DeleteCollection.svg"; import DeleteCollectionIcon from "../../images/DeleteCollection.svg";
import DeleteDatabaseIcon from "../../images/DeleteDatabase.svg"; import DeleteDatabaseIcon from "../../images/DeleteDatabase.svg";
import AddUdfIcon from "../../images/AddUdf.svg"; import DeleteSprocIcon from "../../images/DeleteSproc.svg";
import AddTriggerIcon from "../../images/AddTrigger.svg";
import DeleteTriggerIcon from "../../images/DeleteTrigger.svg"; import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
import DeleteUDFIcon from "../../images/DeleteUDF.svg"; import DeleteUDFIcon from "../../images/DeleteUDF.svg";
import DeleteSprocIcon from "../../images/DeleteSproc.svg"; import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
import * as ViewModels from "../Contracts/ViewModels";
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
import { userContext } from "../UserContext";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import Explorer from "./Explorer"; import Explorer from "./Explorer";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
import StoredProcedure from "./Tree/StoredProcedure"; import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger"; import Trigger from "./Tree/Trigger";
import { userContext } from "../UserContext"; import UserDefinedFunction from "./Tree/UserDefinedFunction";
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
export interface CollectionContextMenuButtonParams { export interface CollectionContextMenuButtonParams {
databaseId: string; databaseId: string;
@@ -43,7 +42,7 @@ export class ResourceTreeContextMenuButtonFactory {
if (userContext.defaultExperience !== DefaultAccountExperienceType.Table) { if (userContext.defaultExperience !== DefaultAccountExperienceType.Table) {
items.push({ items.push({
iconSrc: DeleteDatabaseIcon, iconSrc: DeleteDatabaseIcon,
onClick: () => container.deleteDatabaseConfirmationPane.open(), onClick: () => container.openDeleteDatabaseConfirmationPane(),
label: container.deleteDatabaseText(), label: container.deleteDatabaseText(),
styleClass: "deleteDatabaseMenuItem", styleClass: "deleteDatabaseMenuItem",
}); });

View File

@@ -6,6 +6,7 @@ describe("CollapsibleSectionComponent", () => {
it("renders", () => { it("renders", () => {
const props: CollapsibleSectionProps = { const props: CollapsibleSectionProps = {
title: "Sample title", title: "Sample title",
isExpandedByDefault: true,
}; };
const wrapper = shallow(<CollapsibleSectionComponent {...props} />); const wrapper = shallow(<CollapsibleSectionComponent {...props} />);

View File

@@ -1,9 +1,10 @@
import { Icon, Label, Stack } from "office-ui-fabric-react"; import { Icon, Label, Stack } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { accordionIconStyles, accordionStackTokens } from "../Settings/SettingsRenderUtils"; import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
export interface CollapsibleSectionProps { export interface CollapsibleSectionProps {
title: string; title: string;
isExpandedByDefault: boolean;
} }
export interface CollapsibleSectionState { export interface CollapsibleSectionState {
@@ -14,7 +15,7 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
constructor(props: CollapsibleSectionProps) { constructor(props: CollapsibleSectionProps) {
super(props); super(props);
this.state = { this.state = {
isExpanded: true, isExpanded: this.props.isExpandedByDefault,
}; };
} }
@@ -25,8 +26,14 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<> <>
<Stack className="collapsibleSection" horizontal tokens={accordionStackTokens} onClick={this.toggleCollapsed}> <Stack
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} styles={accordionIconStyles} /> className="collapsibleSection"
horizontal
verticalAlign="center"
tokens={accordionStackTokens}
onClick={this.toggleCollapsed}
>
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
<Label>{this.props.title}</Label> <Label>{this.props.title}</Label>
</Stack> </Stack>
{this.state.isExpanded && this.props.children} {this.state.isExpanded && this.props.children}

View File

@@ -11,16 +11,10 @@ exports[`CollapsibleSectionComponent renders 1`] = `
"childrenGap": 10, "childrenGap": 10,
} }
} }
verticalAlign="center"
> >
<StyledIconBase <Icon
iconName="ChevronDown" iconName="ChevronDown"
styles={
Object {
"root": Object {
"paddingTop": 7,
},
}
}
/> />
<StyledLabelBase> <StyledLabelBase>
Sample title Sample title

View File

@@ -350,12 +350,11 @@ exports[`test render renders with filters 1`] = `
} }
> >
<div <div
className="ms-ScrollablePane root-40" className="ms-ScrollablePane root-72"
data-is-scrollable="true" data-is-scrollable="true"
> >
<div <div
aria-hidden="true" className="stickyAbove-74"
className="stickyAbove-42"
style={ style={
Object { Object {
"height": 0, "height": 0,
@@ -366,7 +365,7 @@ exports[`test render renders with filters 1`] = `
} }
/> />
<div <div
className="ms-ScrollablePane--contentContainer contentContainer-41" className="ms-ScrollablePane--contentContainer contentContainer-73"
data-is-scrollable={true} data-is-scrollable={true}
> >
<Sticky <Sticky
@@ -375,7 +374,6 @@ exports[`test render renders with filters 1`] = `
> >
<div> <div>
<div <div
aria-hidden={true}
style={ style={
Object { Object {
"pointerEvents": "none", "pointerEvents": "none",
@@ -395,7 +393,6 @@ exports[`test render renders with filters 1`] = `
style={Object {}} style={Object {}}
> >
<div <div
aria-hidden={false}
style={ style={
Object { Object {
"backgroundColor": "", "backgroundColor": "",
@@ -411,6 +408,7 @@ exports[`test render renders with filters 1`] = `
> >
<TextFieldBase <TextFieldBase
ariaLabel="Directory filter text box" ariaLabel="Directory filter text box"
canRevealPassword={false}
className="directoryListFilterTextBox" className="directoryListFilterTextBox"
deferredValidationTime={200} deferredValidationTime={200}
onChange={[Function]} onChange={[Function]}
@@ -693,18 +691,18 @@ exports[`test render renders with filters 1`] = `
validateOnLoad={true} validateOnLoad={true}
> >
<div <div
className="ms-TextField directoryListFilterTextBox root-46" className="ms-TextField directoryListFilterTextBox root-78"
> >
<div <div
className="ms-TextField-wrapper" className="ms-TextField-wrapper"
> >
<div <div
className="ms-TextField-fieldGroup fieldGroup-47" className="ms-TextField-fieldGroup fieldGroup-79"
> >
<input <input
aria-invalid={false} aria-invalid={false}
aria-label="Directory filter text box" aria-label="Directory filter text box"
className="ms-TextField-field field-48" className="ms-TextField-field field-80"
id="TextField0" id="TextField0"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
@@ -1123,7 +1121,7 @@ exports[`test render renders with filters 1`] = `
"iconDisabled": Object { "iconDisabled": Object {
"color": "#a19f9d", "color": "#a19f9d",
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"color": "GrayText", "color": "GrayText",
}, },
}, },
@@ -1149,7 +1147,7 @@ exports[`test render renders with filters 1`] = `
"menuIconDisabled": Object { "menuIconDisabled": Object {
"color": "#a19f9d", "color": "#a19f9d",
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"color": "GrayText", "color": "GrayText",
}, },
}, },
@@ -1168,7 +1166,7 @@ exports[`test render renders with filters 1`] = `
"position": "absolute", "position": "absolute",
"right": 2, "right": 2,
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"bottom": -2, "bottom": -2,
"left": -2, "left": -2,
"outlineColor": "ButtonText", "outlineColor": "ButtonText",
@@ -1247,7 +1245,7 @@ exports[`test render renders with filters 1`] = `
"position": "absolute", "position": "absolute",
"right": 2, "right": 2,
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"bottom": -2, "bottom": -2,
"left": -2, "left": -2,
"outlineColor": "ButtonText", "outlineColor": "ButtonText",
@@ -1279,8 +1277,10 @@ exports[`test render renders with filters 1`] = `
}, },
}, },
Object { Object {
"backgroundColor": "#f3f2f1",
"color": "#a19f9d",
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"backgroundColor": "Window", "backgroundColor": "Window",
"borderColor": "GrayText", "borderColor": "GrayText",
"color": "GrayText", "color": "GrayText",
@@ -1300,7 +1300,7 @@ exports[`test render renders with filters 1`] = `
"backgroundColor": "#f3f2f1", "backgroundColor": "#f3f2f1",
"color": "#201f1e", "color": "#201f1e",
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"borderColor": "Highlight", "borderColor": "Highlight",
"color": "Highlight", "color": "Highlight",
}, },
@@ -1326,7 +1326,7 @@ exports[`test render renders with filters 1`] = `
"splitButtonContainer": Array [ "splitButtonContainer": Array [
Object { Object {
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"border": "none", "border": "none",
}, },
}, },
@@ -1344,7 +1344,7 @@ exports[`test render renders with filters 1`] = `
"position": "absolute", "position": "absolute",
"right": 3, "right": 3,
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"border": "none", "border": "none",
"bottom": -2, "bottom": -2,
"left": -2, "left": -2,
@@ -1373,19 +1373,20 @@ exports[`test render renders with filters 1`] = `
"borderBottomRightRadius": "0", "borderBottomRightRadius": "0",
"borderTopRightRadius": "0", "borderTopRightRadius": "0",
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"MsHighContrastAdjust": "none", "MsHighContrastAdjust": "none",
"backgroundColor": "Window", "backgroundColor": "Window",
"border": "1px solid WindowText", "border": "1px solid WindowText",
"borderRightWidth": "0", "borderRightWidth": "0",
"color": "WindowText", "color": "WindowText",
"forcedColorAdjust": "none",
}, },
}, },
}, },
".ms-Button--primary + .ms-Button": Object { ".ms-Button--primary + .ms-Button": Object {
"border": "none", "border": "none",
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"border": "1px solid WindowText", "border": "1px solid WindowText",
"borderLeftWidth": "0", "borderLeftWidth": "0",
}, },
@@ -1398,10 +1399,11 @@ exports[`test render renders with filters 1`] = `
"selectors": Object { "selectors": Object {
".ms-Button--primary": Object { ".ms-Button--primary": Object {
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"MsHighContrastAdjust": "none", "MsHighContrastAdjust": "none",
"backgroundColor": "WindowText", "backgroundColor": "WindowText",
"color": "Window", "color": "Window",
"forcedColorAdjust": "none",
}, },
}, },
}, },
@@ -1411,10 +1413,11 @@ exports[`test render renders with filters 1`] = `
"selectors": Object { "selectors": Object {
".ms-Button--primary": Object { ".ms-Button--primary": Object {
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"MsHighContrastAdjust": "none", "MsHighContrastAdjust": "none",
"backgroundColor": "WindowText", "backgroundColor": "WindowText",
"color": "Window", "color": "Window",
"forcedColorAdjust": "none",
}, },
}, },
}, },
@@ -1424,12 +1427,11 @@ exports[`test render renders with filters 1`] = `
"border": "none", "border": "none",
"outline": "none", "outline": "none",
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"MsHighContrastAdjust": "none",
"backgroundColor": "Window", "backgroundColor": "Window",
"borderColor": "GrayText", "borderColor": "GrayText",
"color": "GrayText", "color": "GrayText",
},
"@media screen and (forced-colors: active)": Object {
"forcedColorAdjust": "none", "forcedColorAdjust": "none",
}, },
}, },
@@ -1441,7 +1443,7 @@ exports[`test render renders with filters 1`] = `
"selectors": Object { "selectors": Object {
".ms-Button--primary": Object { ".ms-Button--primary": Object {
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"backgroundColor": "Highlight", "backgroundColor": "Highlight",
"color": "Window", "color": "Window",
}, },
@@ -1450,7 +1452,7 @@ exports[`test render renders with filters 1`] = `
".ms-Button.is-disabled": Object { ".ms-Button.is-disabled": Object {
"color": "#a19f9d", "color": "#a19f9d",
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"backgroundColor": "Window", "backgroundColor": "Window",
"borderColor": "GrayText", "borderColor": "GrayText",
"color": "GrayText", "color": "GrayText",
@@ -1466,7 +1468,7 @@ exports[`test render renders with filters 1`] = `
"position": "absolute", "position": "absolute",
"right": 31, "right": 31,
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"backgroundColor": "WindowText", "backgroundColor": "WindowText",
}, },
}, },
@@ -1478,7 +1480,7 @@ exports[`test render renders with filters 1`] = `
"position": "absolute", "position": "absolute",
"right": 31, "right": 31,
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"backgroundColor": "WindowText", "backgroundColor": "WindowText",
}, },
}, },
@@ -1495,7 +1497,7 @@ exports[`test render renders with filters 1`] = `
"position": "absolute", "position": "absolute",
"right": 31, "right": 31,
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"backgroundColor": "GrayText", "backgroundColor": "GrayText",
}, },
}, },
@@ -1518,7 +1520,7 @@ exports[`test render renders with filters 1`] = `
":hover": Object { ":hover": Object {
"backgroundColor": "#edebe9", "backgroundColor": "#edebe9",
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"color": "Highlight", "color": "Highlight",
}, },
}, },
@@ -1526,6 +1528,11 @@ exports[`test render renders with filters 1`] = `
}, },
}, },
Object { Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
".ms-Button-menuIcon": Object {
"color": "WindowText",
},
},
"border": "1px solid #8a8886", "border": "1px solid #8a8886",
"borderBottomRightRadius": "2px", "borderBottomRightRadius": "2px",
"borderLeft": "none", "borderLeft": "none",
@@ -1571,7 +1578,7 @@ exports[`test render renders with filters 1`] = `
"selectors": Object { "selectors": Object {
".ms-Button--primary": Object { ".ms-Button--primary": Object {
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"backgroundColor": "Window", "backgroundColor": "Window",
"borderColor": "GrayText", "borderColor": "GrayText",
"color": "GrayText", "color": "GrayText",
@@ -1580,7 +1587,7 @@ exports[`test render renders with filters 1`] = `
}, },
".ms-Button-menuIcon": Object { ".ms-Button-menuIcon": Object {
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"color": "GrayText", "color": "GrayText",
}, },
}, },
@@ -1588,7 +1595,7 @@ exports[`test render renders with filters 1`] = `
":hover": Object { ":hover": Object {
"cursor": "default", "cursor": "default",
}, },
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
"backgroundColor": "Window", "backgroundColor": "Window",
"border": "1px solid GrayText", "border": "1px solid GrayText",
"color": "GrayText", "color": "GrayText",
@@ -1893,7 +1900,7 @@ exports[`test render renders with filters 1`] = `
> >
<button <button
aria-disabled={true} aria-disabled={true}
className="ms-Button ms-Button--default is-disabled directoryListButton root-54" className="ms-Button ms-Button--default is-disabled directoryListButton root-89"
data-is-focusable={false} data-is-focusable={false}
disabled={true} disabled={true}
onClick={[Function]} onClick={[Function]}
@@ -1905,7 +1912,7 @@ exports[`test render renders with filters 1`] = `
type="button" type="button"
> >
<span <span
className="ms-Button-flexContainer flexContainer-55" className="ms-Button-flexContainer flexContainer-90"
data-automationid="splitbuttonprimary" data-automationid="splitbuttonprimary"
> >
<div <div
@@ -1936,8 +1943,7 @@ exports[`test render renders with filters 1`] = `
</List> </List>
</div> </div>
<div <div
aria-hidden="true" className="stickyBelow-75"
className="stickyBelow-43"
style={ style={
Object { Object {
"bottom": "0px", "bottom": "0px",
@@ -1948,7 +1954,7 @@ exports[`test render renders with filters 1`] = `
} }
> >
<div <div
className="stickyBelowItems-44" className="stickyBelowItems-76"
/> />
</div> </div>
</div> </div>

View File

@@ -7,7 +7,7 @@ import { ChildrenMargin } from "./GitHubStyleConstants";
import * as GitHubUtils from "../../../Utils/GitHubUtils"; import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { IGitHubRepo } from "../../../GitHub/GitHubClient"; import { IGitHubRepo } from "../../../GitHub/GitHubClient";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import UrlUtility from "../../../Common/UrlUtility"; import * as UrlUtility from "../../../Common/UrlUtility";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
export interface AddRepoComponentProps { export interface AddRepoComponentProps {

View File

@@ -18,7 +18,7 @@ import {
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { IGalleryItem } from "../../../../Juno/JunoClient"; import { IGalleryItem } from "../../../../Juno/JunoClient";
import { FileSystemUtil } from "../../../Notebook/FileSystemUtil"; import * as FileSystemUtil from "../../../Notebook/FileSystemUtil";
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg"; import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
export interface GalleryCardComponentProps { export interface GalleryCardComponentProps {
@@ -47,6 +47,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
private static readonly cardItemGapBig = 10; private static readonly cardItemGapBig = 10;
private static readonly cardItemGapSmall = 8; private static readonly cardItemGapSmall = 8;
private static readonly cardDeleteSpinnerHeight = 360; private static readonly cardDeleteSpinnerHeight = 360;
private static readonly smallTextLineHeight = 18;
constructor(props: GalleryCardComponentProps) { constructor(props: GalleryCardComponentProps) {
super(props); super(props);
@@ -103,7 +104,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
</Card.Item> </Card.Item>
<Card.Section styles={{ root: { padding: GalleryCardComponent.cardItemGapBig } }}> <Card.Section styles={{ root: { padding: GalleryCardComponent.cardItemGapBig } }}>
<Text variant="small" nowrap> <Text variant="small" nowrap styles={{ root: { height: GalleryCardComponent.smallTextLineHeight } }}>
{this.props.data.tags ? ( {this.props.data.tags ? (
this.props.data.tags.map((tag, index, array) => ( this.props.data.tags.map((tag, index, array) => (
<span key={tag}> <span key={tag}>
@@ -129,7 +130,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
{cardTitle} {cardTitle}
</Text> </Text>
<Text variant="small" styles={{ root: { height: 36 } }}> <Text variant="small" styles={{ root: { height: GalleryCardComponent.smallTextLineHeight * 2 } }}>
{this.renderTruncatedDescription()} {this.renderTruncatedDescription()}
</Text> </Text>

View File

@@ -50,6 +50,13 @@ exports[`GalleryCardComponent renders 1`] = `
> >
<Text <Text
nowrap={true} nowrap={true}
styles={
Object {
"root": Object {
"height": 18,
},
}
}
variant="small" variant="small"
> >
<span <span
@@ -100,7 +107,7 @@ exports[`GalleryCardComponent renders 1`] = `
} }
variant="tiny" variant="tiny"
> >
<StyledIconBase <Icon
iconName="RedEye" iconName="RedEye"
styles={ styles={
Object { Object {
@@ -124,7 +131,7 @@ exports[`GalleryCardComponent renders 1`] = `
} }
variant="tiny" variant="tiny"
> >
<StyledIconBase <Icon
iconName="Download" iconName="Download"
styles={ styles={
Object { Object {
@@ -148,7 +155,7 @@ exports[`GalleryCardComponent renders 1`] = `
} }
variant="tiny" variant="tiny"
> >
<StyledIconBase <Icon
iconName="Heart" iconName="Heart"
styles={ styles={
Object { Object {
@@ -173,7 +180,7 @@ exports[`GalleryCardComponent renders 1`] = `
} }
} }
> >
<Styled <Separator
styles={ styles={
Object { Object {
"root": Object { "root": Object {

View File

@@ -13,7 +13,7 @@ exports[`InfoComponent renders 1`] = `
<div <div
className="infoPanelMain" className="infoPanelMain"
> >
<StyledIconBase <Icon
className="infoIconMain" className="infoIconMain"
iconName="Help" iconName="Help"
styles={ styles={

View File

@@ -14,7 +14,7 @@ import {
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { IGalleryItem } from "../../../Juno/JunoClient"; import { IGalleryItem } from "../../../Juno/JunoClient";
import { FileSystemUtil } from "../../Notebook/FileSystemUtil"; import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
import "./NotebookViewerComponent.less"; import "./NotebookViewerComponent.less";
import CosmosDBLogo from "../../../../images/CosmosDB-logo.svg"; import CosmosDBLogo from "../../../../images/CosmosDB-logo.svg";
import { InfoComponent } from "../NotebookGallery/InfoComponent/InfoComponent"; import { InfoComponent } from "../NotebookGallery/InfoComponent/InfoComponent";

View File

@@ -68,14 +68,14 @@ exports[`NotebookMetadataComponent renders liked notebook 1`] = `
Invalid Date Invalid Date
</Text> </Text>
<Text> <Text>
<StyledIconBase <Icon
iconName="RedEye" iconName="RedEye"
/> />
0 0
</Text> </Text>
<Text> <Text>
<StyledIconBase <Icon
iconName="Download" iconName="Download"
/> />
0 0
@@ -180,14 +180,14 @@ exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
Invalid Date Invalid Date
</Text> </Text>
<Text> <Text>
<StyledIconBase <Icon
iconName="RedEye" iconName="RedEye"
/> />
0 0
</Text> </Text>
<Text> <Text>
<StyledIconBase <Icon
iconName="Download" iconName="Download"
/> />
0 0

View File

@@ -1,17 +1,19 @@
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react";
import { SettingsComponentProps, SettingsComponent, SettingsComponentState } from "./SettingsComponent";
import * as ViewModels from "../../../Contracts/ViewModels";
import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2";
import { collection } from "./TestUtils";
import * as DataModels from "../../../Contracts/DataModels";
import ko from "knockout"; import ko from "knockout";
import { TtlType, isDirty } from "./SettingsUtils"; import React from "react";
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2";
import { SettingsComponent, SettingsComponentProps, SettingsComponentState } from "./SettingsComponent";
import { isDirty, TtlType } from "./SettingsUtils";
import { collection } from "./TestUtils";
jest.mock("../../../Common/dataAccess/getIndexTransformationProgress", () => ({ jest.mock("../../../Common/dataAccess/getIndexTransformationProgress", () => ({
getIndexTransformationProgress: jest.fn().mockReturnValue(undefined), getIndexTransformationProgress: jest.fn().mockReturnValue(undefined),
})); }));
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
jest.mock("../../../Common/dataAccess/updateCollection", () => ({ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
updateCollection: jest.fn().mockReturnValue({ updateCollection: jest.fn().mockReturnValue({
id: undefined, id: undefined,
@@ -29,8 +31,6 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
analyticalStorageTtl: undefined, analyticalStorageTtl: undefined,
} as MongoDBCollectionResource), } as MongoDBCollectionResource),
})); }));
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types";
jest.mock("../../../Common/dataAccess/updateOffer", () => ({ jest.mock("../../../Common/dataAccess/updateOffer", () => ({
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer), updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer),
})); }));
@@ -134,7 +134,6 @@ describe("SettingsComponent", () => {
loadCollections: undefined, loadCollections: undefined,
findCollectionWithId: undefined, findCollectionWithId: undefined,
openAddCollection: undefined, openAddCollection: undefined,
onDeleteDatabaseContextMenuClick: undefined,
readSettings: undefined, readSettings: undefined,
onSettingsClick: undefined, onSettingsClick: undefined,
loadOffer: undefined, loadOffer: undefined,

View File

@@ -1,49 +1,51 @@
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "office-ui-fabric-react";
import * as React from "react"; 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 ViewModels from "../../../Contracts/ViewModels";
import DiscardIcon from "../../../../images/discard.svg"; import DiscardIcon from "../../../../images/discard.svg";
import SaveIcon from "../../../../images/save-cosmos.svg"; import SaveIcon from "../../../../images/save-cosmos.svg";
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor"; import { AuthType } from "../../../AuthType";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import * as Constants from "../../../Common/Constants";
import Explorer from "../../Explorer"; import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection"; import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2"; import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
import "./SettingsComponent.less";
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils"; import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
import {
MongoIndexingPolicyComponent,
MongoIndexingPolicyComponentProps,
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
import {
hasDatabaseSharedThroughput,
GeospatialConfigType,
TtlType,
ChangeFeedPolicyState,
SettingsV2TabTypes,
getTabTitle,
isDirty,
AddMongoIndexProps,
MongoIndexTypes,
parseConflictResolutionMode,
parseConflictResolutionProcedure,
getMongoNotification,
} from "./SettingsUtils";
import { import {
ConflictResolutionComponent, ConflictResolutionComponent,
ConflictResolutionComponentProps, ConflictResolutionComponentProps,
} from "./SettingsSubComponents/ConflictResolutionComponent"; } from "./SettingsSubComponents/ConflictResolutionComponent";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent";
import { Pivot, PivotItem, IPivotProps, IPivotItemProps } from "office-ui-fabric-react";
import "./SettingsComponent.less";
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent"; import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types"; import {
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection"; MongoIndexingPolicyComponent,
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress"; MongoIndexingPolicyComponentProps,
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; } from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent";
import {
AddMongoIndexProps,
ChangeFeedPolicyState,
GeospatialConfigType,
getMongoNotification,
getTabTitle,
hasDatabaseSharedThroughput,
isDirty,
MongoIndexTypes,
parseConflictResolutionMode,
parseConflictResolutionProcedure,
SettingsV2TabTypes,
TtlType,
} from "./SettingsUtils";
interface SettingsV2TabInfo { interface SettingsV2TabInfo {
tab: SettingsV2TabTypes; tab: SettingsV2TabTypes;
@@ -137,9 +139,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.shouldShowIndexingPolicyEditor = this.shouldShowIndexingPolicyEditor =
this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB(); this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB();
this.changeFeedPolicyVisible = this.collection?.container.isFeatureEnabled( this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
Constants.Features.enableChangeFeedPolicy
);
// Mongo container with system partition key still treat as "Fixed" // Mongo container with system partition key still treat as "Fixed"
this.isFixedContainer = this.isFixedContainer =
@@ -325,7 +325,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
? this.saveCollectionSettings(startKey) ? this.saveCollectionSettings(startKey)
: this.saveDatabaseSettings(startKey)); : this.saveDatabaseSettings(startKey));
} catch (error) { } catch (error) {
this.container.isRefreshingExplorer(false);
this.props.settingsTab.isExecutionError(true); this.props.settingsTab.isExecutionError(true);
console.error(error); console.error(error);
traceFailure( traceFailure(
@@ -699,7 +698,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
} }
} }
this.container.isRefreshingExplorer(false);
this.setBaseline(); this.setBaseline();
this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected }); this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected });
traceSuccess( traceSuccess(
@@ -862,7 +860,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}); });
} }
} }
this.container.isRefreshingExplorer(false);
this.setBaseline(); this.setBaseline();
this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected }); this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected });
traceSuccess( traceSuccess(
@@ -877,6 +874,18 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
); );
}; };
public getMongoIndexTabContent = (
mongoIndexingPolicyComponentProps: MongoIndexingPolicyComponentProps
): JSX.Element => {
if (userContext.authType === AuthType.AAD) {
if (this.container.isEnableMongoCapabilityPresent()) {
return <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />;
}
return undefined;
}
return mongoIndexingPolicyAADError;
};
public render(): JSX.Element { public render(): JSX.Element {
const scaleComponentProps: ScaleComponentProps = { const scaleComponentProps: ScaleComponentProps = {
collection: this.collection, collection: this.collection,
@@ -994,15 +1003,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />, content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />,
}); });
} else if (this.container.isPreferredApiMongoDB()) { } else if (this.container.isPreferredApiMongoDB()) {
if (this.container.isEnableMongoCapabilityPresent()) { const mongoIndexTabContext = this.getMongoIndexTabContent(mongoIndexingPolicyComponentProps);
if (mongoIndexTabContext) {
tabs.push({ tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab, tab: SettingsV2TabTypes.IndexingPolicyTab,
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />, content: mongoIndexTabContext,
});
} else {
tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab,
content: mongoIndexingPolicyAADError,
}); });
} }
} }

View File

@@ -23,7 +23,6 @@ import {
ITextStyles, ITextStyles,
IDetailsRowStyles, IDetailsRowStyles,
IStackStyles, IStackStyles,
IIconStyles,
IDetailsListStyles, IDetailsListStyles,
IDropdownStyles, IDropdownStyles,
ISeparatorStyles, ISeparatorStyles,
@@ -116,8 +115,6 @@ export const addMongoIndexSubElementsTokens: IStackTokens = {
childrenGap: 20, childrenGap: 20,
}; };
export const accordionIconStyles: IIconStyles = { root: { paddingTop: 7 } };
export const mediumWidthStackStyles: IStackStyles = { root: { width: 600 } }; export const mediumWidthStackStyles: IStackStyles = { root: { width: 600 } };
export const shortWidthTextFieldStyles: Partial<ITextFieldStyles> = { root: { paddingLeft: 10, width: 210 } }; export const shortWidthTextFieldStyles: Partial<ITextFieldStyles> = { root: { paddingLeft: 10, width: 210 } };

View File

@@ -239,7 +239,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
return ( return (
<Stack {...createAndAddMongoIndexStackProps} styles={mediumWidthStackStyles}> <Stack {...createAndAddMongoIndexStackProps} styles={mediumWidthStackStyles}>
<CollapsibleSectionComponent title="Current index(es)"> <CollapsibleSectionComponent title="Current index(es)" isExpandedByDefault={true}>
{ {
<> <>
<DetailsList <DetailsList
@@ -266,7 +266,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
return ( return (
<Stack styles={mediumWidthStackStyles}> <Stack styles={mediumWidthStackStyles}>
<CollapsibleSectionComponent title="Index(es) to be dropped"> <CollapsibleSectionComponent title="Index(es) to be dropped" isExpandedByDefault={true}>
{indexesToBeDropped.length > 0 && ( {indexesToBeDropped.length > 0 && (
<DetailsList <DetailsList
styles={customDetailsListStyles} styles={customDetailsListStyles}

View File

@@ -42,6 +42,7 @@ exports[`MongoIndexingPolicyComponent renders 1`] = `
} }
> >
<CollapsibleSectionComponent <CollapsibleSectionComponent
isExpandedByDefault={true}
title="Current index(es)" title="Current index(es)"
> >
<StyledWithViewportComponent <StyledWithViewportComponent
@@ -114,7 +115,7 @@ exports[`MongoIndexingPolicyComponent renders 1`] = `
</Stack> </Stack>
</CollapsibleSectionComponent> </CollapsibleSectionComponent>
</Stack> </Stack>
<Styled <Separator
styles={ styles={
Object { Object {
"root": Array [ "root": Array [
@@ -139,6 +140,7 @@ exports[`MongoIndexingPolicyComponent renders 1`] = `
} }
> >
<CollapsibleSectionComponent <CollapsibleSectionComponent
isExpandedByDefault={true}
title="Index(es) to be dropped" title="Index(es) to be dropped"
/> />
</Stack> </Stack>

View File

@@ -1,23 +1,24 @@
import { Label, Link, MessageBar, MessageBarType, Stack, Text, TextField } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import * as Constants from "../../../../Common/Constants"; import * as Constants from "../../../../Common/Constants";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component"; import { configContext, Platform } from "../../../../ConfigContext";
import * as ViewModels from "../../../../Contracts/ViewModels";
import * as DataModels from "../../../../Contracts/DataModels"; import * as DataModels from "../../../../Contracts/DataModels";
import * as ViewModels from "../../../../Contracts/ViewModels";
import * as SharedConstants from "../../../../Shared/Constants"; import * as SharedConstants from "../../../../Shared/Constants";
import { userContext } from "../../../../UserContext";
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
import Explorer from "../../../Explorer"; import Explorer from "../../../Explorer";
import { import {
getTextFieldStyles, getTextFieldStyles,
subComponentStackProps,
titleAndInputStackProps,
throughputUnit,
getThroughputApplyLongDelayMessage, getThroughputApplyLongDelayMessage,
getThroughputApplyShortDelayMessage, getThroughputApplyShortDelayMessage,
subComponentStackProps,
throughputUnit,
titleAndInputStackProps,
updateThroughputBeyondLimitWarningMessage, updateThroughputBeyondLimitWarningMessage,
} from "../SettingsRenderUtils"; } from "../SettingsRenderUtils";
import { hasDatabaseSharedThroughput } from "../SettingsUtils"; import { hasDatabaseSharedThroughput } from "../SettingsUtils";
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils"; import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
import { Link, Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
import { configContext, Platform } from "../../../../ConfigContext";
export interface ScaleComponentProps { export interface ScaleComponentProps {
collection: ViewModels.Collection; collection: ViewModels.Collection;
@@ -79,7 +80,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
}; };
public getMaxRUs = (): number => { public getMaxRUs = (): number => {
if (this.props.container?.isTryCosmosDBSubscription()) { if (userContext.isTryCosmosDBSubscription) {
return Constants.TryCosmosExperience.maxRU; return Constants.TryCosmosExperience.maxRU;
} }
@@ -91,7 +92,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
}; };
public getMinRUs = (): number => { public getMinRUs = (): number => {
if (this.props.container?.isTryCosmosDBSubscription()) { if (userContext.isTryCosmosDBSubscription) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs400; return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
} }
@@ -172,7 +173,6 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
databaseAccount={this.props.container.databaseAccount()} databaseAccount={this.props.container.databaseAccount()}
databaseName={this.databaseId} databaseName={this.databaseId}
collectionName={this.collectionId} collectionName={this.collectionId}
serverId={this.props.container.serverId()}
throughput={this.props.throughput} throughput={this.props.throughput}
throughputBaseline={this.props.throughputBaseline} throughputBaseline={this.props.throughputBaseline}
onThroughputChange={this.props.onThroughputChange} onThroughputChange={this.props.onThroughputChange}

View File

@@ -1,17 +1,16 @@
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react"; import React from "react";
import * as DataModels from "../../../../../Contracts/DataModels";
import { import {
ThroughputInputAutoPilotV3Component, ThroughputInputAutoPilotV3Component,
ThroughputInputAutoPilotV3Props, ThroughputInputAutoPilotV3Props,
} from "./ThroughputInputAutoPilotV3Component"; } from "./ThroughputInputAutoPilotV3Component";
import * as DataModels from "../../../../../Contracts/DataModels";
describe("ThroughputInputAutoPilotV3Component", () => { describe("ThroughputInputAutoPilotV3Component", () => {
const baseProps: ThroughputInputAutoPilotV3Props = { const baseProps: ThroughputInputAutoPilotV3Props = {
databaseAccount: {} as DataModels.DatabaseAccount, databaseAccount: {} as DataModels.DatabaseAccount,
databaseName: "test", databaseName: "test",
collectionName: "test", collectionName: "test",
serverId: undefined,
wasAutopilotOriginallySet: false, wasAutopilotOriginallySet: false,
throughput: 100, throughput: 100,
throughputBaseline: 100, throughputBaseline: 100,

View File

@@ -1,55 +1,52 @@
import React from "react";
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
import { import {
getTextFieldStyles,
getToolTipContainer,
noLeftPaddingCheckBoxStyle,
titleAndInputStackProps,
checkBoxAndInputStackProps,
getChoiceGroupStyles,
messageBarStyles,
getEstimatedSpendingElement,
getAutoPilotV3SpendElement,
manualToAutoscaleDisclaimerElement,
saveThroughputWarningMessage,
ManualEstimatedSpendingDisplayProps,
AutoscaleEstimatedSpendingDisplayProps,
PriceBreakdown,
getRuPriceBreakdown,
transparentDetailsHeaderStyle,
} from "../../SettingsRenderUtils";
import {
Text,
TextField,
ChoiceGroup,
IChoiceGroupOption,
Checkbox, Checkbox,
Stack, ChoiceGroup,
FontIcon,
IChoiceGroupOption,
IColumn,
Label, Label,
Link, Link,
MessageBar, MessageBar,
FontIcon, Stack,
IColumn, Text,
TextField,
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import { ToolTipLabelComponent } from "../ToolTipLabelComponent"; import React from "react";
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
import * as SharedConstants from "../../../../../Shared/Constants";
import * as DataModels from "../../../../../Contracts/DataModels"; import * as DataModels from "../../../../../Contracts/DataModels";
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import { userContext } from "../../../../../UserContext";
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType"; import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
import { usageInGB, calculateEstimateNumber } from "../../../../../Utils/PricingUtils"; import * as SharedConstants from "../../../../../Shared/Constants";
import { Features } from "../../../../../Common/Constants";
import { minAutoPilotThroughput } from "../../../../../Utils/AutoPilotUtils";
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../../../UserContext";
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
import { minAutoPilotThroughput } from "../../../../../Utils/AutoPilotUtils";
import { calculateEstimateNumber, usageInGB } from "../../../../../Utils/PricingUtils";
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import {
AutoscaleEstimatedSpendingDisplayProps,
checkBoxAndInputStackProps,
getAutoPilotV3SpendElement,
getChoiceGroupStyles,
getEstimatedSpendingElement,
getRuPriceBreakdown,
getTextFieldStyles,
getToolTipContainer,
ManualEstimatedSpendingDisplayProps,
manualToAutoscaleDisclaimerElement,
messageBarStyles,
noLeftPaddingCheckBoxStyle,
PriceBreakdown,
saveThroughputWarningMessage,
titleAndInputStackProps,
transparentDetailsHeaderStyle,
} from "../../SettingsRenderUtils";
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
export interface ThroughputInputAutoPilotV3Props { export interface ThroughputInputAutoPilotV3Props {
databaseAccount: DataModels.DatabaseAccount; databaseAccount: DataModels.DatabaseAccount;
databaseName: string; databaseName: string;
collectionName: string; collectionName: string;
serverId: string;
throughput: number; throughput: number;
throughputBaseline: number; throughputBaseline: number;
onThroughputChange: (newThroughput: number) => void; onThroughputChange: (newThroughput: number) => void;
@@ -182,7 +179,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
} }
const isDirty: boolean = this.IsComponentDirty().isDiscardable; const isDirty: boolean = this.IsComponentDirty().isDiscardable;
const serverId: string = this.props.serverId;
const regions = account?.properties?.readLocations?.length || 1; const regions = account?.properties?.readLocations?.length || 1;
const multimaster = account?.properties?.enableMultipleWriteLocations || false; const multimaster = account?.properties?.enableMultipleWriteLocations || false;
@@ -192,7 +188,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
estimatedSpend = this.getEstimatedManualSpendElement( estimatedSpend = this.getEstimatedManualSpendElement(
// if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set... // if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set...
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : this.props.throughputBaseline, this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : this.props.throughputBaseline,
serverId, userContext.portalEnv,
regions, regions,
multimaster, multimaster,
isDirty ? this.props.throughput : undefined isDirty ? this.props.throughput : undefined
@@ -200,7 +196,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
} else { } else {
estimatedSpend = this.getEstimatedAutoscaleSpendElement( estimatedSpend = this.getEstimatedAutoscaleSpendElement(
this.props.maxAutoPilotThroughputBaseline, this.props.maxAutoPilotThroughputBaseline,
serverId, userContext.portalEnv,
regions, regions,
multimaster, multimaster,
isDirty ? this.props.maxAutoPilotThroughput : undefined isDirty ? this.props.maxAutoPilotThroughput : undefined
@@ -468,7 +464,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`; const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`;
const oneTBinKB = 1000000000; const oneTBinKB = 1000000000;
const minRUperGB = 10; const minRUperGB = 10;
const featureFlagEnabled = window.dataExplorer?.isFeatureEnabled(Features.showMinRUSurvey); const featureFlagEnabled = userContext.features.showMinRUSurvey;
const collectionIsEligible = const collectionIsEligible =
userContext.subscriptionType !== SubscriptionType.Internal && userContext.subscriptionType !== SubscriptionType.Internal &&
this.props.usageSizeInKB > oneTBinKB && this.props.usageSizeInKB > oneTBinKB &&

View File

@@ -41,7 +41,7 @@ exports[`ToolTipLabelComponent renders 1`] = `
} }
} }
> >
<StyledIconBase <Icon
ariaLabel="Info" ariaLabel="Info"
iconName="Info" iconName="Info"
styles={ styles={

View File

@@ -1,23 +1,23 @@
import { collection } from "./TestUtils"; import ko from "knockout";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { import {
getMongoIndexType, getMongoIndexType,
getMongoIndexTypeText,
getMongoNotification, getMongoNotification,
getSanitizedInputValue, getSanitizedInputValue,
hasDatabaseSharedThroughput, hasDatabaseSharedThroughput,
isDirty, isDirty,
isIndexTransforming,
MongoIndexTypes, MongoIndexTypes,
MongoNotificationType, MongoNotificationType,
MongoWildcardPlaceHolder,
parseConflictResolutionMode, parseConflictResolutionMode,
parseConflictResolutionProcedure, parseConflictResolutionProcedure,
MongoWildcardPlaceHolder,
getMongoIndexTypeText,
SingleFieldText, SingleFieldText,
WildcardText, WildcardText,
isIndexTransforming,
} from "./SettingsUtils"; } from "./SettingsUtils";
import * as DataModels from "../../../Contracts/DataModels"; import { collection } from "./TestUtils";
import * as ViewModels from "../../../Contracts/ViewModels";
import ko from "knockout";
describe("SettingsUtils", () => { describe("SettingsUtils", () => {
it("hasDatabaseSharedThroughput", () => { it("hasDatabaseSharedThroughput", () => {
@@ -42,7 +42,6 @@ describe("SettingsUtils", () => {
loadCollections: undefined, loadCollections: undefined,
findCollectionWithId: undefined, findCollectionWithId: undefined,
openAddCollection: undefined, openAddCollection: undefined,
onDeleteDatabaseContextMenuClick: undefined,
readSettings: undefined, readSettings: undefined,
onSettingsClick: undefined, onSettingsClick: undefined,
loadOffer: undefined, loadOffer: undefined,

View File

@@ -1,7 +1,7 @@
import React from "react";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react";
import { DescriptionType, NumberUiType, SmartUiInput } from "../../../SelfServe/SelfServeTypes";
import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent"; import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent";
import { NumberUiType, SmartUiInput, DescriptionType } from "../../../SelfServe/SelfServeTypes";
describe("SmartUiComponent", () => { describe("SmartUiComponent", () => {
const exampleData: SmartUiDescriptor = { const exampleData: SmartUiDescriptor = {
@@ -97,9 +97,9 @@ describe("SmartUiComponent", () => {
dataFieldName: "database", dataFieldName: "database",
type: "object", type: "object",
choices: [ choices: [
{ label: "Database 1", key: "db1" }, { labelTKey: "Database 1", key: "db1" },
{ label: "Database 2", key: "db2" }, { labelTKey: "Database 2", key: "db2" },
{ label: "Database 3", key: "db3" }, { labelTKey: "Database 3", key: "db3" },
], ],
defaultKey: "db2", defaultKey: "db2",
}, },

View File

@@ -1,14 +1,13 @@
import * as React from "react"; import { TFunction } from "i18next";
import { Position } from "office-ui-fabric-react/lib/utilities/positioning"; import { Label, Link, MessageBar, MessageBarType, Toggle } from "office-ui-fabric-react";
import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
import { Slider } from "office-ui-fabric-react/lib/Slider"; import { Slider } from "office-ui-fabric-react/lib/Slider";
import { SpinButton } from "office-ui-fabric-react/lib/SpinButton"; import { SpinButton } from "office-ui-fabric-react/lib/SpinButton";
import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown"; import { IStackTokens, Stack } from "office-ui-fabric-react/lib/Stack";
import { TextField } from "office-ui-fabric-react/lib/TextField";
import { Text } from "office-ui-fabric-react/lib/Text"; import { Text } from "office-ui-fabric-react/lib/Text";
import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack"; import { TextField } from "office-ui-fabric-react/lib/TextField";
import { Label, Link, MessageBar, MessageBarType, Toggle } from "office-ui-fabric-react"; import { Position } from "office-ui-fabric-react/lib/utilities/positioning";
import * as InputUtils from "./InputUtils"; import * as React from "react";
import "./SmartUiComponent.less";
import { import {
ChoiceItem, ChoiceItem,
Description, Description,
@@ -19,8 +18,9 @@ import {
NumberUiType, NumberUiType,
SmartUiInput, SmartUiInput,
} from "../../../SelfServe/SelfServeTypes"; } from "../../../SelfServe/SelfServeTypes";
import { TFunction } from "i18next";
import { ToolTipLabelComponent } from "../Settings/SettingsSubComponents/ToolTipLabelComponent"; import { ToolTipLabelComponent } from "../Settings/SettingsSubComponents/ToolTipLabelComponent";
import * as InputUtils from "./InputUtils";
import "./SmartUiComponent.less";
/** /**
* Generic UX renderer * Generic UX renderer
@@ -138,11 +138,12 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
); );
} }
private renderTextInput(input: StringInput, labelId: string): JSX.Element { private renderTextInput(input: StringInput, labelId: string, labelElement: JSX.Element): JSX.Element {
const value = this.props.currentValues.get(input.dataFieldName)?.value as string; const value = this.props.currentValues.get(input.dataFieldName)?.value as string;
const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled; const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled;
return ( return (
<div className="stringInputContainer"> <Stack>
{labelElement}
<TextField <TextField
id={`${input.dataFieldName}-textField-input`} id={`${input.dataFieldName}-textField-input`}
aria-labelledby={labelId} aria-labelledby={labelId}
@@ -155,25 +156,32 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
root: { width: 400 }, root: { width: 400 },
}} }}
/> />
</div> </Stack>
); );
} }
private renderDescription(input: DescriptionDisplay, labelId: string): JSX.Element { private renderDescription(input: DescriptionDisplay, labelId: string, labelElement: JSX.Element): JSX.Element {
const dataFieldName = input.dataFieldName; const dataFieldName = input.dataFieldName;
const description = input.description || (this.props.currentValues.get(dataFieldName)?.value as Description); const description = input.description || (this.props.currentValues.get(dataFieldName)?.value as Description);
if (!description) { if (!description) {
return this.renderError("Description is not provided."); if (!input.isDynamicDescription) {
return this.renderError("Description is not provided.");
}
// If input is a dynamic description and description is not available, empty element is rendered
return <></>;
} }
const descriptionElement = ( const descriptionElement = (
<Text id={`${dataFieldName}-text-display`} aria-labelledby={labelId}> <Stack>
{this.props.getTranslation(description.textTKey)}{" "} {labelElement}
{description.link && ( <Text id={`${dataFieldName}-text-display`} aria-labelledby={labelId}>
<Link target="_blank" href={description.link.href}> {this.props.getTranslation(description.textTKey)}{" "}
{this.props.getTranslation(description.link.textTKey)} {description.link && (
</Link> <Link target="_blank" href={description.link.href}>
)} {this.props.getTranslation(description.link.textTKey)}
</Text> </Link>
)}
</Text>
</Stack>
); );
if (description.type === DescriptionType.Text) { if (description.type === DescriptionType.Text) {
@@ -227,7 +235,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return undefined; return undefined;
}; };
private renderNumberInput(input: NumberInput, labelId: string): JSX.Element { private renderNumberInput(input: NumberInput, labelId: string, labelElement: JSX.Element): JSX.Element {
const { labelTKey, min, max, dataFieldName, step } = input; const { labelTKey, min, max, dataFieldName, step } = input;
const props = { const props = {
min: min, min: min,
@@ -240,61 +248,72 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled; const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
if (input.uiType === NumberUiType.Spinner) { if (input.uiType === NumberUiType.Spinner) {
return ( return (
<Stack styles={{ root: { width: 400 } }} tokens={{ childrenGap: 2 }}> <Stack>
<SpinButton {labelElement}
{...props} <Stack styles={{ root: { width: 400 } }} tokens={{ childrenGap: 2 }}>
id={`${input.dataFieldName}-spinner-input`} <SpinButton
value={value?.toString()} {...props}
onValidate={(newValue) => this.onValidate(input, newValue, props.min, props.max)} id={`${input.dataFieldName}-spinner-input`}
onIncrement={(newValue) => this.onIncrement(input, newValue, props.step, props.max)} value={value?.toString()}
onDecrement={(newValue) => this.onDecrement(input, newValue, props.step, props.min)} onValidate={(newValue) => this.onValidate(input, newValue, props.min, props.max)}
labelPosition={Position.top} onIncrement={(newValue) => this.onIncrement(input, newValue, props.step, props.max)}
aria-labelledby={labelId} onDecrement={(newValue) => this.onDecrement(input, newValue, props.step, props.min)}
disabled={disabled} labelPosition={Position.top}
/> aria-labelledby={labelId}
{this.state.errors.has(dataFieldName) && ( disabled={disabled}
<MessageBar messageBarType={MessageBarType.error}>Error: {this.state.errors.get(dataFieldName)}</MessageBar> />
)} {this.state.errors.has(dataFieldName) && (
<MessageBar messageBarType={MessageBarType.error}>
Error: {this.state.errors.get(dataFieldName)}
</MessageBar>
)}
</Stack>
</Stack> </Stack>
); );
} else if (input.uiType === NumberUiType.Slider) { } else if (input.uiType === NumberUiType.Slider) {
return ( return (
<div id={`${input.dataFieldName}-slider-input`}> <Stack>
<Slider {labelElement}
{...props} <div id={`${input.dataFieldName}-slider-input`}>
value={value} <Slider
disabled={disabled} {...props}
onChange={(newValue) => this.props.onInputChange(input, newValue)} value={value}
styles={{ disabled={disabled}
root: { width: 400 }, onChange={(newValue) => this.props.onInputChange(input, newValue)}
valueLabel: SmartUiComponent.labelStyle, styles={{
}} root: { width: 400 },
/> valueLabel: SmartUiComponent.labelStyle,
</div> }}
/>
</div>
</Stack>
); );
} else { } else {
return <>Unsupported number UI type {input.uiType}</>; return <>Unsupported number UI type {input.uiType}</>;
} }
} }
private renderBooleanInput(input: BooleanInput, labelId: string): JSX.Element { private renderBooleanInput(input: BooleanInput, labelId: string, labelElement: JSX.Element): JSX.Element {
const value = this.props.currentValues.get(input.dataFieldName)?.value as boolean; const value = this.props.currentValues.get(input.dataFieldName)?.value as boolean;
const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled; const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled;
return ( return (
<Toggle <Stack>
id={`${input.dataFieldName}-toggle-input`} {labelElement}
aria-labelledby={labelId} <Toggle
checked={value || false} id={`${input.dataFieldName}-toggle-input`}
onText={this.props.getTranslation(input.trueLabelTKey)} aria-labelledby={labelId}
offText={this.props.getTranslation(input.falseLabelTKey)} checked={value || false}
disabled={disabled} onText={this.props.getTranslation(input.trueLabelTKey)}
onChange={(event, checked: boolean) => this.props.onInputChange(input, checked)} offText={this.props.getTranslation(input.falseLabelTKey)}
styles={{ root: { width: 400 } }} disabled={disabled}
/> onChange={(event, checked: boolean) => this.props.onInputChange(input, checked)}
styles={{ root: { width: 400 } }}
/>
</Stack>
); );
} }
private renderChoiceInput(input: ChoiceInput, labelId: string): JSX.Element { private renderChoiceInput(input: ChoiceInput, labelId: string, labelElement: JSX.Element): JSX.Element {
const { defaultKey, dataFieldName, choices, placeholderTKey } = input; const { defaultKey, dataFieldName, choices, placeholderTKey } = input;
const value = this.props.currentValues.get(dataFieldName)?.value as string; const value = this.props.currentValues.get(dataFieldName)?.value as string;
const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled; const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
@@ -303,22 +322,26 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
selectedKey = ""; selectedKey = "";
} }
return ( return (
<Dropdown <Stack>
id={`${input.dataFieldName}-dropdown-input`} {labelElement}
aria-labelledby={labelId} <Dropdown
selectedKey={selectedKey} id={`${input.dataFieldName}-dropdown-input`}
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())} aria-labelledby={labelId}
placeholder={this.props.getTranslation(placeholderTKey)} selectedKey={selectedKey}
disabled={disabled} onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
options={choices.map((c) => ({ placeholder={this.props.getTranslation(placeholderTKey)}
key: c.key, disabled={disabled}
text: this.props.getTranslation(c.label), // Removed dropdownWidth="auto" as dropdown accept only number
}))} options={choices.map((c) => ({
styles={{ key: c.key,
root: { width: 400 }, text: this.props.getTranslation(c.labelTKey),
dropdown: SmartUiComponent.labelStyle, }))}
}} styles={{
/> root: { width: 400 },
dropdown: SmartUiComponent.labelStyle,
}}
/>
</Stack>
); );
} }
@@ -326,7 +349,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return <MessageBar messageBarType={MessageBarType.error}>Error: {errorMessage}</MessageBar>; return <MessageBar messageBarType={MessageBarType.error}>Error: {errorMessage}</MessageBar>;
} }
private renderDisplayWithInfoBubble(input: AnyDisplay, info: Info): JSX.Element { private renderElement(input: AnyDisplay, info: Info): JSX.Element {
if (input.errorMessage) { if (input.errorMessage) {
return this.renderError(input.errorMessage); return this.renderError(input.errorMessage);
} }
@@ -335,34 +358,31 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return <></>; return <></>;
} }
const labelId = `${input.dataFieldName}-label`; const labelId = `${input.dataFieldName}-label`;
return ( const labelElement: JSX.Element = input.labelTKey && (
<Stack> <Label id={labelId}>
{input.labelTKey && ( <ToolTipLabelComponent
<Label id={labelId}> label={this.props.getTranslation(input.labelTKey)}
<ToolTipLabelComponent toolTipElement={this.renderInfo(info)}
label={this.props.getTranslation(input.labelTKey)} />
toolTipElement={this.renderInfo(info)} </Label>
/>
</Label>
)}
{this.renderDisplay(input, labelId)}
</Stack>
); );
return <Stack>{this.renderControl(input, labelId, labelElement)}</Stack>;
} }
private renderDisplay(input: AnyDisplay, labelId: string): JSX.Element { private renderControl(input: AnyDisplay, labelId: string, labelElement: JSX.Element): JSX.Element {
switch (input.type) { switch (input.type) {
case "string": case "string":
if ("description" in input || "isDynamicDescription" in input) { if ("description" in input || "isDynamicDescription" in input) {
return this.renderDescription(input as DescriptionDisplay, labelId); return this.renderDescription(input as DescriptionDisplay, labelId, labelElement);
} }
return this.renderTextInput(input as StringInput, labelId); return this.renderTextInput(input as StringInput, labelId, labelElement);
case "number": case "number":
return this.renderNumberInput(input as NumberInput, labelId); return this.renderNumberInput(input as NumberInput, labelId, labelElement);
case "boolean": case "boolean":
return this.renderBooleanInput(input as BooleanInput, labelId); return this.renderBooleanInput(input as BooleanInput, labelId, labelElement);
case "object": case "object":
return this.renderChoiceInput(input as ChoiceInput, labelId); return this.renderChoiceInput(input as ChoiceInput, labelId, labelElement);
default: default:
throw new Error(`Unknown input type: ${input.type}`); throw new Error(`Unknown input type: ${input.type}`);
} }
@@ -373,7 +393,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return ( return (
<Stack tokens={containerStackTokens} className="widgetRendererContainer"> <Stack tokens={containerStackTokens} className="widgetRendererContainer">
<Stack.Item>{node.input && this.renderDisplayWithInfoBubble(node.input, node.info as Info)}</Stack.Item> <Stack.Item>{node.input && this.renderElement(node.input, node.info as Info)}</Stack.Item>
{node.children && node.children.map((child) => <div key={child.id}>{this.renderNode(child)}</div>)} {node.children && node.children.map((child) => <div key={child.id}>{this.renderNode(child)}</div>)}
</Stack> </Stack>
); );

View File

@@ -23,19 +23,21 @@ exports[`SmartUiComponent disable all inputs 1`] = `
> >
<StackItem> <StackItem>
<Stack> <Stack>
<Text <Stack>
aria-labelledby="description-label" <Text
id="description-text-display" aria-labelledby="description-label"
> id="description-text-display"
this is an example description text.
<StyledLinkBase
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
target="_blank"
> >
Click here for more information. this is an example description text.
</StyledLinkBase>
</Text> <StyledLinkBase
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
target="_blank"
>
Click here for more information.
</StyledLinkBase>
</Text>
</Stack>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -53,51 +55,53 @@ exports[`SmartUiComponent disable all inputs 1`] = `
> >
<StackItem> <StackItem>
<Stack> <Stack>
<StyledLabelBase <Stack>
id="throughput-label" <StyledLabelBase
> id="throughput-label"
<ToolTipLabelComponent >
label="Throughput (input)" <ToolTipLabelComponent
/> label="Throughput (input)"
</StyledLabelBase> />
<Stack </StyledLabelBase>
styles={ <Stack
Object { styles={
"root": Object {
"width": 400,
},
}
}
tokens={
Object {
"childrenGap": 2,
}
}
>
<CustomizedSpinButton
aria-labelledby="throughput-label"
ariaLabel="Throughput (input)"
decrementButtonIcon={
Object { Object {
"iconName": "ChevronDownSmall", "root": Object {
"width": 400,
},
} }
} }
disabled={true} tokens={
id="throughput-spinner-input"
incrementButtonIcon={
Object { Object {
"iconName": "ChevronUpSmall", "childrenGap": 2,
} }
} }
label="" >
labelPosition={0} <CustomizedSpinButton
max={500} aria-labelledby="throughput-label"
min={400} ariaLabel="Throughput (input)"
onDecrement={[Function]} decrementButtonIcon={
onIncrement={[Function]} Object {
onValidate={[Function]} "iconName": "ChevronDownSmall",
step={10} }
/> }
disabled={true}
id="throughput-spinner-input"
incrementButtonIcon={
Object {
"iconName": "ChevronUpSmall",
}
}
label=""
labelPosition={0}
max={500}
min={400}
onDecrement={[Function]}
onIncrement={[Function]}
onValidate={[Function]}
step={10}
/>
</Stack>
</Stack> </Stack>
</Stack> </Stack>
</StackItem> </StackItem>
@@ -116,37 +120,39 @@ exports[`SmartUiComponent disable all inputs 1`] = `
> >
<StackItem> <StackItem>
<Stack> <Stack>
<StyledLabelBase <Stack>
id="throughput2-label" <StyledLabelBase
> id="throughput2-label"
<ToolTipLabelComponent >
label="Throughput (Slider)" <ToolTipLabelComponent
/> label="Throughput (Slider)"
</StyledLabelBase> />
<div </StyledLabelBase>
id="throughput2-slider-input" <div
> id="throughput2-slider-input"
<StyledSliderBase >
ariaLabel="Throughput (Slider)" <StyledSliderBase
disabled={true} ariaLabel="Throughput (Slider)"
max={500} disabled={true}
min={400} max={500}
onChange={[Function]} min={400}
step={10} onChange={[Function]}
styles={ step={10}
Object { styles={
"root": Object { Object {
"width": 400, "root": Object {
}, "width": 400,
"valueLabel": Object { },
"color": "#393939", "valueLabel": Object {
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "color": "#393939",
"fontSize": 12, "fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
}, "fontSize": 12,
},
}
} }
} />
/> </div>
</div> </Stack>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -185,16 +191,14 @@ exports[`SmartUiComponent disable all inputs 1`] = `
> >
<StackItem> <StackItem>
<Stack> <Stack>
<StyledLabelBase <Stack>
id="containerId-label" <StyledLabelBase
> id="containerId-label"
<ToolTipLabelComponent >
label="Container id" <ToolTipLabelComponent
/> label="Container id"
</StyledLabelBase> />
<div </StyledLabelBase>
className="stringInputContainer"
>
<StyledTextFieldBase <StyledTextFieldBase
aria-labelledby="containerId-label" aria-labelledby="containerId-label"
disabled={true} disabled={true}
@@ -210,7 +214,7 @@ exports[`SmartUiComponent disable all inputs 1`] = `
type="text" type="text"
value="" value=""
/> />
</div> </Stack>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -228,29 +232,31 @@ exports[`SmartUiComponent disable all inputs 1`] = `
> >
<StackItem> <StackItem>
<Stack> <Stack>
<StyledLabelBase <Stack>
id="analyticalStore-label" <StyledLabelBase
> id="analyticalStore-label"
<ToolTipLabelComponent >
label="Analytical Store" <ToolTipLabelComponent
/> label="Analytical Store"
</StyledLabelBase> />
<StyledToggleBase </StyledLabelBase>
aria-labelledby="analyticalStore-label" <StyledToggleBase
checked={false} aria-labelledby="analyticalStore-label"
disabled={true} checked={false}
id="analyticalStore-toggle-input" disabled={true}
offText="Disabled" id="analyticalStore-toggle-input"
onChange={[Function]} offText="Disabled"
onText="Enabled" onChange={[Function]}
styles={ onText="Enabled"
Object { styles={
"root": Object { Object {
"width": 400, "root": Object {
}, "width": 400,
},
}
} }
} />
/> </Stack>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -268,48 +274,50 @@ exports[`SmartUiComponent disable all inputs 1`] = `
> >
<StackItem> <StackItem>
<Stack> <Stack>
<StyledLabelBase <Stack>
id="database-label" <StyledLabelBase
> id="database-label"
<ToolTipLabelComponent >
label="Database" <ToolTipLabelComponent
/> label="Database"
</StyledLabelBase> />
<StyledWithResponsiveMode </StyledLabelBase>
aria-labelledby="database-label" <StyledWithResponsiveMode
disabled={true} aria-labelledby="database-label"
id="database-dropdown-input" disabled={true}
onChange={[Function]} id="database-dropdown-input"
options={ onChange={[Function]}
Array [ options={
Object { Array [
"key": "db1", Object {
"text": "Database 1", "key": "db1",
}, "text": "Database 1",
Object { },
"key": "db2", Object {
"text": "Database 2", "key": "db2",
}, "text": "Database 2",
Object { },
"key": "db3", Object {
"text": "Database 3", "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,
},
"root": Object {
"width": 400,
},
} }
} selectedKey="db2"
/> styles={
Object {
"dropdown": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
"root": Object {
"width": 400,
},
}
}
/>
</Stack>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -340,19 +348,21 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
> >
<StackItem> <StackItem>
<Stack> <Stack>
<Text <Stack>
aria-labelledby="description-label" <Text
id="description-text-display" aria-labelledby="description-label"
> id="description-text-display"
this is an example description text.
<StyledLinkBase
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
target="_blank"
> >
Click here for more information. this is an example description text.
</StyledLinkBase>
</Text> <StyledLinkBase
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
target="_blank"
>
Click here for more information.
</StyledLinkBase>
</Text>
</Stack>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -370,51 +380,53 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
> >
<StackItem> <StackItem>
<Stack> <Stack>
<StyledLabelBase <Stack>
id="throughput-label" <StyledLabelBase
> id="throughput-label"
<ToolTipLabelComponent >
label="Throughput (input)" <ToolTipLabelComponent
/> label="Throughput (input)"
</StyledLabelBase> />
<Stack </StyledLabelBase>
styles={ <Stack
Object { styles={
"root": Object {
"width": 400,
},
}
}
tokens={
Object {
"childrenGap": 2,
}
}
>
<CustomizedSpinButton
aria-labelledby="throughput-label"
ariaLabel="Throughput (input)"
decrementButtonIcon={
Object { Object {
"iconName": "ChevronDownSmall", "root": Object {
"width": 400,
},
} }
} }
disabled={false} tokens={
id="throughput-spinner-input"
incrementButtonIcon={
Object { Object {
"iconName": "ChevronUpSmall", "childrenGap": 2,
} }
} }
label="" >
labelPosition={0} <CustomizedSpinButton
max={500} aria-labelledby="throughput-label"
min={400} ariaLabel="Throughput (input)"
onDecrement={[Function]} decrementButtonIcon={
onIncrement={[Function]} Object {
onValidate={[Function]} "iconName": "ChevronDownSmall",
step={10} }
/> }
disabled={false}
id="throughput-spinner-input"
incrementButtonIcon={
Object {
"iconName": "ChevronUpSmall",
}
}
label=""
labelPosition={0}
max={500}
min={400}
onDecrement={[Function]}
onIncrement={[Function]}
onValidate={[Function]}
step={10}
/>
</Stack>
</Stack> </Stack>
</Stack> </Stack>
</StackItem> </StackItem>
@@ -433,36 +445,38 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
> >
<StackItem> <StackItem>
<Stack> <Stack>
<StyledLabelBase <Stack>
id="throughput2-label" <StyledLabelBase
> id="throughput2-label"
<ToolTipLabelComponent >
label="Throughput (Slider)" <ToolTipLabelComponent
/> label="Throughput (Slider)"
</StyledLabelBase> />
<div </StyledLabelBase>
id="throughput2-slider-input" <div
> id="throughput2-slider-input"
<StyledSliderBase >
ariaLabel="Throughput (Slider)" <StyledSliderBase
max={500} ariaLabel="Throughput (Slider)"
min={400} max={500}
onChange={[Function]} min={400}
step={10} onChange={[Function]}
styles={ step={10}
Object { styles={
"root": Object { Object {
"width": 400, "root": Object {
}, "width": 400,
"valueLabel": Object { },
"color": "#393939", "valueLabel": Object {
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "color": "#393939",
"fontSize": 12, "fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
}, "fontSize": 12,
},
}
} }
} />
/> </div>
</div> </Stack>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -501,16 +515,14 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
> >
<StackItem> <StackItem>
<Stack> <Stack>
<StyledLabelBase <Stack>
id="containerId-label" <StyledLabelBase
> id="containerId-label"
<ToolTipLabelComponent >
label="Container id" <ToolTipLabelComponent
/> label="Container id"
</StyledLabelBase> />
<div </StyledLabelBase>
className="stringInputContainer"
>
<StyledTextFieldBase <StyledTextFieldBase
aria-labelledby="containerId-label" aria-labelledby="containerId-label"
id="containerId-textField-input" id="containerId-textField-input"
@@ -525,7 +537,7 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
type="text" type="text"
value="" value=""
/> />
</div> </Stack>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -543,28 +555,30 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
> >
<StackItem> <StackItem>
<Stack> <Stack>
<StyledLabelBase <Stack>
id="analyticalStore-label" <StyledLabelBase
> id="analyticalStore-label"
<ToolTipLabelComponent >
label="Analytical Store" <ToolTipLabelComponent
/> label="Analytical Store"
</StyledLabelBase> />
<StyledToggleBase </StyledLabelBase>
aria-labelledby="analyticalStore-label" <StyledToggleBase
checked={false} aria-labelledby="analyticalStore-label"
id="analyticalStore-toggle-input" checked={false}
offText="Disabled" id="analyticalStore-toggle-input"
onChange={[Function]} offText="Disabled"
onText="Enabled" onChange={[Function]}
styles={ onText="Enabled"
Object { styles={
"root": Object { Object {
"width": 400, "root": Object {
}, "width": 400,
},
}
} }
} />
/> </Stack>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -582,47 +596,49 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
> >
<StackItem> <StackItem>
<Stack> <Stack>
<StyledLabelBase <Stack>
id="database-label" <StyledLabelBase
> id="database-label"
<ToolTipLabelComponent >
label="Database" <ToolTipLabelComponent
/> label="Database"
</StyledLabelBase> />
<StyledWithResponsiveMode </StyledLabelBase>
aria-labelledby="database-label" <StyledWithResponsiveMode
id="database-dropdown-input" aria-labelledby="database-label"
onChange={[Function]} id="database-dropdown-input"
options={ onChange={[Function]}
Array [ options={
Object { Array [
"key": "db1", Object {
"text": "Database 1", "key": "db1",
}, "text": "Database 1",
Object { },
"key": "db2", Object {
"text": "Database 2", "key": "db2",
}, "text": "Database 2",
Object { },
"key": "db3", Object {
"text": "Database 3", "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,
},
"root": Object {
"width": 400,
},
} }
} selectedKey="db2"
/> styles={
Object {
"dropdown": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
"root": Object {
"width": 400,
},
}
}
/>
</Stack>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>

View File

@@ -0,0 +1,20 @@
@import "../../../../less/Common/Constants";
.throughputInputContainer {
.throughputInputRadioBtn {
margin: 0;
}
}
.throughputInputRadioBtnLabel {
font-size: @mediumFontSize;
padding: 0 @LargeSpace 0 @SmallSpace;
}
.throughputInputSpacing {
margin-bottom: @SmallSpace;
& > * {
margin-bottom: @SmallSpace;
}
}

View File

@@ -0,0 +1,302 @@
import { Checkbox, DirectionalHint, Icon, Link, Stack, Text, TextField, TooltipHost } from "office-ui-fabric-react";
import React from "react";
import * as Constants from "../../../Common/Constants";
import * as SharedConstants from "../../../Shared/Constants";
import { userContext } from "../../../UserContext";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../../Utils/PricingUtils";
export interface ThroughputInputProps {
isDatabase: boolean;
showFreeTierExceedThroughputTooltip: boolean;
setThroughputValue: (throughput: number) => void;
setIsAutoscale: (isAutoscale: boolean) => void;
onCostAcknowledgeChange: (isAcknowledged: boolean) => void;
}
export interface ThroughputInputState {
isAutoscaleSelected: boolean;
throughput: number;
isCostAcknowledged: boolean;
}
export class ThroughputInput extends React.Component<ThroughputInputProps, ThroughputInputState> {
constructor(props: ThroughputInputProps) {
super(props);
this.state = {
isAutoscaleSelected: true,
throughput: AutoPilotUtils.minAutoPilotThroughput,
isCostAcknowledged: false,
};
this.props.setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
this.props.setIsAutoscale(true);
}
render(): JSX.Element {
return (
<div className="throughputInputContainer throughputInputSpacing">
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text variant="small" style={{ lineHeight: "20px" }}>
{this.getThroughputLabelText()}
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={PricingUtils.getRuToolTipText()}>
<Icon iconName="InfoSolid" className="panelInfoIcon" />
</TooltipHost>
</Stack>
<Stack horizontal verticalAlign="center">
<input
className="throughputInputRadioBtn"
aria-label="Autoscale mode"
checked={this.state.isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={this.onAutoscaleRadioBtnChange.bind(this)}
/>
<span className="throughputInputRadioBtnLabel">Autoscale</span>
<input
className="throughputInputRadioBtn"
aria-label="Manual mode"
checked={!this.state.isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={this.onManualRadioBtnChange.bind(this)}
/>
<span className="throughputInputRadioBtnLabel">Manual</span>
</Stack>
{this.state.isAutoscaleSelected && (
<Stack className="throughputInputSpacing">
<Text variant="small">
Provision maximum RU/s required by this resource. Estimate your required RU/s with&nbsp;
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
capacity calculator
</Link>
.
</Text>
<Stack horizontal>
<Text variant="small" style={{ lineHeight: "20px" }}>
Max RU/s
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={this.getAutoScaleTooltip()}>
<Icon iconName="InfoSolid" className="panelInfoIcon" />
</TooltipHost>
</Stack>
<TextField
type="number"
styles={{
fieldGroup: { width: 300, height: 27 },
field: { fontSize: 12 },
}}
onChange={(event, newInput?: string) => this.onThroughputValueChange(newInput)}
step={AutoPilotUtils.autoPilotIncrementStep}
min={AutoPilotUtils.minAutoPilotThroughput}
value={this.state.throughput.toString()}
aria-label="Max request units per second"
required={true}
/>
<Text variant="small">
Your {this.props.isDatabase ? "database" : "container"} throughput will automatically scale from{" "}
<b>
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.state.throughput)} RU/s (10% of max RU/s) -{" "}
{this.state.throughput} RU/s
</b>{" "}
based on usage.
</Text>
</Stack>
)}
{!this.state.isAutoscaleSelected && (
<Stack className="throughputInputSpacing">
<Text variant="small">
Estimate your required RU/s with&nbsp;
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
capacity calculator
</Link>
.
</Text>
<TooltipHost
directionalHint={DirectionalHint.topLeftEdge}
content={
this.props.showFreeTierExceedThroughputTooltip &&
this.state.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs400
? "The first 400 RU/s in this account are free. Billing will apply to any throughput beyond 400 RU/s."
: undefined
}
>
<TextField
type="number"
styles={{
fieldGroup: { width: 300, height: 27 },
field: { fontSize: 12 },
}}
onChange={(event, newInput?: string) => this.onThroughputValueChange(newInput)}
step={100}
min={SharedConstants.CollectionCreation.DefaultCollectionRUs400}
max={userContext.isTryCosmosDBSubscription ? Constants.TryCosmosExperience.maxRU : Infinity}
value={this.state.throughput.toString()}
aria-label="Max request units per second"
required={true}
/>
</TooltipHost>
</Stack>
)}
<CostEstimateText requestUnits={this.state.throughput} isAutoscale={this.state.isAutoscaleSelected} />
{this.state.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && (
<Stack horizontal verticalAlign="start">
<Checkbox
checked={this.state.isCostAcknowledged}
styles={{
checkbox: { width: 12, height: 12 },
label: { padding: 0, margin: "4px 4px 0 0" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => {
this.setState({ isCostAcknowledged: isChecked });
this.props.onCostAcknowledgeChange(isChecked);
}}
/>
<Text variant="small" style={{ lineHeight: "20px" }}>
{this.getCostAcknowledgeText()}
</Text>
</Stack>
)}
</div>
);
}
private getThroughputLabelText(): string {
if (this.state.isAutoscaleSelected) {
return AutoPilotUtils.getAutoPilotHeaderText();
}
const minRU: string = SharedConstants.CollectionCreation.DefaultCollectionRUs400.toLocaleString();
const maxRU: string = userContext.isTryCosmosDBSubscription
? Constants.TryCosmosExperience.maxRU.toLocaleString()
: "unlimited";
return this.state.isAutoscaleSelected
? AutoPilotUtils.getAutoPilotHeaderText()
: `Throughput (${minRU} - ${maxRU} RU/s)`;
}
private onThroughputValueChange(newInput: string): void {
const newThroughput = parseInt(newInput);
this.setState({ throughput: newThroughput });
this.props.setThroughputValue(newThroughput);
}
private getAutoScaleTooltip(): string {
return `After the first ${AutoPilotUtils.getStorageBasedOnUserInput(
this.state.throughput
)} GB of data stored, the max
RU/s will be automatically upgraded based on the new storage value.`;
}
private getCostAcknowledgeText(): string {
const databaseAccount = userContext.databaseAccount;
if (!databaseAccount || !databaseAccount.properties) {
return "";
}
const numberOfRegions: number = databaseAccount.properties.readLocations?.length || 1;
const multimasterEnabled: boolean = databaseAccount.properties.enableMultipleWriteLocations;
return PricingUtils.getEstimatedSpendAcknowledgeString(
this.state.throughput,
userContext.portalEnv,
numberOfRegions,
multimasterEnabled,
this.state.isAutoscaleSelected
);
}
private onAutoscaleRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
if (event.target.checked && !this.state.isAutoscaleSelected) {
this.setState({ isAutoscaleSelected: true, throughput: AutoPilotUtils.minAutoPilotThroughput });
this.props.setIsAutoscale(true);
}
}
private onManualRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
if (event.target.checked && this.state.isAutoscaleSelected) {
this.setState({
isAutoscaleSelected: false,
throughput: SharedConstants.CollectionCreation.DefaultCollectionRUs400,
});
this.props.setIsAutoscale(false);
this.props.setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
}
}
}
interface CostEstimateTextProps {
requestUnits: number;
isAutoscale: boolean;
}
const CostEstimateText: React.FunctionComponent<CostEstimateTextProps> = (props: CostEstimateTextProps) => {
const { requestUnits, isAutoscale } = props;
const databaseAccount = userContext.databaseAccount;
if (!databaseAccount || !databaseAccount.properties) {
return <></>;
}
const serverId: string = userContext.portalEnv;
const numberOfRegions: number = databaseAccount.properties.readLocations?.length || 1;
const multimasterEnabled: boolean = databaseAccount.properties.enableMultipleWriteLocations;
const hourlyPrice: number = PricingUtils.computeRUUsagePriceHourly({
serverId,
requestUnits,
numberOfRegions,
multimasterEnabled,
isAutoscale,
});
const dailyPrice: number = hourlyPrice * 24;
const monthlyPrice: number = hourlyPrice * SharedConstants.hoursInAMonth;
const currency: string = PricingUtils.getPriceCurrency(serverId);
const currencySign: string = PricingUtils.getCurrencySign(serverId);
const multiplier = PricingUtils.getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
const pricePerRu = isAutoscale
? PricingUtils.getAutoscalePricePerRu(serverId, multiplier) * multiplier
: PricingUtils.getPricePerRu(serverId) * multiplier;
if (isAutoscale) {
return (
<Text variant="small">
Estimated monthly cost ({currency}):{" "}
<b>
{currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice / 10)} -{" "}
{currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice)}{" "}
</b>
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits / 10} - {requestUnits}{" "}
RU/s, {currencySign + pricePerRu}/RU)
</Text>
);
}
return (
<Text variant="small">
Cost ({currency}):{" "}
<b>
{currencySign + PricingUtils.calculateEstimateNumber(hourlyPrice)} hourly /{" "}
{currencySign + PricingUtils.calculateEstimateNumber(dailyPrice)} daily /{" "}
{currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice)} monthly{" "}
</b>
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "}
{currencySign + pricePerRu}/RU)
<br />
<em>{PricingUtils.estimatedCostDisclaimer}</em>
</Text>
);
};

View File

@@ -2,17 +2,17 @@ jest.mock("../Graph/GraphExplorerComponent/GremlinClient");
jest.mock("../../Common/dataAccess/createCollection"); jest.mock("../../Common/dataAccess/createCollection");
jest.mock("../../Common/dataAccess/createDocument"); jest.mock("../../Common/dataAccess/createDocument");
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import Q from "q"; import Q from "q";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument } from "../../Common/dataAccess/createDocument";
import Explorer from "../Explorer"; import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
describe("ContainerSampleGenerator", () => { describe("ContainerSampleGenerator", () => {
const createExplorerStub = (database: ViewModels.Database): Explorer => { const createExplorerStub = (database: ViewModels.Database): Explorer => {
const explorerStub = {} as Explorer; const explorerStub = {} as Explorer;
explorerStub.nonSystemDatabases = ko.computed(() => [database]); explorerStub.databases = ko.observableArray<ViewModels.Database>([database]);
explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => false); explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false); explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiDocumentDB = ko.computed<boolean>(() => false); explorerStub.isPreferredApiDocumentDB = ko.computed<boolean>(() => false);

View File

@@ -1,9 +1,9 @@
import { DataSamplesUtil } from "./DataSamplesUtil";
import * as sinon from "sinon";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import * as ko from "knockout"; import * as ko from "knockout";
import * as sinon from "sinon";
import { Collection, Database } from "../../Contracts/ViewModels";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { Database, Collection } from "../../Contracts/ViewModels"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { DataSamplesUtil } from "./DataSamplesUtil";
describe("DataSampleUtils", () => { describe("DataSampleUtils", () => {
const sampleCollectionId = "sampleCollectionId"; const sampleCollectionId = "sampleCollectionId";
@@ -16,7 +16,7 @@ describe("DataSampleUtils", () => {
collections: ko.observableArray<Collection>([collection]), collections: ko.observableArray<Collection>([collection]),
} as Database; } as Database;
const explorer = {} as Explorer; const explorer = {} as Explorer;
explorer.nonSystemDatabases = ko.computed(() => [database]); explorer.databases = ko.observableArray<Database>([database]);
explorer.showOkModalDialog = () => {}; explorer.showOkModalDialog = () => {};
const dataSamplesUtil = new DataSamplesUtil(explorer); const dataSamplesUtil = new DataSamplesUtil(explorer);

View File

@@ -1,8 +1,8 @@
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
export class DataSamplesUtil { export class DataSamplesUtil {
private static readonly DialogTitle = "Create Sample Container"; private static readonly DialogTitle = "Create Sample Container";
@@ -17,7 +17,7 @@ export class DataSamplesUtil {
const databaseName = generator.getDatabaseId(); const databaseName = generator.getDatabaseId();
const containerName = generator.getCollectionId(); const containerName = generator.getCollectionId();
if (this.hasContainer(databaseName, containerName, this.container.nonSystemDatabases())) { if (this.hasContainer(databaseName, containerName, this.container.databases())) {
const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`; const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`;
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);

View File

@@ -0,0 +1,43 @@
jest.mock("./../Common/dataAccess/deleteDatabase");
jest.mock("./../Shared/Telemetry/TelemetryProcessor");
import * as ko from "knockout";
import { deleteDatabase } from "./../Common/dataAccess/deleteDatabase";
import * as ViewModels from "./../Contracts/ViewModels";
import Explorer from "./Explorer";
describe("Explorer.isLastDatabase() and Explorer.isLastNonEmptyDatabase()", () => {
let explorer: Explorer;
beforeAll(() => {
(deleteDatabase as jest.Mock).mockResolvedValue(undefined);
});
beforeEach(() => {
explorer = new Explorer();
});
it("should be true if only 1 database", () => {
const database = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastDatabase()).toBe(true);
});
it("should be false if only 2 databases", () => {
const database = {} as ViewModels.Database;
const database2 = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database, database2]);
expect(explorer.isLastDatabase()).toBe(false);
});
it("should be false if not last empty database", () => {
const database = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastNonEmptyDatabase()).toBe(false);
});
it("should be true if last non empty database", () => {
const database = {} as ViewModels.Database;
database.collections = ko.observableArray<ViewModels.Collection>([{} as ViewModels.Collection]);
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastNonEmptyDatabase()).toBe(true);
});
});

View File

@@ -44,18 +44,19 @@ import { DialogProps, TextFieldProps } from "./Controls/Dialog";
import { GalleryTab } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { GalleryTab } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter"; import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import { FileSystemUtil } from "./Notebook/FileSystemUtil"; import * as FileSystemUtil from "./Notebook/FileSystemUtil";
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
import { NotebookUtil } from "./Notebook/NotebookUtil"; import { NotebookUtil } from "./Notebook/NotebookUtil";
import AddCollectionPane from "./Panes/AddCollectionPane"; import AddCollectionPane from "./Panes/AddCollectionPane";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import AddDatabasePane from "./Panes/AddDatabasePane"; import AddDatabasePane from "./Panes/AddDatabasePane";
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane"; import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import { ContextualPaneBase } from "./Panes/ContextualPaneBase"; import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane"; import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel"; import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel";
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane"; import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane"; import { ExecuteSprocParamsPanel } from "./Panes/ExecuteSprocParamsPanel";
import GraphStylingPane from "./Panes/GraphStylingPane"; import GraphStylingPane from "./Panes/GraphStylingPane";
import { LoadQueryPane } from "./Panes/LoadQueryPane"; import { LoadQueryPane } from "./Panes/LoadQueryPane";
import NewVertexPane from "./Panes/NewVertexPane"; import NewVertexPane from "./Panes/NewVertexPane";
@@ -69,7 +70,6 @@ import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane";
import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane"; import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane";
import { UploadFilePane } from "./Panes/UploadFilePane"; import { UploadFilePane } from "./Panes/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane"; import { UploadItemsPane } from "./Panes/UploadItemsPane";
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import TabsBase from "./Tabs/TabsBase"; import TabsBase from "./Tabs/TabsBase";
@@ -163,8 +163,6 @@ export default class Explorer {
public isAccountReady: ko.Observable<boolean>; public isAccountReady: ko.Observable<boolean>;
public canSaveQueries: ko.Computed<boolean>; public canSaveQueries: ko.Computed<boolean>;
public features: ko.Observable<any>; public features: ko.Observable<any>;
public serverId: ko.Observable<string>;
public isTryCosmosDBSubscription: ko.Observable<boolean>;
public queriesClient: QueriesClient; public queriesClient: QueriesClient;
public tableDataClient: TableDataClient; public tableDataClient: TableDataClient;
public splitter: Splitter; public splitter: Splitter;
@@ -181,16 +179,10 @@ export default class Explorer {
// Resource Tree // Resource Tree
public databases: ko.ObservableArray<ViewModels.Database>; public databases: ko.ObservableArray<ViewModels.Database>;
public nonSystemDatabases: ko.Computed<ViewModels.Database[]>;
public selectedDatabaseId: ko.Computed<string>; public selectedDatabaseId: ko.Computed<string>;
public selectedCollectionId: ko.Computed<string>; public selectedCollectionId: ko.Computed<string>;
public isLeftPaneExpanded: ko.Observable<boolean>; public isLeftPaneExpanded: ko.Observable<boolean>;
public selectedNode: ko.Observable<ViewModels.TreeNode>; public selectedNode: ko.Observable<ViewModels.TreeNode>;
/**
* @deprecated
* Use a local loading state and spinner instead. Using a global isRefreshing state causes problems.
* */
public isRefreshingExplorer: ko.Observable<boolean>;
private resourceTree: ResourceTreeAdapter; private resourceTree: ResourceTreeAdapter;
// Resource Token // Resource Token
@@ -198,9 +190,8 @@ export default class Explorer {
public resourceTokenCollectionId: ko.Observable<string>; public resourceTokenCollectionId: ko.Observable<string>;
public resourceTokenCollection: ko.Observable<ViewModels.CollectionBase>; public resourceTokenCollection: ko.Observable<ViewModels.CollectionBase>;
public resourceTokenPartitionKey: ko.Observable<string>; public resourceTokenPartitionKey: ko.Observable<string>;
public isAuthWithResourceToken: ko.Observable<boolean>;
public isResourceTokenCollectionNodeSelected: ko.Computed<boolean>; public isResourceTokenCollectionNodeSelected: ko.Computed<boolean>;
private resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken; public resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken;
// Tabs // Tabs
public isTabsContentExpanded: ko.Observable<boolean>; public isTabsContentExpanded: ko.Observable<boolean>;
@@ -212,7 +203,6 @@ export default class Explorer {
public addDatabasePane: AddDatabasePane; public addDatabasePane: AddDatabasePane;
public addCollectionPane: AddCollectionPane; public addCollectionPane: AddCollectionPane;
public deleteCollectionConfirmationPane: DeleteCollectionConfirmationPane; public deleteCollectionConfirmationPane: DeleteCollectionConfirmationPane;
public deleteDatabaseConfirmationPane: DeleteDatabaseConfirmationPane;
public graphStylingPane: GraphStylingPane; public graphStylingPane: GraphStylingPane;
public addTableEntityPane: AddTableEntityPane; public addTableEntityPane: AddTableEntityPane;
public editTableEntityPane: EditTableEntityPane; public editTableEntityPane: EditTableEntityPane;
@@ -220,14 +210,9 @@ export default class Explorer {
public querySelectPane: QuerySelectPane; public querySelectPane: QuerySelectPane;
public newVertexPane: NewVertexPane; public newVertexPane: NewVertexPane;
public cassandraAddCollectionPane: CassandraAddCollectionPane; public cassandraAddCollectionPane: CassandraAddCollectionPane;
public settingsPane: SettingsPane;
public executeSprocParamsPane: ExecuteSprocParamsPane;
public uploadItemsPane: UploadItemsPane;
public uploadItemsPaneAdapter: UploadItemsPaneAdapter;
public loadQueryPane: LoadQueryPane; public loadQueryPane: LoadQueryPane;
public saveQueryPane: ContextualPaneBase; public saveQueryPane: ContextualPaneBase;
public browseQueriesPane: BrowseQueriesPane; public browseQueriesPane: BrowseQueriesPane;
public uploadFilePane: UploadFilePane;
public stringInputPane: StringInputPane; public stringInputPane: StringInputPane;
public setupNotebooksPane: SetupNotebooksPane; public setupNotebooksPane: SetupNotebooksPane;
public gitHubReposPane: ContextualPaneBase; public gitHubReposPane: ContextualPaneBase;
@@ -264,7 +249,6 @@ export default class Explorer {
public closeDialog: ExplorerParams["closeDialog"]; public closeDialog: ExplorerParams["closeDialog"];
private _panes: ContextualPaneBase[] = []; private _panes: ContextualPaneBase[] = [];
private _isSystemDatabasePredicate: (database: ViewModels.Database) => boolean = (database) => false;
private _isInitializingNotebooks: boolean; private _isInitializingNotebooks: boolean;
private notebookBasePath: ko.Observable<string>; private notebookBasePath: ko.Observable<string>;
private _arcadiaManager: ArcadiaResourceManager; private _arcadiaManager: ArcadiaResourceManager;
@@ -301,22 +285,6 @@ export default class Explorer {
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>(); this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType); this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType);
let firstInitialization = true;
this.isRefreshingExplorer = ko.observable<boolean>(true);
this.isRefreshingExplorer.subscribe((isRefreshing: boolean) => {
if (!isRefreshing && firstInitialization) {
// set focus on first element
firstInitialization = false;
try {
document.getElementById("createNewContainerCommandButton").parentElement.parentElement.focus();
} catch (e) {
Logger.logWarning(
"getElementById('createNewContainerCommandButton') failed to find element",
"Explorer/this.isRefreshingExplorer.subscribe"
);
}
}
});
this.isAccountReady = ko.observable<boolean>(false); this.isAccountReady = ko.observable<boolean>(false);
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
this.arcadiaToken = ko.observable<string>(); this.arcadiaToken = ko.observable<string>();
@@ -337,7 +305,9 @@ export default class Explorer {
this.isSynapseLinkUpdating = ko.observable<boolean>(false); this.isSynapseLinkUpdating = ko.observable<boolean>(false);
this.isAccountReady.subscribe(async (isAccountReady: boolean) => { this.isAccountReady.subscribe(async (isAccountReady: boolean) => {
if (isAccountReady) { if (isAccountReady) {
this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true); userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases(true);
RouteHandler.getInstance().initHandler(); RouteHandler.getInstance().initHandler();
this.notebookWorkspaceManager = new NotebookWorkspaceManager(); this.notebookWorkspaceManager = new NotebookWorkspaceManager();
this.arcadiaWorkspaces = ko.observableArray(); this.arcadiaWorkspaces = ko.observableArray();
@@ -348,9 +318,9 @@ export default class Explorer {
Promise.all([this._refreshNotebooksEnabledStateForAccount(), this._refreshSparkEnabledStateForAccount()]).then( Promise.all([this._refreshNotebooksEnabledStateForAccount(), this._refreshSparkEnabledStateForAccount()]).then(
async () => { async () => {
this.isNotebookEnabled( this.isNotebookEnabled(
!this.isAuthWithResourceToken() && userContext.authType !== AuthType.ResourceToken &&
((await this._containsDefaultNotebookWorkspace(this.databaseAccount())) || ((await this._containsDefaultNotebookWorkspace(this.databaseAccount())) ||
this.isFeatureEnabled(Constants.Features.enableNotebooks)) userContext.features.enableNotebooks)
); );
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
@@ -372,7 +342,7 @@ export default class Explorer {
this.isSparkEnabledForAccount() && this.isSparkEnabledForAccount() &&
this.arcadiaWorkspaces() && this.arcadiaWorkspaces() &&
this.arcadiaWorkspaces().length > 0) || this.arcadiaWorkspaces().length > 0) ||
this.isFeatureEnabled(Constants.Features.enableSpark) userContext.features.enableSpark
); );
if (this.isSparkEnabled()) { if (this.isSparkEnabled()) {
appInsights.trackEvent( appInsights.trackEvent(
@@ -396,26 +366,20 @@ export default class Explorer {
}); });
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>(); this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
this.features = ko.observable();
this.serverId = ko.observable<string>();
this.queriesClient = new QueriesClient(this); this.queriesClient = new QueriesClient(this);
this.isTryCosmosDBSubscription = ko.observable<boolean>(false);
this.resourceTokenDatabaseId = ko.observable<string>(); this.resourceTokenDatabaseId = ko.observable<string>();
this.resourceTokenCollectionId = ko.observable<string>(); this.resourceTokenCollectionId = ko.observable<string>();
this.resourceTokenCollection = ko.observable<ViewModels.CollectionBase>(); this.resourceTokenCollection = ko.observable<ViewModels.CollectionBase>();
this.resourceTokenPartitionKey = ko.observable<string>(); this.resourceTokenPartitionKey = ko.observable<string>();
this.isAuthWithResourceToken = ko.observable<boolean>(false);
this.isGitHubPaneEnabled = ko.observable<boolean>(false); this.isGitHubPaneEnabled = ko.observable<boolean>(false);
this.isMongoIndexingEnabled = ko.observable<boolean>(false); this.isMongoIndexingEnabled = ko.observable<boolean>(false);
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false); this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false); this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
this.canExceedMaximumValue = ko.computed<boolean>(() => this.canExceedMaximumValue = ko.computed<boolean>(() => userContext.features.canExceedMaximumValue);
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
);
this.isSchemaEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema)); this.isSchemaEnabled = ko.computed<boolean>(() => userContext.features.enableSchema);
this.isAutoscaleDefaultEnabled = ko.observable<boolean>(false); this.isAutoscaleDefaultEnabled = ko.observable<boolean>(false);
@@ -495,7 +459,7 @@ export default class Explorer {
}); });
this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => { this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => {
if (this.isFeatureEnabled(Constants.Features.enableFixedCollectionWithSharedThroughput)) { if (userContext.features.enableFixedCollectionWithSharedThroughput) {
return true; return true;
} }
@@ -554,20 +518,7 @@ export default class Explorer {
() => () =>
configContext.platform === Platform.Portal && !this.isRunningOnNationalCloud() && !this.isPreferredApiGraph() configContext.platform === Platform.Portal && !this.isRunningOnNationalCloud() && !this.isPreferredApiGraph()
); );
this.isRightPanelV2Enabled = ko.computed<boolean>(() => this.isRightPanelV2Enabled = ko.computed<boolean>(() => userContext.features.enableRightPanelV2);
this.isFeatureEnabled(Constants.Features.enableRightPanelV2)
);
this.defaultExperience.subscribe((defaultExperience: string) => {
if (
defaultExperience &&
defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase()
) {
this._isSystemDatabasePredicate = (database: ViewModels.Database): boolean => {
return database.id() === "system";
};
}
});
this.selectedDatabaseId = ko.computed<string>(() => { this.selectedDatabaseId = ko.computed<string>(() => {
const selectedNode = this.selectedNode(); const selectedNode = this.selectedNode();
if (!selectedNode) { if (!selectedNode) {
@@ -589,10 +540,6 @@ export default class Explorer {
} }
}); });
this.nonSystemDatabases = ko.computed(() => {
return this.databases().filter((database: ViewModels.Database) => !this._isSystemDatabasePredicate(database));
});
this.addDatabasePane = new AddDatabasePane({ this.addDatabasePane = new AddDatabasePane({
id: "adddatabasepane", id: "adddatabasepane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -615,13 +562,6 @@ export default class Explorer {
container: this, container: this,
}); });
this.deleteDatabaseConfirmationPane = new DeleteDatabaseConfirmationPane({
id: "deletedatabaseconfirmationpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.graphStylingPane = new GraphStylingPane({ this.graphStylingPane = new GraphStylingPane({
id: "graphstylingpane", id: "graphstylingpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -671,29 +611,6 @@ export default class Explorer {
container: this, container: this,
}); });
this.settingsPane = new SettingsPane({
id: "settingspane",
visible: ko.observable<boolean>(false),
container: this,
});
this.executeSprocParamsPane = new ExecuteSprocParamsPane({
id: "executesprocparamspane",
visible: ko.observable<boolean>(false),
container: this,
});
this.uploadItemsPane = new UploadItemsPane({
id: "uploaditemspane",
visible: ko.observable<boolean>(false),
container: this,
});
this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this);
this.loadQueryPane = new LoadQueryPane({ this.loadQueryPane = new LoadQueryPane({
id: "loadquerypane", id: "loadquerypane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -715,13 +632,6 @@ export default class Explorer {
container: this, container: this,
}); });
this.uploadFilePane = new UploadFilePane({
id: "uploadfilepane",
visible: ko.observable<boolean>(false),
container: this,
});
this.stringInputPane = new StringInputPane({ this.stringInputPane = new StringInputPane({
id: "stringinputpane", id: "stringinputpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -742,7 +652,6 @@ export default class Explorer {
this.addDatabasePane, this.addDatabasePane,
this.addCollectionPane, this.addCollectionPane,
this.deleteCollectionConfirmationPane, this.deleteCollectionConfirmationPane,
this.deleteDatabaseConfirmationPane,
this.graphStylingPane, this.graphStylingPane,
this.addTableEntityPane, this.addTableEntityPane,
this.editTableEntityPane, this.editTableEntityPane,
@@ -750,13 +659,9 @@ export default class Explorer {
this.querySelectPane, this.querySelectPane,
this.newVertexPane, this.newVertexPane,
this.cassandraAddCollectionPane, this.cassandraAddCollectionPane,
this.settingsPane,
this.executeSprocParamsPane,
this.uploadItemsPane,
this.loadQueryPane, this.loadQueryPane,
this.saveQueryPane, this.saveQueryPane,
this.browseQueriesPane, this.browseQueriesPane,
this.uploadFilePane,
this.stringInputPane, this.stringInputPane,
this.setupNotebooksPane, this.setupNotebooksPane,
]; ];
@@ -852,8 +757,6 @@ export default class Explorer {
this.editTableEntityPane.title("Edit Table Row"); this.editTableEntityPane.title("Edit Table Row");
this.deleteCollectionConfirmationPane.title("Delete Table"); this.deleteCollectionConfirmationPane.title("Delete Table");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id"); this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.deleteDatabaseConfirmationPane.title("Delete Keyspace");
this.deleteDatabaseConfirmationPane.databaseIdConfirmationText("Confirm by typing the keyspace id");
this.tableDataClient = new CassandraAPIDataClient(); this.tableDataClient = new CassandraAPIDataClient();
break; break;
} }
@@ -907,42 +810,29 @@ export default class Explorer {
}); });
// Override notebook server parameters from URL parameters // Override notebook server parameters from URL parameters
const featureSubcription = this.features.subscribe((features) => { if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) {
const serverInfo = this.notebookServerInfo(); this.notebookServerInfo({
if (this.isFeatureEnabled(Constants.Features.notebookServerUrl)) { notebookServerEndpoint: userContext.features.notebookServerUrl,
serverInfo.notebookServerEndpoint = features[Constants.Features.notebookServerUrl]; authToken: userContext.features.notebookServerToken,
} });
}
if (this.isFeatureEnabled(Constants.Features.notebookServerToken)) { if (userContext.features.notebookBasePath) {
serverInfo.authToken = features[Constants.Features.notebookServerToken]; this.notebookBasePath(userContext.features.notebookBasePath);
} }
this.notebookServerInfo(serverInfo);
this.notebookServerInfo.valueHasMutated();
if (this.isFeatureEnabled(Constants.Features.notebookBasePath)) { if (userContext.features.livyEndpoint) {
this.notebookBasePath(features[Constants.Features.notebookBasePath]); this.sparkClusterConnectionInfo({
} userName: undefined,
password: undefined,
if (this.isFeatureEnabled(Constants.Features.livyEndpoint)) { endpoints: [
this.sparkClusterConnectionInfo({ {
userName: undefined, endpoint: userContext.features.livyEndpoint,
password: undefined, kind: DataModels.SparkClusterEndpointKind.Livy,
endpoints: [ },
{ ],
endpoint: features[Constants.Features.livyEndpoint], });
kind: DataModels.SparkClusterEndpointKind.Livy, }
},
],
});
this.sparkClusterConnectionInfo.valueHasMutated();
}
if (this.isFeatureEnabled(Constants.Features.enableSDKoperations)) {
updateUserContext({ useSDKOperations: true });
}
featureSubcription.dispose();
});
} }
public openEnableSynapseLinkDialog(): void { public openEnableSynapseLinkDialog(): void {
@@ -1026,20 +916,6 @@ export default class Explorer {
return this.selectedNode() == null; return this.selectedNode() == null;
} }
public isFeatureEnabled(feature: string): boolean {
const features = this.features();
if (!features) {
return false;
}
if (feature in features && features[feature]) {
return true;
}
return false;
}
public logConsoleData(consoleData: ConsoleData): void { public logConsoleData(consoleData: ConsoleData): void {
this.setNotificationConsoleData(consoleData); this.setNotificationConsoleData(consoleData);
} }
@@ -1086,7 +962,6 @@ export default class Explorer {
} }
public refreshAllDatabases(isInitialLoad?: boolean): Q.Promise<any> { public refreshAllDatabases(isInitialLoad?: boolean): Q.Promise<any> {
this.isRefreshingExplorer(true);
const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, { const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
@@ -1116,22 +991,19 @@ export default class Explorer {
this.deleteDatabasesFromList(deltaDatabases.toDelete); this.deleteDatabasesFromList(deltaDatabases.toDelete);
this.selectedNode(currentlySelectedNode); this.selectedNode(currentlySelectedNode);
this._setLoadingStatusText("Fetching containers..."); this._setLoadingStatusText("Fetching containers...");
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd) this.refreshAndExpandNewDatabases(deltaDatabases.toAdd).then(
.then( () => {
() => { this._setLoadingStatusText("Successfully fetched containers.");
this._setLoadingStatusText("Successfully fetched containers."); deferred.resolve();
deferred.resolve(); },
}, (reason) => {
(reason) => { this._setLoadingStatusText("Failed to fetch containers.");
this._setLoadingStatusText("Failed to fetch containers."); deferred.reject(reason);
deferred.reject(reason); }
} );
)
.finally(() => this.isRefreshingExplorer(false));
}, },
(error) => { (error) => {
this._setLoadingStatusText("Failed to fetch databases."); this._setLoadingStatusText("Failed to fetch databases.");
this.isRefreshingExplorer(false);
deferred.reject(error); deferred.reject(error);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
@@ -1191,8 +1063,9 @@ export default class Explorer {
description: "Refresh button clicked", description: "Refresh button clicked",
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
this.isRefreshingExplorer(true); userContext.authType === AuthType.ResourceToken
this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(); ? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases();
this.refreshNotebookList(); this.refreshNotebookList();
}; };
@@ -1285,12 +1158,12 @@ export default class Explorer {
throw error; throw error;
} finally { } finally {
// Overwrite with feature flags // Overwrite with feature flags
if (this.isFeatureEnabled(Constants.Features.notebookServerUrl)) { if (userContext.features.notebookServerUrl) {
connectionInfo.notebookServerEndpoint = this.features()[Constants.Features.notebookServerUrl]; connectionInfo.notebookServerEndpoint = userContext.features.notebookServerUrl;
} }
if (this.isFeatureEnabled(Constants.Features.notebookServerToken)) { if (userContext.features.notebookServerToken) {
connectionInfo.authToken = this.features()[Constants.Features.notebookServerToken]; connectionInfo.authToken = userContext.features.notebookServerToken;
} }
this.notebookServerInfo(connectionInfo); this.notebookServerInfo(connectionInfo);
@@ -1406,7 +1279,12 @@ export default class Explorer {
} }
public isLastNonEmptyDatabase(): boolean { public isLastNonEmptyDatabase(): boolean {
if (this.isLastDatabase() && this.databases()[0].collections && this.databases()[0].collections().length > 0) { if (
this.isLastDatabase() &&
this.databases()[0] &&
this.databases()[0].collections &&
this.databases()[0].collections().length > 0
) {
return true; return true;
} }
return false; return false;
@@ -1440,16 +1318,12 @@ export default class Explorer {
if (inputs.defaultCollectionThroughput) { if (inputs.defaultCollectionThroughput) {
this.collectionCreationDefaults = inputs.defaultCollectionThroughput; this.collectionCreationDefaults = inputs.defaultCollectionThroughput;
} }
this.features(inputs.features);
this.serverId(inputs.serverId ?? Constants.ServerIds.productionPortal);
this.databaseAccount(databaseAccount); this.databaseAccount(databaseAccount);
this.subscriptionType(inputs.subscriptionType ?? SharedConstants.CollectionCreation.DefaultSubscriptionType); this.subscriptionType(inputs.subscriptionType ?? SharedConstants.CollectionCreation.DefaultSubscriptionType);
this.hasWriteAccess(inputs.hasWriteAccess ?? true); this.hasWriteAccess(inputs.hasWriteAccess ?? true);
if (inputs.addCollectionDefaultFlight) { if (inputs.addCollectionDefaultFlight) {
this.flight(inputs.addCollectionDefaultFlight); this.flight(inputs.addCollectionDefaultFlight);
} }
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription ?? false);
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken ?? false);
this.setFeatureFlagsFromFlights(inputs.flights); this.setFeatureFlagsFromFlights(inputs.flights);
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.LoadDatabaseAccount, Action.LoadDatabaseAccount,
@@ -1530,9 +1404,9 @@ export default class Explorer {
public isRunningOnNationalCloud(): boolean { public isRunningOnNationalCloud(): boolean {
return ( return (
this.serverId() === Constants.ServerIds.blackforest || userContext.portalEnv === "blackforest" ||
this.serverId() === Constants.ServerIds.fairfax || userContext.portalEnv === "fairfax" ||
this.serverId() === Constants.ServerIds.mooncake userContext.portalEnv === "mooncake"
); );
} }
@@ -2203,38 +2077,6 @@ export default class Explorer {
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(notificationProgressId)); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(notificationProgressId));
} }
public onUploadToNotebookServerClicked(parent?: NotebookContentItem): void {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePane.openWithOptions({
paneTitle: "Upload file to notebook server",
selectFileInputLabel: "Select file to upload",
errorMessage: "Could not upload file",
inProgressMessage: "Uploading file to notebook server",
successMessage: "Successfully uploaded file to notebook server",
onSubmit: async (file: File): Promise<NotebookContentItem> => {
const readFileAsText = (inputFile: File): Promise<string> => {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onerror = () => {
reader.abort();
reject(`Problem parsing file: ${inputFile}`);
};
reader.onload = () => {
resolve(reader.result as string);
};
reader.readAsText(inputFile);
});
};
const fileContent = await readFileAsText(file);
return this.uploadFile(file.name, fileContent, parent);
},
extensions: undefined,
submitButtonLabel: "Upload",
});
}
public refreshContentItem(item: NotebookContentItem): Promise<void> { public refreshContentItem(item: NotebookContentItem): Promise<void> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to refresh notebook list, but notebook is not enabled"; const error = "Attempt to refresh notebook list, but notebook is not enabled";
@@ -2397,10 +2239,12 @@ export default class Explorer {
public onNewCollectionClicked(): void { public onNewCollectionClicked(): void {
if (this.isPreferredApiCassandra()) { if (this.isPreferredApiCassandra()) {
this.cassandraAddCollectionPane.open(); this.cassandraAddCollectionPane.open();
} else if (userContext.features.enableReactPane) {
this.openAddCollectionPanel();
} else { } else {
this.addCollectionPane.open(this.selectedDatabaseId()); this.addCollectionPane.open(this.selectedDatabaseId());
document.getElementById("linkAddCollection").focus();
} }
document.getElementById("linkAddCollection").focus();
} }
private refreshCommandBarButtons(): void { private refreshCommandBarButtons(): void {
@@ -2529,7 +2373,7 @@ export default class Explorer {
} }
public openDeleteCollectionConfirmationPane(): void { public openDeleteCollectionConfirmationPane(): void {
this.isFeatureEnabled(Constants.Features.enableKOPanel) userContext.features.enableKOPanel
? this.deleteCollectionConfirmationPane.open() ? this.deleteCollectionConfirmationPane.open()
: this.openSidePanel( : this.openSidePanel(
"Delete Collection", "Delete Collection",
@@ -2540,4 +2384,54 @@ export default class Explorer {
/> />
); );
} }
public openDeleteDatabaseConfirmationPane(): void {
this.openSidePanel(
"Delete Database",
<DeleteDatabaseConfirmationPanel
explorer={this}
openNotificationConsole={this.expandConsole}
closePanel={this.closeSidePanel}
selectedDatabase={this.findSelectedDatabase()}
/>
);
}
public openUploadItemsPanePane(): void {
this.openSidePanel("Upload", <UploadItemsPane explorer={this} closePanel={this.closeSidePanel} />);
}
public openSettingPane(): void {
this.openSidePanel("Settings", <SettingsPane explorer={this} closePanel={this.closeSidePanel} />);
}
public openExecuteSprocParamsPanel(): void {
this.openSidePanel(
"Input parameters",
<ExecuteSprocParamsPanel explorer={this} closePanel={() => this.closeSidePanel()} />
);
}
public async openAddCollectionPanel(): Promise<void> {
await this.loadDatabaseOffers();
this.openSidePanel(
"New Collection",
<AddCollectionPanel
explorer={this}
closePanel={() => this.closeSidePanel()}
openNotificationConsole={() => this.expandConsole()}
/>
);
}
public openUploadFilePanel(parent?: NotebookContentItem): void {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.openSidePanel(
"Upload File",
<UploadFilePane
explorer={this}
closePanel={this.closeSidePanel}
uploadFile={(name: string, content: string) => this.uploadFile(name, content, parent)}
/>
);
}
} }

View File

@@ -4,11 +4,8 @@
* - inspired from gremlin-javascript for nodejs: https://github.com/jbmusso/gremlin-javascript * - inspired from gremlin-javascript for nodejs: https://github.com/jbmusso/gremlin-javascript
* - tested on cosmosdb gremlin server * - tested on cosmosdb gremlin server
* - only supports sessionless gremlin requests * - only supports sessionless gremlin requests
* - Relies on text-encoding polyfill (github.com/inexorabletash/text-encoding) for TextEncoder/TextDecoder on IE, Edge.
*/ */
import { TextEncoder, TextDecoder } from "text-encoding";
export interface GremlinSimpleClientParameters { export interface GremlinSimpleClientParameters {
endpoint: string; // The websocket endpoint endpoint: string; // The websocket endpoint
user: string; user: string;

View File

@@ -1,8 +1,10 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory"; import { AuthType } from "../../../AuthType";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import NotebookManager from "../../Notebook/NotebookManager"; import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import NotebookManager from "../../Notebook/NotebookManager";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
describe("CommandBarComponentButtonFactory tests", () => { describe("CommandBarComponentButtonFactory tests", () => {
let mockExplorer: Explorer; let mockExplorer: Explorer;
@@ -13,7 +15,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(false);
mockExplorer.isPreferredApiTable = ko.computed(() => true); mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
@@ -53,7 +54,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(false);
mockExplorer.isPreferredApiTable = ko.computed(() => true); mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
@@ -118,7 +118,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(false);
mockExplorer.isPreferredApiTable = ko.computed(() => true); mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
@@ -199,7 +198,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(false);
mockExplorer.isPreferredApiTable = ko.computed(() => true); mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
@@ -281,7 +279,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(false);
mockExplorer.isPreferredApiTable = ko.computed(() => true); mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
@@ -340,12 +337,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(true);
mockExplorer.isPreferredApiDocumentDB = ko.computed(() => true); mockExplorer.isPreferredApiDocumentDB = ko.computed(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true); mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true);
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false); mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
updateUserContext({
authType: AuthType.ResourceToken,
});
}); });
it("should only show New SQL Query and Open Query buttons", () => { it("should only show New SQL Query and Open Query buttons", () => {

View File

@@ -1,37 +1,38 @@
import * as ViewModels from "../../../Contracts/ViewModels"; import * as React from "react";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { Areas } from "../../../Common/Constants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
import AddCollectionIcon from "../../../../images/AddCollection.svg"; import AddCollectionIcon from "../../../../images/AddCollection.svg";
import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg"; import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg";
import * as Constants from "../../../Common/Constants";
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg"; import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
import SettingsIcon from "../../../../images/settings_15x15.svg";
import AddUdfIcon from "../../../../images/AddUdf.svg";
import AddTriggerIcon from "../../../../images/AddTrigger.svg"; import AddTriggerIcon from "../../../../images/AddTrigger.svg";
import AddUdfIcon from "../../../../images/AddUdf.svg";
import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg";
import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
import FeedbackIcon from "../../../../images/Feedback-Command.svg"; import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import GitHubIcon from "../../../../images/github.svg";
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
import EnableNotebooksIcon from "../../../../images/notebook/Notebook-enable.svg"; import EnableNotebooksIcon from "../../../../images/notebook/Notebook-enable.svg";
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg"; import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg"; import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
import GitHubIcon from "../../../../images/github.svg"; import OpenInTabIcon from "../../../../images/open-in-tab.svg";
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
import SettingsIcon from "../../../../images/settings_15x15.svg";
import SynapseIcon from "../../../../images/synapse-link.svg"; import SynapseIcon from "../../../../images/synapse-link.svg";
import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants";
import { Areas } from "../../../Common/Constants";
import { configContext, Platform } from "../../../ConfigContext"; import { configContext, Platform } from "../../../ConfigContext";
import Explorer from "../../Explorer"; import * as ViewModels from "../../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import * as React from "react"; import Explorer from "../../Explorer";
import { OpenFullScreen } from "../../OpenFullScreen"; import { OpenFullScreen } from "../../OpenFullScreen";
let counter = 0; let counter = 0;
export function createStaticCommandBarButtons(container: Explorer): CommandButtonComponentProps[] { export function createStaticCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
if (container.isAuthWithResourceToken()) { if (userContext.authType === AuthType.ResourceToken) {
return createStaticCommandBarButtonsForResourceToken(container); return createStaticCommandBarButtonsForResourceToken(container);
} }
@@ -163,7 +164,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
const settingsPaneButton: CommandButtonComponentProps = { const settingsPaneButton: CommandButtonComponentProps = {
iconSrc: SettingsIcon, iconSrc: SettingsIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => container.settingsPane.open(), onCommandClick: () => container.openSettingPane(),
commandButtonLabel: undefined, commandButtonLabel: undefined,
ariaLabel: label, ariaLabel: label,
tooltipText: label, tooltipText: label,
@@ -406,7 +407,7 @@ function createuploadNotebookButton(container: Explorer): CommandButtonComponent
return { return {
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => container.onUploadToNotebookServerClicked(), onCommandClick: () => container.openUploadFilePanel(),
commandButtonLabel: label, commandButtonLabel: label,
hasPopup: false, hasPopup: false,
disabled: false, disabled: false,

View File

@@ -1,37 +1,33 @@
// Utilities for file system /**
* file list returns path starting with ./blah
export class FileSystemUtil { * rename returns simply blah.
/** * Both are the same. This method only handles these two cases and no other complicated paths that may contain ..
* file list returns path starting with ./blah * ./ inside the path.
* rename returns simply blah. * TODO: this should go away when not using jupyter for file operations and use normalized paths.
* Both are the same. This method only handles these two cases and no other complicated paths that may contain .. * @param path1
* ./ inside the path. * @param path2
* TODO: this should go away when not using jupyter for file operations and use normalized paths. */
* @param path1 export function isPathEqual(path1: string, path2: string): boolean {
* @param path2 const normalize = (path: string): string => {
*/ const dotSlash = "./";
public static isPathEqual(path1: string, path2: string): boolean { if (path.indexOf(dotSlash) === 0) {
const normalize = (path: string): string => { path = path.substring(dotSlash.length);
const dotSlash = "./";
if (path.indexOf(dotSlash) === 0) {
path = path.substring(dotSlash.length);
}
return path;
};
return normalize(path1) === normalize(path2);
}
/**
* Remove extension
* @param path
* @param extension Without the ".". e.g. "ipynb" (and not ".ipynb")
*/
public static stripExtension(path: string, extension: string): string {
const splitted = path.split(".");
if (splitted[splitted.length - 1] === extension) {
splitted.pop();
} }
return splitted.join("."); return path;
} };
return normalize(path1) === normalize(path2);
}
/**
* Remove extension
* @param path
* @param extension Without the ".". e.g. "ipynb" (and not ".ipynb")
*/
export function stripExtension(path: string, extension: string): string {
const splitted = path.split(".");
if (splitted[splitted.length - 1] === extension) {
splitted.pop();
}
return splitted.join(".");
} }

View File

@@ -1,16 +1,14 @@
// Manages all the redux logic for the notebook nteract code // Manages all the redux logic for the notebook nteract code
// TODO: Merge with NotebookClient? // TODO: Merge with NotebookClient?
import { NotebookWorkspaceConnectionInfo } from "../../Contracts/DataModels";
import * as Constants from "../../Common/Constants";
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
// Vendor modules // Vendor modules
import { import {
actions, actions,
AppState, AppState,
createHostRef, ContentRecord, createHostRef,
createKernelspecsRef, createKernelspecsRef,
makeAppRecord, HostRecord,
HostRef,
IContentProvider, KernelspecsRef, makeAppRecord,
makeCommsRecord, makeCommsRecord,
makeContentsRecord, makeContentsRecord,
makeEditorsRecord, makeEditorsRecord,
@@ -18,24 +16,22 @@ import {
makeHostsRecord, makeHostsRecord,
makeJupyterHostRecord, makeJupyterHostRecord,
makeStateRecord, makeStateRecord,
makeTransformsRecord, makeTransformsRecord
ContentRecord,
HostRecord,
HostRef,
KernelspecsRef,
IContentProvider,
} from "@nteract/core"; } from "@nteract/core";
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration";
import { Media } from "@nteract/outputs"; import { Media } from "@nteract/outputs";
import TransformVDOM from "@nteract/transform-vdom"; import TransformVDOM from "@nteract/transform-vdom";
import * as Immutable from "immutable"; import * as Immutable from "immutable";
import { Store, AnyAction, MiddlewareAPI, Middleware, Dispatch } from "redux";
import configureStore from "./NotebookComponent/store";
import { Notification } from "react-notification-system"; import { Notification } from "react-notification-system";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { AnyAction, Dispatch, Middleware, MiddlewareAPI, Store } from "redux";
import * as Constants from "../../Common/Constants";
import { NotebookWorkspaceConnectionInfo } from "../../Contracts/DataModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import configureStore from "./NotebookComponent/store";
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
import IFrameHTML from "./NotebookRenderer/outputs/IFrameHTML";
import IFrameJavaScript from "./NotebookRenderer/outputs/IFrameJavaScript";
export type KernelSpecsDisplay = { name: string; displayName: string }; export type KernelSpecsDisplay = { name: string; displayName: string };
@@ -168,8 +164,8 @@ export class NotebookClientV2 {
"application/vnd.vega.v5+json": NullTransform, "application/vnd.vega.v5+json": NullTransform,
"application/vdom.v1+json": TransformVDOM, "application/vdom.v1+json": TransformVDOM,
"application/json": Media.Json, "application/json": Media.Json,
"application/javascript": Media.JavaScript, "application/javascript": IFrameJavaScript,
"text/html": Media.HTML, "text/html": IFrameHTML,
"text/markdown": Media.Markdown, "text/markdown": Media.Markdown,
"text/latex": Media.LaTeX, "text/latex": Media.LaTeX,
"image/svg+xml": Media.SVG, "image/svg+xml": Media.SVG,

View File

@@ -1,4 +1,4 @@
import { EMPTY, merge, of, timer, concat, Subject, Subscriber, Observable, Observer } from "rxjs"; import { EMPTY, merge, of, timer, concat, Subject, Subscriber, Observable, Observer, from } from "rxjs";
import { webSocket } from "rxjs/webSocket"; import { webSocket } from "rxjs/webSocket";
import { StateObservable } from "redux-observable"; import { StateObservable } from "redux-observable";
import { ofType } from "redux-observable"; import { ofType } from "redux-observable";
@@ -44,7 +44,7 @@ import { CdbAppState } from "./types";
import { decryptJWTToken } from "../../../Utils/AuthorizationUtils"; import { decryptJWTToken } from "../../../Utils/AuthorizationUtils";
import * as TextFile from "./contents/file/text-file"; import * as TextFile from "./contents/file/text-file";
import { NotebookUtil } from "../NotebookUtil"; import { NotebookUtil } from "../NotebookUtil";
import { FileSystemUtil } from "../FileSystemUtil"; import * as FileSystemUtil from "../FileSystemUtil";
import * as cdbActions from "../NotebookComponent/actions"; import * as cdbActions from "../NotebookComponent/actions";
import { Areas } from "../../../Common/Constants"; import { Areas } from "../../../Common/Constants";
@@ -944,6 +944,39 @@ const traceNotebookKernelEpic = (
); );
}; };
const resetCellStatusOnExecuteCanceledEpic = (
action$: Observable<actions.ExecuteCanceled>,
state$: StateObservable<AppState>
): Observable<actions.UpdateCellStatus> => {
return action$.pipe(
ofType(actions.EXECUTE_CANCELED),
mergeMap((action) => {
const contentRef = action.payload.contentRef;
const model = state$.value.core.entities.contents.byRef.get(contentRef).model;
let busyCellIds: string[] = [];
if (model.type === "notebook") {
const cellMap = model.transient.get("cellMap");
if (cellMap) {
for (const entry of cellMap.toArray()) {
const cellId = entry[0];
const status = model.transient.getIn(["cellMap", cellId, "status"]);
if (status === "busy") {
busyCellIds.push(cellId);
}
}
}
}
return from(busyCellIds).pipe(
map((busyCellId) => {
return actions.updateCellStatus({ id: busyCellId, contentRef, status: undefined });
})
);
})
);
};
export const allEpics = [ export const allEpics = [
addInitialCodeCellEpic, addInitialCodeCellEpic,
focusInitialCodeCellEpic, focusInitialCodeCellEpic,
@@ -960,4 +993,5 @@ export const allEpics = [
traceNotebookTelemetryEpic, traceNotebookTelemetryEpic,
traceNotebookInfoEpic, traceNotebookInfoEpic,
traceNotebookKernelEpic, traceNotebookKernelEpic,
resetCellStatusOnExecuteCanceledEpic,
]; ];

View File

@@ -1,12 +1,11 @@
import * as DataModels from "../../Contracts/DataModels";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import * as StringUtils from "../../Utils/StringUtils";
import { FileSystemUtil } from "./FileSystemUtil";
import { NotebookUtil } from "./NotebookUtil";
import { ServerConfig, IContent, IContentProvider, FileType, IEmptyContent } from "@nteract/core";
import { AjaxResponse } from "rxjs/ajax";
import { stringifyNotebook } from "@nteract/commutable"; import { stringifyNotebook } from "@nteract/commutable";
import { FileType, IContent, IContentProvider, IEmptyContent, ServerConfig } from "@nteract/core";
import { AjaxResponse } from "rxjs/ajax";
import * as DataModels from "../../Contracts/DataModels";
import * as StringUtils from "../../Utils/StringUtils";
import * as FileSystemUtil from "./FileSystemUtil";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import { NotebookUtil } from "./NotebookUtil";
export class NotebookContentClient { export class NotebookContentClient {
constructor( constructor(

View File

@@ -0,0 +1,63 @@
import * as React from "react";
import styled from "styled-components";
interface Props {
/**
* The HTML string that will be rendered.
*/
data: string;
/**
* The media type associated with the HTML
* string. This defaults to text/html.
*/
mediaType: "text/html";
}
const StyledIFrame = styled.iframe`
width: 100%;
border-style: unset;
`;
export class IFrameHTML extends React.PureComponent<Props> {
static defaultProps = {
data: "",
mediaType: "text/html"
};
frame?: HTMLIFrameElement;
appendChildDOM(): void {
if (!this.frame) {
return;
}
this.frame.contentDocument.open();
this.frame.contentDocument.write(this.props.data);
this.frame.contentDocument.close();
}
componentDidMount(): void {
this.appendChildDOM();
}
componentDidUpdate(): void {
this.appendChildDOM();
}
render() {
return (
<StyledIFrame
ref={frame => this.frame = frame}
allow="accelerometer; autoplay; camera; gyroscope; magnetometer; microphone; xr-spatial-tracking"
sandbox="allow-downloads allow-forms allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-popups-to-escape-sandbox"
onLoad={() => this.onFrameLoaded()} />
);
}
onFrameLoaded() {
this.frame.height = (this.frame.contentDocument.body.scrollHeight + 4) + "px";
this.frame.contentDocument.body.style.margin = "0px";
}
}
export default IFrameHTML;

View File

@@ -0,0 +1,28 @@
import React from "react";
import IFrameHTML from "./IFrameHTML";
interface Props {
/**
* The JavaScript code that we would like to execute.
*/
data: string;
/**
* The media type associated with our component.
*/
mediaType: "text/javascript";
}
export class IFrameJavaScript extends React.PureComponent<Props> {
static defaultProps = {
data: "",
mediaType: "application/javascript"
};
render() {
return (
<IFrameHTML data={`<script>${this.props.data}</script>`} />
);
}
}
export default IFrameJavaScript;

View File

@@ -1,7 +1,7 @@
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled. // TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
import * as ViewModels from "../Contracts/ViewModels";
import { ActionContracts } from "../Contracts/ExplorerContracts"; import { ActionContracts } from "../Contracts/ExplorerContracts";
import * as ViewModels from "../Contracts/ViewModels";
import Explorer from "./Explorer"; import Explorer from "./Explorer";
export function handleOpenAction( export function handleOpenAction(
@@ -145,7 +145,7 @@ function openPane(action: ActionContracts.OpenPane, explorer: Explorer) {
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings] (<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings]
) { ) {
explorer.closeAllPanes(); explorer.closeAllPanes();
explorer.settingsPane.open(); explorer.openSettingPane();
} }
} }

View File

@@ -1,22 +1,22 @@
import * as _ from "underscore";
import * as AddCollectionUtility from "../../Shared/AddCollectionUtility";
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ko from "knockout"; import * as ko from "knockout";
import * as PricingUtils from "../../Utils/PricingUtils"; import * as _ from "underscore";
import * as SharedConstants from "../../Shared/Constants"; import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import editable from "../../Common/EditableUtility";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { configContext, Platform } from "../../ConfigContext";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent";
import { createCollection } from "../../Common/dataAccess/createCollection"; import { createCollection } from "../../Common/dataAccess/createCollection";
import editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { configContext, Platform } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import * as ViewModels from "../../Contracts/ViewModels";
import * as AddCollectionUtility from "../../Shared/AddCollectionUtility";
import * as SharedConstants from "../../Shared/Constants";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../Utils/PricingUtils";
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
export interface AddCollectionPaneOptions extends ViewModels.PaneOptions { export interface AddCollectionPaneOptions extends ViewModels.PaneOptions {
isPreferredApiTable: ko.Computed<boolean>; isPreferredApiTable: ko.Computed<boolean>;
@@ -49,7 +49,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
public throughputDatabase: ViewModels.Editable<number>; public throughputDatabase: ViewModels.Editable<number>;
public isPreferredApiTable: ko.Computed<boolean>; public isPreferredApiTable: ko.Computed<boolean>;
public partitionKeyPlaceholder: ko.Computed<string>; public partitionKeyPlaceholder: ko.Computed<string>;
public isTryCosmosDBSubscription: ko.Computed<boolean>; public isTryCosmosDBSubscription: ko.Observable<boolean>;
public maxThroughputRU: ko.Observable<number>; public maxThroughputRU: ko.Observable<number>;
public minThroughputRU: ko.Observable<number>; public minThroughputRU: ko.Observable<number>;
public throughputRangeText: ko.Computed<string>; public throughputRangeText: ko.Computed<string>;
@@ -186,7 +186,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
return ""; return "";
} }
const serverId: string = this.container.serverId();
const regions = const regions =
(account && (account &&
account.properties && account.properties &&
@@ -200,23 +199,28 @@ export default class AddCollectionPane extends ContextualPaneBase {
if (!this.isSharedAutoPilotSelected()) { if (!this.isSharedAutoPilotSelected()) {
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString( throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
offerThroughput, offerThroughput,
serverId, userContext.portalEnv,
regions, regions,
multimaster, multimaster,
this.isSharedAutoPilotSelected() this.isSharedAutoPilotSelected()
); );
estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster); estimatedSpend = PricingUtils.getEstimatedSpendHtml(
offerThroughput,
userContext.portalEnv,
regions,
multimaster
);
} else { } else {
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString( throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
this.sharedAutoPilotThroughput(), this.sharedAutoPilotThroughput(),
serverId, userContext.portalEnv,
regions, regions,
multimaster, multimaster,
this.isSharedAutoPilotSelected() this.isSharedAutoPilotSelected()
); );
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.sharedAutoPilotThroughput(), this.sharedAutoPilotThroughput(),
serverId, userContext.portalEnv,
regions, regions,
multimaster multimaster
); );
@@ -240,7 +244,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
return ""; return "";
} }
const serverId: string = this.container.serverId();
const regions = const regions =
(account && (account &&
account.properties && account.properties &&
@@ -254,28 +257,28 @@ export default class AddCollectionPane extends ContextualPaneBase {
if (!this.isAutoPilotSelected()) { if (!this.isAutoPilotSelected()) {
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString( throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
this.throughputMultiPartition(), this.throughputMultiPartition(),
serverId, userContext.portalEnv,
regions, regions,
multimaster, multimaster,
this.isAutoPilotSelected() this.isAutoPilotSelected()
); );
estimatedSpend = PricingUtils.getEstimatedSpendHtml( estimatedSpend = PricingUtils.getEstimatedSpendHtml(
this.throughputMultiPartition(), this.throughputMultiPartition(),
serverId, userContext.portalEnv,
regions, regions,
multimaster multimaster
); );
} else { } else {
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString( throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
this.autoPilotThroughput(), this.autoPilotThroughput(),
serverId, userContext.portalEnv,
regions, regions,
multimaster, multimaster,
this.isAutoPilotSelected() this.isAutoPilotSelected()
); );
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.autoPilotThroughput(), this.autoPilotThroughput(),
serverId, userContext.portalEnv,
regions, regions,
multimaster multimaster
); );
@@ -285,9 +288,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return estimatedSpend; return estimatedSpend;
}); });
this.isTryCosmosDBSubscription = ko.pureComputed<boolean>(() => { this.isTryCosmosDBSubscription = ko.observable<boolean>(userContext.isTryCosmosDBSubscription || false);
return (this.container && this.container.isTryCosmosDBSubscription()) || false;
});
this.isTryCosmosDBSubscription.subscribe((isTryCosmosDB: boolean) => { this.isTryCosmosDBSubscription.subscribe((isTryCosmosDB: boolean) => {
if (!!isTryCosmosDB) { if (!!isTryCosmosDB) {
@@ -298,7 +299,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.canRequestSupport = ko.pureComputed(() => { this.canRequestSupport = ko.pureComputed(() => {
if ( if (
configContext.platform !== Platform.Emulator && configContext.platform !== Platform.Emulator &&
!this.container.isTryCosmosDBSubscription() && !userContext.isTryCosmosDBSubscription &&
configContext.platform !== Platform.Portal configContext.platform !== Platform.Portal
) { ) {
const offerThroughput: number = this._getThroughput(); const offerThroughput: number = this._getThroughput();
@@ -489,7 +490,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.upsellMessage = ko.pureComputed<string>(() => { this.upsellMessage = ko.pureComputed<string>(() => {
return PricingUtils.getUpsellMessage( return PricingUtils.getUpsellMessage(
this.container.serverId(), userContext.portalEnv,
this.isFreeTierAccount(), this.isFreeTierAccount(),
this.container.isFirstResourceCreated(), this.container.isFirstResourceCreated(),
this.container.defaultExperience(), this.container.defaultExperience(),
@@ -993,7 +994,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.container.openEnableSynapseLinkDialog(); this.container.openEnableSynapseLinkDialog();
} }
public ttl90DaysEnabled: () => boolean = () => this.container.isFeatureEnabled(Constants.Features.ttl90Days); public ttl90DaysEnabled: () => boolean = () => userContext.features.ttl90Days;
public isValid(): boolean { public isValid(): boolean {
// TODO add feature flag that disables validation for customers with custom accounts // TODO add feature flag that disables validation for customers with custom accounts
@@ -1201,7 +1202,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
if (this.isAnalyticalStorageOn()) { if (this.isAnalyticalStorageOn()) {
// TODO: always default to 90 days once the backend hotfix is deployed // TODO: always default to 90 days once the backend hotfix is deployed
return this.container.isFeatureEnabled(Constants.Features.ttl90Days) return userContext.features.ttl90Days
? Constants.AnalyticalStorageTtl.Days90 ? Constants.AnalyticalStorageTtl.Days90
: Constants.AnalyticalStorageTtl.Infinite; : Constants.AnalyticalStorageTtl.Infinite;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,19 @@
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ko from "knockout"; import * as ko from "knockout";
import * as PricingUtils from "../../Utils/PricingUtils"; import * as Constants from "../../Common/Constants";
import * as SharedConstants from "../../Shared/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import editable from "../../Common/EditableUtility";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { createDatabase } from "../../Common/dataAccess/createDatabase"; import { createDatabase } from "../../Common/dataAccess/createDatabase";
import { configContext, Platform } from "../../ConfigContext"; import editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { configContext, Platform } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { SubscriptionType } from "../../Contracts/SubscriptionType"; import { SubscriptionType } from "../../Contracts/SubscriptionType";
import * as ViewModels from "../../Contracts/ViewModels";
import * as SharedConstants from "../../Shared/Constants";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../Utils/PricingUtils";
import { ContextualPaneBase } from "./ContextualPaneBase";
export default class AddDatabasePane extends ContextualPaneBase { export default class AddDatabasePane extends ContextualPaneBase {
public defaultExperience: ko.Computed<string>; public defaultExperience: ko.Computed<string>;
@@ -122,7 +122,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
return ""; return "";
} }
const serverId = this.container.serverId();
const regions = const regions =
(account && (account &&
account.properties && account.properties &&
@@ -134,10 +133,15 @@ export default class AddDatabasePane extends ContextualPaneBase {
let estimatedSpendAcknowledge: string; let estimatedSpendAcknowledge: string;
let estimatedSpend: string; let estimatedSpend: string;
if (!this.isAutoPilotSelected()) { if (!this.isAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster); estimatedSpend = PricingUtils.getEstimatedSpendHtml(
offerThroughput,
userContext.portalEnv,
regions,
multimaster
);
estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
offerThroughput, offerThroughput,
serverId, userContext.portalEnv,
regions, regions,
multimaster, multimaster,
this.isAutoPilotSelected() this.isAutoPilotSelected()
@@ -145,13 +149,13 @@ export default class AddDatabasePane extends ContextualPaneBase {
} else { } else {
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.maxAutoPilotThroughputSet(), this.maxAutoPilotThroughputSet(),
serverId, userContext.portalEnv,
regions, regions,
multimaster multimaster
); );
estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.maxAutoPilotThroughputSet(), this.maxAutoPilotThroughputSet(),
serverId, userContext.portalEnv,
regions, regions,
multimaster, multimaster,
this.isAutoPilotSelected() this.isAutoPilotSelected()
@@ -165,7 +169,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.canRequestSupport = ko.pureComputed(() => { this.canRequestSupport = ko.pureComputed(() => {
if ( if (
configContext.platform !== Platform.Emulator && configContext.platform !== Platform.Emulator &&
!this.container.isTryCosmosDBSubscription() && !userContext.isTryCosmosDBSubscription &&
configContext.platform !== Platform.Portal configContext.platform !== Platform.Portal
) { ) {
const offerThroughput: number = this.throughput(); const offerThroughput: number = this.throughput();
@@ -239,7 +243,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.upsellMessage = ko.pureComputed<string>(() => { this.upsellMessage = ko.pureComputed<string>(() => {
return PricingUtils.getUpsellMessage( return PricingUtils.getUpsellMessage(
this.container.serverId(), userContext.portalEnv,
this.isFreeTierAccount(), this.isFreeTierAccount(),
this.container.isFirstResourceCreated(), this.container.isFirstResourceCreated(),
this.container.defaultExperience(), this.container.defaultExperience(),

View File

@@ -114,7 +114,7 @@
aria-label="Keyspace id" aria-label="Keyspace id"
/> />
<datalist id="keyspacesList" data-bind="foreach: container.nonSystemDatabases"> <datalist id="keyspacesList" data-bind="foreach: container.databases">
<option data-bind="value: $data.id"></option> <option data-bind="value: $data.id"></option>
</datalist> </datalist>

View File

@@ -1,21 +1,21 @@
import * as _ from "underscore";
import * as AddCollectionUtility from "../../Shared/AddCollectionUtility";
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ko from "knockout"; import * as ko from "knockout";
import * as PricingUtils from "../../Utils/PricingUtils"; import * as _ from "underscore";
import * as SharedConstants from "../../Shared/Constants"; import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { HashMap } from "../../Common/HashMap"; import { HashMap } from "../../Common/HashMap";
import { configContext, Platform } from "../../ConfigContext"; import { configContext, Platform } from "../../ConfigContext";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import * as DataModels from "../../Contracts/DataModels";
import { SubscriptionType } from "../../Contracts/SubscriptionType"; import { SubscriptionType } from "../../Contracts/SubscriptionType";
import * as ViewModels from "../../Contracts/ViewModels";
import * as AddCollectionUtility from "../../Shared/AddCollectionUtility";
import * as SharedConstants from "../../Shared/Constants";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../Utils/PricingUtils";
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ContextualPaneBase } from "./ContextualPaneBase";
export default class CassandraAddCollectionPane extends ContextualPaneBase { export default class CassandraAddCollectionPane extends ContextualPaneBase {
public createTableQuery: ko.Observable<string>; public createTableQuery: ko.Observable<string>;
@@ -127,7 +127,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
return ""; return "";
} }
const serverId = this.container.serverId();
const regions = const regions =
(account && (account &&
account.properties && account.properties &&
@@ -139,10 +138,15 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
let estimatedSpend: string; let estimatedSpend: string;
let estimatedDedicatedSpendAcknowledge: string; let estimatedDedicatedSpendAcknowledge: string;
if (!this.isAutoPilotSelected()) { if (!this.isAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster); estimatedSpend = PricingUtils.getEstimatedSpendHtml(
offerThroughput,
userContext.portalEnv,
regions,
multimaster
);
estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
offerThroughput, offerThroughput,
serverId, userContext.portalEnv,
regions, regions,
multimaster, multimaster,
this.isAutoPilotSelected() this.isAutoPilotSelected()
@@ -150,13 +154,13 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
} else { } else {
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.selectedAutoPilotThroughput(), this.selectedAutoPilotThroughput(),
serverId, userContext.portalEnv,
regions, regions,
multimaster multimaster
); );
estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.selectedAutoPilotThroughput(), this.selectedAutoPilotThroughput(),
serverId, userContext.portalEnv,
regions, regions,
multimaster, multimaster,
this.isAutoPilotSelected() this.isAutoPilotSelected()
@@ -172,7 +176,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
return ""; return "";
} }
const serverId = this.container.serverId();
const regions = const regions =
(account && (account &&
account.properties && account.properties &&
@@ -183,10 +186,15 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
let estimatedSpend: string; let estimatedSpend: string;
let estimatedSharedSpendAcknowledge: string; let estimatedSharedSpendAcknowledge: string;
if (!this.isSharedAutoPilotSelected()) { if (!this.isSharedAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml(this.keyspaceThroughput(), serverId, regions, multimaster); estimatedSpend = PricingUtils.getEstimatedSpendHtml(
this.keyspaceThroughput(),
userContext.portalEnv,
regions,
multimaster
);
estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.keyspaceThroughput(), this.keyspaceThroughput(),
serverId, userContext.portalEnv,
regions, regions,
multimaster, multimaster,
this.isSharedAutoPilotSelected() this.isSharedAutoPilotSelected()
@@ -194,13 +202,13 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
} else { } else {
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.sharedAutoPilotThroughput(), this.sharedAutoPilotThroughput(),
serverId, userContext.portalEnv,
regions, regions,
multimaster multimaster
); );
estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.sharedAutoPilotThroughput(), this.sharedAutoPilotThroughput(),
serverId, userContext.portalEnv,
regions, regions,
multimaster, multimaster,
this.isSharedAutoPilotSelected() this.isSharedAutoPilotSelected()
@@ -215,7 +223,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
}); });
this.canRequestSupport = ko.pureComputed(() => { this.canRequestSupport = ko.pureComputed(() => {
if (configContext.platform !== Platform.Emulator && !this.container.isTryCosmosDBSubscription()) { if (configContext.platform !== Platform.Emulator && !userContext.isTryCosmosDBSubscription) {
const offerThroughput: number = this.throughput(); const offerThroughput: number = this.throughput();
return offerThroughput <= 100000; return offerThroughput <= 100000;
} }
@@ -253,10 +261,8 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
}); });
this.keyspaceIds(cachedKeyspaceIdsList); this.keyspaceIds(cachedKeyspaceIdsList);
}; };
this.container.nonSystemDatabases.subscribe((newDatabases: ViewModels.Database[]) => this.container.databases.subscribe((newDatabases: ViewModels.Database[]) => updateKeyspaceIds(newDatabases));
updateKeyspaceIds(newDatabases) updateKeyspaceIds(this.container.databases());
);
updateKeyspaceIds(this.container.nonSystemDatabases());
} }
this.autoPilotUsageCost = ko.pureComputed<string>(() => { this.autoPilotUsageCost = ko.pureComputed<string>(() => {

View File

@@ -133,7 +133,7 @@ describe("Delete Collection Confirmation Pane", () => {
.simulate("change", { target: { value: selectedCollectionId } }); .simulate("change", { target: { value: selectedCollectionId } });
expect(wrapper.exists("#sidePanelOkButton")).toBe(true); expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
wrapper.find("#sidePanelOkButton").hostNodes().simulate("click"); wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId); expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
wrapper.unmount(); wrapper.unmount();
@@ -154,7 +154,7 @@ describe("Delete Collection Confirmation Pane", () => {
.simulate("change", { target: { value: feedbackText } }); .simulate("change", { target: { value: feedbackText } });
expect(wrapper.exists("#sidePanelOkButton")).toBe(true); expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
wrapper.find("#sidePanelOkButton").hostNodes().simulate("click"); wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId); expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
const deleteFeedback = new DeleteFeedback( const deleteFeedback = new DeleteFeedback(

View File

@@ -1,20 +1,19 @@
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as React from "react";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { PanelFooterComponent } from "./PanelFooterComponent";
import { Collection } from "../../Contracts/ViewModels";
import { Text, TextField } from "office-ui-fabric-react"; import { Text, TextField } from "office-ui-fabric-react";
import { userContext } from "../../UserContext"; import * as React from "react";
import { Areas } from "../../Common/Constants"; import { Areas } from "../../Common/Constants";
import { deleteCollection } from "../../Common/dataAccess/deleteCollection"; import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import { PanelErrorComponent, PanelErrorProps } from "./PanelErrorComponent";
import DeleteFeedback from "../../Common/DeleteFeedback"; import DeleteFeedback from "../../Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { Collection } from "../../Contracts/ViewModels";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares.gif"; import { PanelFooterComponent } from "./PanelFooterComponent";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
import { PanelLoadingScreen } from "./PanelLoadingScreen";
export interface DeleteCollectionConfirmationPanelProps { export interface DeleteCollectionConfirmationPanelProps {
explorer: Explorer; explorer: Explorer;
closePanel: () => void; closePanel: () => void;
@@ -44,8 +43,8 @@ export class DeleteCollectionConfirmationPanel extends React.Component<
render(): JSX.Element { render(): JSX.Element {
return ( return (
<div className="panelContentContainer"> <form className="panelFormWrapper" onSubmit={this.submit.bind(this)}>
<PanelErrorComponent {...this.getPanelErrorProps()} /> <PanelInfoErrorComponent {...this.getPanelErrorProps()} />
<div className="panelMainContent"> <div className="panelMainContent">
<div className="confirmDeleteInput"> <div className="confirmDeleteInput">
<span className="mandatoryStar">* </span> <span className="mandatoryStar">* </span>
@@ -79,18 +78,16 @@ export class DeleteCollectionConfirmationPanel extends React.Component<
</div> </div>
)} )}
</div> </div>
<PanelFooterComponent buttonLabel="OK" onOKButtonClicked={() => this.submit()} /> <PanelFooterComponent buttonLabel="OK" />
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!this.state.isExecuting}> {this.state.isExecuting && <PanelLoadingScreen />}
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} /> </form>
</div>
</div>
); );
} }
private getPanelErrorProps(): PanelErrorProps { private getPanelErrorProps(): PanelInfoErrorProps {
if (this.state.formError) { if (this.state.formError) {
return { return {
isWarning: false, messageType: "error",
message: this.state.formError, message: this.state.formError,
showErrorDetails: true, showErrorDetails: true,
openNotificationConsole: this.props.openNotificationConsole, openNotificationConsole: this.props.openNotificationConsole,
@@ -98,7 +95,7 @@ export class DeleteCollectionConfirmationPanel extends React.Component<
} }
return { return {
isWarning: true, messageType: "warning",
showErrorDetails: false, showErrorDetails: false,
message: message:
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.", "Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
@@ -109,9 +106,10 @@ export class DeleteCollectionConfirmationPanel extends React.Component<
return this.props.explorer.isLastCollection() && !this.props.explorer.isSelectedDatabaseShared(); return this.props.explorer.isLastCollection() && !this.props.explorer.isSelectedDatabaseShared();
} }
public async submit(): Promise<void> { public async submit(event: React.FormEvent<HTMLFormElement>): Promise<void> {
const collection = this.props.explorer.findSelectedCollection(); event.preventDefault();
const collection = this.props.explorer.findSelectedCollection();
if (!collection || this.inputCollectionName !== collection.id()) { if (!collection || this.inputCollectionName !== collection.id()) {
const errorMessage = "Input collection name does not match the selected collection"; const errorMessage = "Input collection name does not match the selected collection";
this.setState({ formError: errorMessage }); this.setState({ formError: errorMessage });

View File

@@ -1,109 +0,0 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="deletedatabaseconfirmationpane">
<!-- Delete Databaes Confirmation form - Start -->
<div class="contextual-pane-in">
<form
class="paneContentContainer"
data-bind="
submit: submit"
>
<!-- Delete Database Confirmation header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Delete Database Confirmation header - End -->
<div class="warningErrorContainer" data-bind="visible: !formErrors()">
<div class="warningErrorContent">
<span><img class="paneWarningIcon" src="/warning.svg" alt="Warning" /></span>
<span class="warningErrorDetailsLinkContainer">
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this
resource and all of its children resources.
</span>
</div>
</div>
<!-- Delete Database Confirmation errors - Start -->
<div
class="warningErrorContainer"
aria-live="assertive"
data-bind="
visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error" /></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
<a class="errorLink" role="link" data-bind="click: showErrorDetails">More details</a>
</span>
</div>
</div>
<!-- Delete Database Confirmation errors - End -->
<!-- Delete Database Confirmation inputs - Start -->
<div class="paneMainContent">
<div>
<span class="mandatoryStar">*</span> <span data-bind="text: databaseIdConfirmationText"></span>
<p>
<input
type="text"
name="databaseIdConfirmation"
data-test="confirmDatabaseId"
required
class="collid"
data-bind="value: databaseIdConfirmation, hasFocus: firstFieldHasFocus"
aria-label="Confirm by typing the database id"
/>
</p>
</div>
<div data-bind="visible: recordDeleteFeedback">
<div>Help us improve Azure Cosmos DB!</div>
<div>What is the reason why you are deleting this database?</div>
<p>
<textarea
type="text"
data-test="databaseDeleteFeedback"
name="databaseDeleteFeedback"
rows="3"
cols="53"
maxlength="512"
class="collid"
data-bind="value: databaseDeleteFeedback"
aria-label="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
>
</textarea>
</p>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input type="submit" data-test="deleteDatabase" value="OK" class="btncreatecoll1" />
</div>
</div>
<!-- Delete Database Confirmation inputs - End -->
</form>
</div>
<!-- Delete Database Confirmation form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -1,127 +0,0 @@
jest.mock("../../Common/dataAccess/deleteDatabase");
jest.mock("../../Shared/Telemetry/TelemetryProcessor");
import * as ko from "knockout";
import Q from "q";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import DeleteDatabaseConfirmationPane from "./DeleteDatabaseConfirmationPane";
import DeleteFeedback from "../../Common/DeleteFeedback";
import Explorer from "../Explorer";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { TreeNode } from "../../Contracts/ViewModels";
import { TabsManager } from "../Tabs/TabsManager";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
describe("Delete Database Confirmation Pane", () => {
describe("Explorer.isLastDatabase() and Explorer.isLastNonEmptyDatabase()", () => {
let explorer: Explorer;
beforeAll(() => {
(deleteDatabase as jest.Mock).mockResolvedValue(undefined);
});
beforeEach(() => {
explorer = new Explorer();
});
it("should be true if only 1 database", () => {
let database = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastDatabase()).toBe(true);
});
it("should be false if only 2 databases", () => {
let database = {} as ViewModels.Database;
let database2 = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database, database2]);
expect(explorer.isLastDatabase()).toBe(false);
});
it("should be false if not last empty database", () => {
let database = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastNonEmptyDatabase()).toBe(false);
});
it("should be true if last non empty database", () => {
let database = {} as ViewModels.Database;
database.collections = ko.observableArray<ViewModels.Collection>([{} as ViewModels.Collection]);
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastNonEmptyDatabase()).toBe(true);
});
});
describe("shouldRecordFeedback()", () => {
it("should return true if last non empty database or is last database that has shared throughput, else false", () => {
let fakeExplorer = {} as Explorer;
let pane = new DeleteDatabaseConfirmationPane({
id: "deletedatabaseconfirmationpane",
visible: ko.observable<boolean>(false),
container: fakeExplorer as any,
});
fakeExplorer.isLastNonEmptyDatabase = () => true;
pane.container = fakeExplorer as any;
expect(pane.shouldRecordFeedback()).toBe(true);
fakeExplorer.isLastDatabase = () => true;
fakeExplorer.isSelectedDatabaseShared = () => true;
pane.container = fakeExplorer as any;
expect(pane.shouldRecordFeedback()).toBe(true);
fakeExplorer.isLastNonEmptyDatabase = () => false;
fakeExplorer.isLastDatabase = () => true;
fakeExplorer.isSelectedDatabaseShared = () => false;
pane.container = fakeExplorer as any;
expect(pane.shouldRecordFeedback()).toBe(false);
});
});
describe("submit()", () => {
it("on submit() it should log feedback if last non empty database or is last database that has shared throughput", () => {
let selectedDatabaseId = "testDB";
let fakeExplorer = {} as Explorer;
fakeExplorer.findSelectedDatabase = () => {
return {
id: ko.observable<string>(selectedDatabaseId),
rid: "test",
collections: ko.observableArray<ViewModels.Collection>(),
} as ViewModels.Database;
};
fakeExplorer.refreshAllDatabases = () => Q.resolve();
fakeExplorer.selectedDatabaseId = ko.computed<string>(() => selectedDatabaseId);
fakeExplorer.isSelectedDatabaseShared = () => false;
const SubscriptionId = "testId";
const AccountName = "testAccount";
fakeExplorer.databaseAccount = ko.observable<DataModels.DatabaseAccount>({
id: SubscriptionId,
name: AccountName,
} as DataModels.DatabaseAccount);
fakeExplorer.defaultExperience = ko.observable<string>("DocumentDB");
fakeExplorer.isPreferredApiCassandra = ko.computed(() => {
return false;
});
fakeExplorer.selectedNode = ko.observable<TreeNode>();
fakeExplorer.tabsManager = new TabsManager();
fakeExplorer.isLastNonEmptyDatabase = () => true;
let pane = new DeleteDatabaseConfirmationPane({
id: "deletedatabaseconfirmationpane",
visible: ko.observable<boolean>(false),
container: fakeExplorer as any,
});
pane.databaseIdConfirmation = ko.observable<string>(selectedDatabaseId);
const Feedback = "my feedback";
pane.databaseDeleteFeedback(Feedback);
return pane.submit().then(() => {
let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback);
expect(TelemetryProcessor.trace).toHaveBeenCalledWith(Action.DeleteDatabase, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
});
});
});
});

View File

@@ -1,143 +0,0 @@
import * as ko from "knockout";
import Q from "q";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import DeleteFeedback from "../../Common/DeleteFeedback";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
import { ARMError } from "../../Utils/arm/request";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
public databaseIdConfirmationText: ko.Observable<string>;
public databaseIdConfirmation: ko.Observable<string>;
public databaseDeleteFeedback: ko.Observable<string>;
public recordDeleteFeedback: ko.Observable<boolean>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.databaseIdConfirmationText = ko.observable<string>("Confirm by typing the database id");
this.databaseIdConfirmation = ko.observable<string>();
this.databaseDeleteFeedback = ko.observable<string>();
this.recordDeleteFeedback = ko.observable<boolean>(false);
this.title("Delete Database");
this.resetData();
}
public submit(): Q.Promise<any> {
if (!this._isValid()) {
const selectedDatabase: ViewModels.Database = this.container.findSelectedDatabase();
this.formErrors("Input database name does not match the selected database");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while deleting collection ${selectedDatabase && selectedDatabase.id()}: ${this.formErrors()}`
);
return Q.resolve();
}
this.formErrors("");
this.isExecuting(true);
const selectedDatabase = this.container.findSelectedDatabase();
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDatabase, {
databaseId: selectedDatabase.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
});
return Q(
deleteDatabase(selectedDatabase.id()).then(
() => {
this.isExecuting(false);
this.close();
this.container.refreshAllDatabases();
this.container.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
this.container.selectedNode(null);
selectedDatabase
.collections()
.forEach((collection: ViewModels.Collection) =>
this.container.tabsManager.closeTabsByComparator(
(tab) =>
tab.node?.id() === collection.id() &&
(tab.node as ViewModels.Collection).databaseId === collection.databaseId
)
);
this.resetData();
TelemetryProcessor.traceSuccess(
Action.DeleteDatabase,
{
databaseId: selectedDatabase.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
},
startKey
);
if (this.shouldRecordFeedback()) {
let deleteFeedback = new DeleteFeedback(
this.container.databaseAccount().id,
this.container.databaseAccount().name,
DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience()),
this.databaseDeleteFeedback()
);
TelemetryProcessor.trace(Action.DeleteDatabase, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
this.databaseDeleteFeedback("");
}
},
(error: any) => {
this.isExecuting(false);
const errorMessage = getErrorMessage(error);
this.formErrors(errorMessage);
this.formErrorsDetails(errorMessage);
TelemetryProcessor.traceFailure(
Action.DeleteDatabase,
{
databaseId: selectedDatabase.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
)
);
}
public resetData() {
this.databaseIdConfirmation("");
super.resetData();
}
public async open() {
await this.container.loadSelectedDatabaseOffer();
this.recordDeleteFeedback(this.shouldRecordFeedback());
super.open();
}
public shouldRecordFeedback(): boolean {
return (
this.container.isLastNonEmptyDatabase() ||
(this.container.isLastDatabase() && this.container.isSelectedDatabaseShared())
);
}
private _isValid(): boolean {
const selectedDatabase = this.container.findSelectedDatabase();
if (!selectedDatabase) {
return false;
}
return this.databaseIdConfirmation() === selectedDatabase.id();
}
}

View File

@@ -0,0 +1,139 @@
jest.mock("../../Common/dataAccess/deleteDatabase");
jest.mock("../../Shared/Telemetry/TelemetryProcessor");
import { mount, ReactWrapper, shallow } from "enzyme";
import * as ko from "knockout";
import React from "react";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
import DeleteFeedback from "../../Common/DeleteFeedback";
import { ApiKind, DatabaseAccount } from "../../Contracts/DataModels";
import { Collection, Database } from "../../Contracts/ViewModels";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import { DeleteDatabaseConfirmationPanel } from "./DeleteDatabaseConfirmationPanel";
describe("Delete Database Confirmation Pane", () => {
describe("shouldRecordFeedback()", () => {
it("should return true if last non empty database or is last database that has shared throughput, else false", () => {
const fakeExplorer = new Explorer();
fakeExplorer.refreshAllDatabases = () => undefined;
fakeExplorer.isLastCollection = () => true;
fakeExplorer.isSelectedDatabaseShared = () => false;
const database = {} as Database;
database.collections = ko.observableArray<Collection>([{} as Collection]);
database.id = ko.observable<string>("testDatabse");
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
openNotificationConsole: (): void => undefined,
selectedDatabase: database,
};
const wrapper = shallow(<DeleteDatabaseConfirmationPanel {...props} />);
props.explorer.isLastNonEmptyDatabase = () => true;
wrapper.setProps(props);
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true);
props.explorer.isLastNonEmptyDatabase = () => false;
props.explorer.isLastDatabase = () => false;
wrapper.setProps(props);
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false);
props.explorer.isLastNonEmptyDatabase = () => false;
props.explorer.isLastDatabase = () => true;
props.explorer.isSelectedDatabaseShared = () => false;
wrapper.setProps(props);
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false);
});
});
describe("submit()", () => {
const selectedDatabaseId = "testDatabse";
const fakeExplorer = new Explorer();
fakeExplorer.refreshAllDatabases = () => undefined;
fakeExplorer.isLastCollection = () => true;
fakeExplorer.isSelectedDatabaseShared = () => false;
let wrapper: ReactWrapper;
beforeAll(() => {
updateUserContext({
databaseAccount: {
name: "testDatabaseAccountName",
properties: {
cassandraEndpoint: "testEndpoint",
},
id: "testDatabaseAccountId",
} as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB,
});
(deleteDatabase as jest.Mock).mockResolvedValue(undefined);
(TelemetryProcessor.trace as jest.Mock).mockReturnValue(undefined);
});
beforeEach(() => {
const database = {} as Database;
database.collections = ko.observableArray<Collection>([{} as Collection]);
database.id = ko.observable<string>(selectedDatabaseId);
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
openNotificationConsole: (): void => undefined,
selectedDatabase: database,
};
wrapper = mount(<DeleteDatabaseConfirmationPanel {...props} />);
props.explorer.isLastNonEmptyDatabase = () => true;
wrapper.setProps(props);
});
it("Should call delete database", () => {
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
wrapper
.find("#confirmDatabaseId")
.hostNodes()
.simulate("change", { target: { value: selectedDatabaseId } });
expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
expect(deleteDatabase).toHaveBeenCalledWith(selectedDatabaseId);
wrapper.unmount();
});
it("should record feedback", async () => {
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
wrapper
.find("#confirmDatabaseId")
.hostNodes()
.simulate("change", { target: { value: selectedDatabaseId } });
expect(wrapper.exists("#deleteDatabaseFeedbackInput")).toBe(true);
const feedbackText = "Test delete Database feedback text";
wrapper
.find("#deleteDatabaseFeedbackInput")
.hostNodes()
.simulate("change", { target: { value: feedbackText } });
expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
expect(deleteDatabase).toHaveBeenCalledWith(selectedDatabaseId);
const deleteFeedback = new DeleteFeedback(
"testDatabaseAccountId",
"testDatabaseAccountName",
ApiKind.SQL,
feedbackText
);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(TelemetryProcessor.trace).toHaveBeenCalledWith(Action.DeleteDatabase, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
wrapper.unmount();
});
});
});

View File

@@ -0,0 +1,168 @@
import { useBoolean } from "@uifabric/react-hooks";
import { Text, TextField } from "office-ui-fabric-react";
import React, { FunctionComponent, useState } from "react";
import { Areas } from "../../Common/Constants";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
import DeleteFeedback from "../../Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { Collection, Database } from "../../Contracts/ViewModels";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { PanelFooterComponent } from "./PanelFooterComponent";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
import { PanelLoadingScreen } from "./PanelLoadingScreen";
interface DeleteDatabaseConfirmationPanelProps {
explorer: Explorer;
closePanel: () => void;
openNotificationConsole: () => void;
selectedDatabase: Database;
}
export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseConfirmationPanelProps> = (
props: DeleteDatabaseConfirmationPanelProps
): JSX.Element => {
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [formError, setFormError] = useState<string>("");
const [databaseInput, setDatabaseInput] = useState<string>("");
const [databaseFeedbackInput, setDatabaseFeedbackInput] = useState<string>("");
const getPanelErrorProps = (): PanelInfoErrorProps => {
if (formError) {
return {
messageType: "error",
message: formError,
showErrorDetails: true,
openNotificationConsole: props.openNotificationConsole,
};
}
return {
messageType: "warning",
showErrorDetails: false,
message:
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
};
};
const submit = async (event: React.FormEvent<HTMLFormElement>): Promise<void> => {
const { selectedDatabase, explorer } = props;
event.preventDefault();
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
setFormError("Input database name does not match the selected database");
logConsoleError(`Error while deleting collection ${selectedDatabase && selectedDatabase.id()}`);
return;
}
setFormError("");
setLoadingTrue();
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDatabase, {
databaseId: selectedDatabase.id(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: "Delete Database",
});
try {
await deleteDatabase(selectedDatabase.id());
props.closePanel();
explorer.refreshAllDatabases();
explorer.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
explorer.selectedNode(undefined);
selectedDatabase
.collections()
.forEach((collection: Collection) =>
explorer.tabsManager.closeTabsByComparator(
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
)
);
TelemetryProcessor.traceSuccess(
Action.DeleteDatabase,
{
databaseId: selectedDatabase.id(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: "Delete Database",
},
startKey
);
if (shouldRecordFeedback()) {
const deleteFeedback = new DeleteFeedback(
userContext?.databaseAccount.id,
userContext?.databaseAccount.name,
DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.defaultExperience),
databaseFeedbackInput
);
TelemetryProcessor.trace(Action.DeleteDatabase, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
}
} catch (error) {
setLoadingFalse();
setFormError(error);
const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure(
Action.DeleteDatabase,
{
databaseId: selectedDatabase.id(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: "Delete Database",
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
};
const shouldRecordFeedback = (): boolean => {
const { explorer } = props;
return explorer.isLastNonEmptyDatabase() || (explorer.isLastDatabase() && explorer.isSelectedDatabaseShared());
};
return (
<form className="panelFormWrapper" onSubmit={submit}>
<PanelInfoErrorComponent {...getPanelErrorProps()} />
<div className="panelMainContent">
<div className="confirmDeleteInput">
<span className="mandatoryStar">* </span>
<Text variant="small">Confirm by typing the database id</Text>
<TextField
id="confirmDatabaseId"
autoFocus
styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => {
setDatabaseInput(newInput);
}}
/>
</div>
{shouldRecordFeedback() && (
<div className="deleteDatabaseFeedback">
<Text variant="small" block>
Help us improve Azure Cosmos DB!
</Text>
<Text variant="small" block>
What is the reason why you are deleting this database?
</Text>
<TextField
id="deleteDatabaseFeedbackInput"
styles={{ fieldGroup: { width: 300 } }}
multiline
rows={3}
onChange={(event, newInput?: string) => {
setDatabaseFeedbackInput(newInput);
}}
/>
</div>
)}
</div>
<PanelFooterComponent buttonLabel="OK" />
{isLoading && <PanelLoadingScreen />}
</form>
);
};

View File

@@ -1,175 +0,0 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="executesprocparamspane">
<!-- Input params form -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: execute">
<!-- Input params header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Input params header - End -->
<!-- Input params errors - Start -->
<div
class="warningErrorContainer"
aria-live="assertive"
data-bind="visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error" /></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
<a
class="errorLink"
role="link"
data-bind="
visible: formErrorsDetails() && formErrorsDetails() !== '',
click: showErrorDetails"
>More details</a
>
</span>
</div>
</div>
<!-- Input params errors - End -->
<!-- Script for each param clause to be used for executing a stored procedure -->
<script type="text/html" id="param-template">
<tr>
<td class="paramTemplateRow">
<select class="dataTypeSelector" data-bind="value: type, attr: { 'aria-label': type }">
<option value="custom">Custom</option>
<option value="string">String</option>
</select>
</td>
<td class="paramTemplateRow">
<input class="valueTextBox" aria-label="Param" data-bind="textInput: value" />
<span
class="spEntityAddCancel"
data-bind="click: $parent.deleteParam.bind($parent, $index()), event: { keypress: $parent.onDeleteParamKeyPress.bind($parent, $index()) }"
role="button"
tabindex="0"
>
<img src="/Entity_cancel.svg" alt="Delete param" />
</span>
<span
class="spEntityAddCancel"
data-bind="click: $parent.addNewParamAtIndex.bind($parent, $index()), event: { keypress: $parent.onAddNewParamAtIndexKeyPress.bind($parent, $index()) }"
role="button"
tabindex="0"
>
<img src="/Add-property.svg" alt="Add param" />
</span>
</td>
</tr>
</script>
<!-- Input params input - Start -->
<div class="paneMainContent">
<div>
<!-- Partition key input - Start -->
<div class="partitionKeyContainer" data-bind="visible: collectionHasPartitionKey">
<div class="inputHeader">Partition key value</div>
<div class="scrollBox">
<table class="paramsClauseTable">
<thead>
<tr>
<th>Type</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td class="paramTemplateRow">
<select
class="dataTypeSelector"
data-bind="value: partitionKeyType, attr: { 'aria-label': partitionKeyType }"
>
<option value="custom">Custom</option>
<option value="string">String</option>
</select>
</td>
<td class="paramTemplateRow">
<input
class="partitionKeyValue"
id="partitionKeyValue"
role="textbox"
tabindex="0"
aria-label="Partition key value"
data-bind="textInput: partitionKeyValue"
autofocus
/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Partition key input - End -->
<!-- Input params table - Start -->
<div class="paramsTable">
<div class="enterInputParams">Enter input parameters (if any)</div>
<div class="scrollBox" id="executeSprocParamsScroll">
<table class="paramsClauseTable">
<thead>
<tr>
<th class="paramTableTypeHead">Type</th>
<th>Param</th>
</tr>
</thead>
<tbody data-bind="template: { name: 'param-template', foreach: params }"></tbody>
</table>
</div>
<div
id="addNewParamLink"
class="addNewParam"
data-bind="click: addNewParam, event: { keypress: onAddNewParamKeyPress }, attr:{ title: addNewParamLabel }"
role="button"
tabindex="0"
>
<span>
<img src="/Add-property.svg" alt="Add new param" />
<span class="addNewParamLabel" data-bind="text: addNewParamLabel" />
</span>
</div>
</div>
<!-- Input params table - End -->
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input
type="submit"
value="Execute"
class="btncreatecoll1"
data-bind="{ css: { btnDisabled: !executeButtonEnabled() }}"
/>
</div>
</div>
<!-- Input param input - End -->
</form>
</div>
<!-- Input params form - End -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -1,172 +0,0 @@
import * as ko from "knockout";
import * as _ from "underscore";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
import StoredProcedure from "../Tree/StoredProcedure";
export interface ExecuteSprocParam {
type: ko.Observable<string>;
value: ko.Observable<string>;
}
type UnwrappedExecuteSprocParam = {
type: string;
value: any;
};
export class ExecuteSprocParamsPane extends ContextualPaneBase {
public params: ko.ObservableArray<ExecuteSprocParam>;
public partitionKeyType: ko.Observable<string>;
public partitionKeyValue: ko.Observable<string>;
public collectionHasPartitionKey: ko.Observable<boolean>;
public addNewParamLabel: string = "Add New Param";
public executeButtonEnabled: ko.Computed<boolean>;
private _selectedSproc: StoredProcedure;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Input parameters");
this.partitionKeyType = ko.observable<string>("custom");
this.partitionKeyValue = ko.observable<string>();
this.executeButtonEnabled = ko.computed<boolean>(() => this.validPartitionKeyValue());
this.params = ko.observableArray<ExecuteSprocParam>([
{ type: ko.observable<string>("string"), value: ko.observable<string>() },
]);
this.collectionHasPartitionKey = ko.observable<boolean>();
this.resetData();
}
public open() {
super.open();
const currentSelectedSproc = this.container && this.container.findSelectedStoredProcedure();
if (!!currentSelectedSproc && !!this._selectedSproc && this._selectedSproc.rid !== currentSelectedSproc.rid) {
this.params([]);
this.partitionKeyValue("");
}
this._selectedSproc = currentSelectedSproc;
this.collectionHasPartitionKey((this.container && !!this.container.findSelectedCollection().partitionKey) || false);
const focusElement = document.getElementById("partitionKeyValue");
focusElement && focusElement.focus();
}
public execute = () => {
this.formErrors("");
const partitionKeyValue: string = (() => {
if (!this.collectionHasPartitionKey()) {
return undefined;
}
const type: string = this.partitionKeyType();
let value: string = this.partitionKeyValue();
if (type === "custom") {
if (value === "undefined" || value === undefined) {
return undefined;
}
if (value === "null" || value === null) {
return null;
}
try {
value = JSON.parse(value);
} catch (e) {
this.formErrors(`Invalid param specified: ${value}`);
this.formErrorsDetails(`Invalid param specified: ${value} is not a valid literal value`);
}
}
return value;
})();
const unwrappedParams: UnwrappedExecuteSprocParam[] = ko.toJS(this.params());
const wrappedSprocParams: UnwrappedExecuteSprocParam[] = !this.params()
? undefined
: _.map(unwrappedParams, (unwrappedParam: UnwrappedExecuteSprocParam) => {
let paramValue: string = unwrappedParam.value;
if (unwrappedParam.type === "custom" && (paramValue === "undefined" || paramValue === "")) {
paramValue = undefined;
} else if (unwrappedParam.type === "custom") {
try {
paramValue = JSON.parse(paramValue);
} catch (e) {
this.formErrors(`Invalid param specified: ${paramValue}`);
this.formErrorsDetails(`Invalid param specified: ${paramValue} is not a valid literal value`);
}
}
unwrappedParam.value = paramValue;
return unwrappedParam;
});
if (this.formErrors()) {
return;
}
const sprocParams = wrappedSprocParams && _.pluck(wrappedSprocParams, "value");
this._selectedSproc.execute(sprocParams, partitionKeyValue);
this.close();
};
private validPartitionKeyValue = (): boolean => {
return !this.collectionHasPartitionKey || (this.partitionKeyValue() != null && this.partitionKeyValue().length > 0);
};
public addNewParam = (): void => {
this.params.push({ type: ko.observable<string>("string"), value: ko.observable<string>() });
this._maintainFocusOnAddNewParamLink();
};
public onAddNewParamKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.addNewParam();
event.stopPropagation();
return false;
}
return true;
};
public addNewParamAtIndex = (index: number): void => {
this.params.splice(index, 0, { type: ko.observable<string>("string"), value: ko.observable<string>() });
};
public onAddNewParamAtIndexKeyPress = (index: number, source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.addNewParamAtIndex(index);
event.stopPropagation();
return false;
}
return true;
};
public deleteParam = (indexToRemove: number): void => {
const params = _.reject(this.params(), (param: ExecuteSprocParam, index: number) => {
return index === indexToRemove;
});
this.params(params);
};
public onDeleteParamKeyPress = (indexToRemove: number, source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.deleteParam(indexToRemove);
event.stopPropagation();
return false;
}
return true;
};
public close(): void {
super.close();
this.formErrors("");
this.formErrorsDetails("");
}
private _maintainFocusOnAddNewParamLink(): void {
const addNewParamLink = document.getElementById("addNewParamLink");
addNewParamLink.scrollIntoView();
}
}

View File

@@ -0,0 +1,91 @@
import {
Dropdown,
IDropdownOption,
IDropdownStyles,
IImageProps,
Image,
Label,
Stack,
TextField,
} from "office-ui-fabric-react";
import React, { FunctionComponent } from "react";
import AddPropertyIcon from "../../../../images/Add-property.svg";
import EntityCancelIcon from "../../../../images/Entity_cancel.svg";
const dropdownStyles: Partial<IDropdownStyles> = { dropdown: { width: 100 } };
const options = [
{ key: "string", text: "String" },
{ key: "custom", text: "Custom" },
];
export interface InputParameterProps {
dropdownLabel?: string;
inputParameterTitle?: string;
inputLabel?: string;
isAddRemoveVisible: boolean;
onDeleteParamKeyPress?: () => void;
onAddNewParamKeyPress?: () => void;
onParamValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
onParamKeyChange: (event: React.FormEvent<HTMLElement>, selectedParam: IDropdownOption) => void;
paramValue: string;
selectedKey: string | number;
}
export const InputParameter: FunctionComponent<InputParameterProps> = ({
dropdownLabel,
inputParameterTitle,
inputLabel,
isAddRemoveVisible,
paramValue,
selectedKey,
onDeleteParamKeyPress,
onAddNewParamKeyPress,
onParamValueChange,
onParamKeyChange,
}: InputParameterProps): JSX.Element => {
const imageProps: IImageProps = {
width: 20,
height: 30,
className: dropdownLabel ? "addRemoveIconLabel" : "addRemoveIcon",
};
return (
<>
{inputParameterTitle && <Label>{inputParameterTitle}</Label>}
<Stack horizontal>
<Dropdown
label={dropdownLabel && dropdownLabel}
selectedKey={selectedKey}
onChange={onParamKeyChange}
options={options}
styles={dropdownStyles}
/>
<TextField
label={inputLabel && inputLabel}
id="confirmCollectionId"
autoFocus
value={paramValue}
onChange={onParamValueChange}
/>
{isAddRemoveVisible && (
<>
<Image
{...imageProps}
src={EntityCancelIcon}
alt="Delete param"
id="deleteparam"
onClick={onDeleteParamKeyPress}
/>
<Image
{...imageProps}
src={AddPropertyIcon}
alt="Add param"
id="addparam"
onClick={onAddNewParamKeyPress}
/>
</>
)}
</Stack>
</>
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
import { mount } from "enzyme";
import React from "react";
import Explorer from "../../Explorer";
import { ExecuteSprocParamsPanel } from "./index";
describe("Excute Sproc Param Pane", () => {
const fakeExplorer = {} as Explorer;
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
};
it("should render Default properly", () => {
const wrapper = mount(<ExecuteSprocParamsPanel {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("initially display 2 input field, 1 partition and 1 parameter", () => {
const wrapper = mount(<ExecuteSprocParamsPanel {...props} />);
expect(wrapper.find("input[type='text']")).toHaveLength(2);
});
it("add a new parameter field", () => {
const wrapper = mount(<ExecuteSprocParamsPanel {...props} />);
wrapper.find("#addparam").last().simulate("click");
expect(wrapper.find("input[type='text']")).toHaveLength(3);
});
it("remove a parameter field", () => {
const wrapper = mount(<ExecuteSprocParamsPanel {...props} />);
wrapper.find("#deleteparam").last().simulate("click");
expect(wrapper.find("input[type='text']")).toHaveLength(1);
});
});

View File

@@ -0,0 +1,163 @@
import { useBoolean } from "@uifabric/react-hooks";
import { IDropdownOption, IImageProps, Image, Stack, Text } from "office-ui-fabric-react";
import React, { FunctionComponent, useState } from "react";
import AddPropertyIcon from "../../../../images/Add-property.svg";
import Explorer from "../../Explorer";
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
import { InputParameter } from "./InputParameter";
interface ExecuteSprocParamsPaneProps {
explorer: Explorer;
closePanel: () => void;
}
const imageProps: IImageProps = {
width: 20,
height: 30,
};
interface UnwrappedExecuteSprocParam {
key: string;
text: string;
}
export const ExecuteSprocParamsPanel: FunctionComponent<ExecuteSprocParamsPaneProps> = ({
explorer,
closePanel,
}: ExecuteSprocParamsPaneProps): JSX.Element => {
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [paramKeyValues, setParamKeyValues] = useState<UnwrappedExecuteSprocParam[]>([{ key: "string", text: "" }]);
const [partitionValue, setPartitionValue] = useState<string>("");
const [selectedKey, setSelectedKey] = React.useState<IDropdownOption>({ key: "string", text: "" });
const [formError, setFormError] = useState<string>("");
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
const onPartitionKeyChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
setSelectedKey(item);
};
const genericPaneProps: GenericRightPaneProps = {
container: explorer,
formError: formError,
formErrorDetail: formErrorsDetails,
id: "executesprocparamspane",
isExecuting: isLoading,
title: "Input parameters",
submitButtonText: "Execute",
onClose: () => closePanel(),
onSubmit: () => submit(),
};
const validateUnwrappedParams = (): boolean => {
const unwrappedParams: UnwrappedExecuteSprocParam[] = paramKeyValues;
for (let i = 0; i < unwrappedParams.length; i++) {
const { key: paramType, text: paramValue } = unwrappedParams[i];
if (paramType === "custom" && (paramValue === "" || paramValue === undefined)) {
return false;
}
}
return true;
};
const setInvalidParamError = (invalidParam: string): void => {
setFormError(`Invalid param specified: ${invalidParam}`);
setFormErrorsDetails(`Invalid param specified: ${invalidParam} is not a valid literal value`);
};
const submit = (): void => {
const wrappedSprocParams: UnwrappedExecuteSprocParam[] = paramKeyValues;
const { key: partitionKey } = selectedKey;
if (partitionKey === "custom" && (partitionValue === "" || partitionValue === undefined)) {
setInvalidParamError(partitionValue);
return;
}
if (!validateUnwrappedParams()) {
setInvalidParamError("");
return;
}
setLoadingTrue();
const sprocParams = wrappedSprocParams && wrappedSprocParams.map((sprocParam) => sprocParam.text);
const currentSelectedSproc = explorer.findSelectedStoredProcedure();
currentSelectedSproc.execute(sprocParams, partitionValue);
setLoadingFalse();
closePanel();
};
const deleteParamAtIndex = (indexToRemove: number): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue.splice(indexToRemove, 1);
setParamKeyValues(cloneParamKeyValue);
};
const addNewParamAtIndex = (indexToAdd: number): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue.splice(indexToAdd, 0, { key: "string", text: "" });
setParamKeyValues(cloneParamKeyValue);
};
const paramValueChange = (value: string, indexOfInput: number): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue[indexOfInput].text = value;
setParamKeyValues(cloneParamKeyValue);
};
const paramKeyChange = (
_event: React.FormEvent<HTMLDivElement>,
selectedParam: IDropdownOption,
indexOfParam: number
): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue[indexOfParam].key = selectedParam.key.toString();
setParamKeyValues(cloneParamKeyValue);
};
const addNewParamAtLastIndex = (): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue.splice(cloneParamKeyValue.length, 0, { key: "string", text: "" });
setParamKeyValues(cloneParamKeyValue);
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<div className="panelFormWrapper">
<div className="panelMainContent">
<InputParameter
dropdownLabel="Key"
inputParameterTitle="Partition key value"
inputLabel="Value"
isAddRemoveVisible={false}
onParamValueChange={(_event, newInput?: string) => {
setPartitionValue(newInput);
}}
onParamKeyChange={onPartitionKeyChange}
paramValue={partitionValue}
selectedKey={selectedKey.key}
/>
{paramKeyValues.map((paramKeyValue, index) => (
<InputParameter
key={paramKeyValue && paramKeyValue.text + index}
dropdownLabel={!index && "Key"}
inputParameterTitle={!index && "Enter input parameters (if any)"}
inputLabel={!index && "Param"}
isAddRemoveVisible={true}
onDeleteParamKeyPress={() => deleteParamAtIndex(index)}
onAddNewParamKeyPress={() => addNewParamAtIndex(index + 1)}
onParamValueChange={(event, newInput?: string) => {
paramValueChange(newInput, index);
}}
onParamKeyChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
paramKeyChange(event, selectedParam, index);
}}
paramValue={paramKeyValue && paramKeyValue.text}
selectedKey={paramKeyValue && paramKeyValue.key}
/>
))}
<Stack horizontal onClick={addNewParamAtLastIndex}>
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
<Text className="addNewParamStyle">Add New Param</Text>
</Stack>
</div>
</div>
</GenericRightPaneComponent>
);
};

View File

@@ -1,9 +1,9 @@
import * as React from "react";
import { IconButton, PrimaryButton } from "office-ui-fabric-react/lib/Button";
import { KeyCodes } from "../../Common/Constants";
import { Subscription } from "knockout"; import { Subscription } from "knockout";
import { IconButton, PrimaryButton } from "office-ui-fabric-react/lib/Button";
import * as React from "react";
import ErrorRedIcon from "../../../images/error_red.svg"; import ErrorRedIcon from "../../../images/error_red.svg";
import LoadingIndicatorIcon from "../../../images/LoadingIndicator_3Squares.gif"; import LoadingIndicatorIcon from "../../../images/LoadingIndicator_3Squares.gif";
import { KeyCodes } from "../../Common/Constants";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
export interface GenericRightPaneProps { export interface GenericRightPaneProps {

View File

@@ -1,24 +1,19 @@
import AddDatabasePaneTemplate from "./AddDatabasePane.html";
import AddCollectionPaneTemplate from "./AddCollectionPane.html"; import AddCollectionPaneTemplate from "./AddCollectionPane.html";
import AddDatabasePaneTemplate from "./AddDatabasePane.html";
import BrowseQueriesPaneTemplate from "./BrowseQueriesPane.html";
import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html";
import DeleteCollectionConfirmationPaneTemplate from "./DeleteCollectionConfirmationPane.html"; import DeleteCollectionConfirmationPaneTemplate from "./DeleteCollectionConfirmationPane.html";
import DeleteDatabaseConfirmationPaneTemplate from "./DeleteDatabaseConfirmationPane.html"; import GitHubReposPaneTemplate from "./GitHubReposPane.html";
import GraphNewVertexPaneTemplate from "./GraphNewVertexPane.html"; import GraphNewVertexPaneTemplate from "./GraphNewVertexPane.html";
import GraphStylingPaneTemplate from "./GraphStylingPane.html"; import GraphStylingPaneTemplate from "./GraphStylingPane.html";
import TableAddEntityPaneTemplate from "./Tables/TableAddEntityPane.html";
import TableEditEntityPaneTemplate from "./Tables/TableEditEntityPane.html";
import TableColumnOptionsPaneTemplate from "./Tables/TableColumnOptionsPane.html";
import TableQuerySelectPaneTemplate from "./Tables/TableQuerySelectPane.html";
import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html";
import SettingsPaneTemplate from "./SettingsPane.html";
import ExecuteSprocParamsPaneTemplate from "./ExecuteSprocParamsPane.html";
import UploadItemsPaneTemplate from "./UploadItemsPane.html";
import LoadQueryPaneTemplate from "./LoadQueryPane.html"; import LoadQueryPaneTemplate from "./LoadQueryPane.html";
import SaveQueryPaneTemplate from "./SaveQueryPane.html"; import SaveQueryPaneTemplate from "./SaveQueryPane.html";
import BrowseQueriesPaneTemplate from "./BrowseQueriesPane.html";
import UploadFilePaneTemplate from "./UploadFilePane.html";
import StringInputPaneTemplate from "./StringInputPane.html";
import SetupNotebooksPaneTemplate from "./SetupNotebooksPane.html"; import SetupNotebooksPaneTemplate from "./SetupNotebooksPane.html";
import GitHubReposPaneTemplate from "./GitHubReposPane.html"; import StringInputPaneTemplate from "./StringInputPane.html";
import TableAddEntityPaneTemplate from "./Tables/TableAddEntityPane.html";
import TableColumnOptionsPaneTemplate from "./Tables/TableColumnOptionsPane.html";
import TableEditEntityPaneTemplate from "./Tables/TableEditEntityPane.html";
import TableQuerySelectPaneTemplate from "./Tables/TableQuerySelectPane.html";
export class PaneComponent { export class PaneComponent {
constructor(data: any) { constructor(data: any) {
@@ -53,15 +48,6 @@ export class DeleteCollectionConfirmationPaneComponent {
} }
} }
export class DeleteDatabaseConfirmationPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: DeleteDatabaseConfirmationPaneTemplate,
};
}
}
export class GraphNewVertexPaneComponent { export class GraphNewVertexPaneComponent {
constructor() { constructor() {
return { return {
@@ -125,33 +111,6 @@ export class CassandraAddCollectionPaneComponent {
} }
} }
export class SettingsPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: SettingsPaneTemplate,
};
}
}
export class ExecuteSprocParamsComponent {
constructor() {
return {
viewModel: PaneComponent,
template: ExecuteSprocParamsPaneTemplate,
};
}
}
export class UploadItemsPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: UploadItemsPaneTemplate,
};
}
}
export class LoadQueryPaneComponent { export class LoadQueryPaneComponent {
constructor() { constructor() {
return { return {
@@ -179,15 +138,6 @@ export class BrowseQueriesPaneComponent {
} }
} }
export class UploadFilePaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: UploadFilePaneTemplate,
};
}
}
export class StringInputPaneComponent { export class StringInputPaneComponent {
constructor() { constructor() {
return { return {

View File

@@ -1,12 +1,58 @@
@import "../../../less/Common/Constants"; @import "../../../less/Common/Constants";
.panelContentContainer { .panelFormWrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
.panelMainContent { .panelMainContent {
flex-grow: 1; flex-grow: 1;
padding: 0 34px;
margin: 20px 0;
overflow: auto;
& > * {
margin-bottom: @DefaultSpace;
& > * {
margin-bottom: @SmallSpace;
}
}
.panelInfoIcon {
font-size: @mediumFontSize;
width: @mediumFontSize;
margin: auto 0 auto @SmallSpace;
color: @InfoIconColor;
cursor: default;
vertical-align: middle;
}
.panelTextBold {
font-weight: 600;
line-height: 20px;
}
.panelTextField {
font-size: @mediumFontSize;
border: 1px solid #605e5c;
color: #000;
padding: 4px 10px;
width: @newCollectionPaneInputWidth;
}
.panelRadioBtn {
margin: 0;
}
.panelRadioBtnLabel {
font-size: @mediumFontSize;
padding: 0 @LargeSpace 0 @SmallSpace;
}
.collapsibleSection {
margin-bottom: 0;
}
} }
} }
@@ -16,26 +62,30 @@
font-weight: 400; font-weight: 400;
} }
.panelWarningErrorContainer { .panelInfoErrorContainer {
background-color: @BaseLow; background-color: @BaseLow;
padding: @DefaultSpace; padding: @DefaultSpace;
display: inline-flex; display: inline-flex;
margin-bottom: 24px; margin: 20px 34px 0 34px;
.panelWarningIcon { i {
font-size: @WarningErrorIconSize; font-size: @WarningErrorIconSize;
width: @WarningErrorIconSize; width: @WarningErrorIconSize;
margin: auto 0 auto @SmallSpace; margin-left: @SmallSpace;
}
.panelWarningIcon {
color: @WarningIconColor; color: @WarningIconColor;
} }
.panelErrorIcon { .panelErrorIcon {
font-size: @WarningErrorIconSize;
width: @WarningErrorIconSize;
margin: auto 0 auto @SmallSpace;
color: @ErrorIconColor; color: @ErrorIconColor;
} }
.panelLargeInfoIcon {
color: @InfoIconColor;
}
.panelWarningErrorDetailsLinkContainer { .panelWarningErrorDetailsLinkContainer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -48,10 +98,46 @@
} }
} }
.panelFooter button { .panelFooter {
height: 30px; padding: 20px 34px;
border-top: solid 1px #bbbbbb;
& button {
height: 30px;
}
} }
.deleteCollectionFeedback { .deleteCollectionFeedback {
margin-top: 12px; margin-top: 12px;
} }
.addRemoveIcon {
margin-left: 4px !important;
}
.addRemoveIconLabel {
margin-top: 28px;
margin-left: 4px !important;
}
.addNewParamStyle {
margin-top: 5px;
margin-left: 5px !important;
cursor: pointer;
}
.panelGroupSpacing > * {
margin-bottom: @SmallSpace;
}
.panelAddIconLabel {
font-size: 20px;
width: 20px;
margin: 30px 0 0 10px;
cursor: default;
}
.panelAddIcon {
font-size: 20px;
width: 20px;
margin: 30px 0 0 10px;
cursor: default;
}
.removeIcon {
color: @InfoIconColor;
}

View File

@@ -9,10 +9,30 @@ export interface PanelContainerProps {
closePanel: () => void; closePanel: () => void;
} }
export class PanelContainerComponent extends React.Component<PanelContainerProps> { export interface PanelContainerState {
height: string;
}
export class PanelContainerComponent extends React.Component<PanelContainerProps, PanelContainerState> {
private static readonly consoleHeaderHeight = 32; private static readonly consoleHeaderHeight = 32;
private static readonly consoleContentHeight = 220; private static readonly consoleContentHeight = 220;
constructor(props: PanelContainerProps) {
super(props);
this.state = {
height: this.getPanelHeight(),
};
}
componentDidMount(): void {
window.addEventListener("resize", () => this.setState({ height: this.getPanelHeight() }));
}
componentWillUnmount(): void {
window.removeEventListener("resize", () => this.setState({ height: this.getPanelHeight() }));
}
render(): JSX.Element { render(): JSX.Element {
if (!this.props.panelContent) { if (!this.props.panelContent) {
return <></>; return <></>;
@@ -30,8 +50,10 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
headerClassName="panelHeader" headerClassName="panelHeader"
styles={{ styles={{
navigation: { borderBottom: "1px solid #cccccc" }, navigation: { borderBottom: "1px solid #cccccc" },
content: { padding: "24px 34px 20px 34px", height: "100%" }, content: { padding: 0, height: "100%" },
scrollableContent: { height: "100%" }, scrollableContent: { height: "100%" },
header: { padding: "0 0 8px 34px" },
commands: { marginTop: 8 },
}} }}
style={{ height: this.getPanelHeight() }} style={{ height: this.getPanelHeight() }}
> >

View File

@@ -1,29 +0,0 @@
import React from "react";
import { Icon, Text } from "office-ui-fabric-react";
export interface PanelErrorProps {
message: string;
isWarning: boolean;
showErrorDetails: boolean;
openNotificationConsole?: () => void;
}
export const PanelErrorComponent: React.FunctionComponent<PanelErrorProps> = (props: PanelErrorProps): JSX.Element => (
<div className="panelWarningErrorContainer">
{props.isWarning ? (
<Icon iconName="WarningSolid" className="panelWarningIcon" />
) : (
<Icon iconName="StatusErrorFull" className="panelErrorIcon" />
)}
<span className="panelWarningErrorDetailsLinkContainer">
<Text className="panelWarningErrorMessage" variant="small">
{props.message}
</Text>
{props.showErrorDetails && (
<a className="paneErrorLink" role="link" onClick={props.openNotificationConsole}>
More details
</a>
)}
</span>
</div>
);

View File

@@ -3,13 +3,12 @@ import { PrimaryButton } from "office-ui-fabric-react";
export interface PanelFooterProps { export interface PanelFooterProps {
buttonLabel: string; buttonLabel: string;
onOKButtonClicked: () => void;
} }
export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = ( export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = (
props: PanelFooterProps props: PanelFooterProps
): JSX.Element => ( ): JSX.Element => (
<div className="panelFooter"> <div className="panelFooter">
<PrimaryButton id="sidePanelOkButton" text={props.buttonLabel} onClick={() => props.onOKButtonClicked()} /> <PrimaryButton type="submit" id="sidePanelOkButton" text={props.buttonLabel} />
</div> </div>
); );

View File

@@ -0,0 +1,45 @@
import React from "react";
import { Icon, Link, Stack, Text } from "office-ui-fabric-react";
export interface PanelInfoErrorProps {
message: string;
messageType: string;
showErrorDetails: boolean;
link?: string;
linkText?: string;
openNotificationConsole?: () => void;
}
export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProps> = (
props: PanelInfoErrorProps
): JSX.Element => {
let icon: JSX.Element;
if (props.messageType === "error") {
icon = <Icon iconName="StatusErrorFull" className="panelErrorIcon" />;
} else if (props.messageType === "warning") {
icon = <Icon iconName="WarningSolid" className="panelWarningIcon" />;
} else if (props.messageType === "info") {
icon = <Icon iconName="InfoSolid" className="panelLargeInfoIcon" />;
}
return (
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="start">
{icon}
<span className="panelWarningErrorDetailsLinkContainer">
<Text className="panelWarningErrorMessage" variant="small">
{props.message}{" "}
{props.link && props.linkText && (
<Link target="_blank" href={props.link}>
{props.linkText}
</Link>
)}
</Text>
{props.showErrorDetails && (
<a className="paneErrorLink" role="link" onClick={props.openNotificationConsole}>
More details
</a>
)}
</span>
</Stack>
);
};

View File

@@ -0,0 +1,8 @@
import React from "react";
import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares.gif";
export const PanelLoadingScreen: React.FunctionComponent = () => (
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer">
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} />
</div>
);

View File

@@ -14,7 +14,7 @@ import { handleError, getErrorMessage, getErrorStack } from "../../Common/ErrorH
import { GalleryTab } from "../Controls/NotebookGallery/GalleryViewerComponent"; import { GalleryTab } from "../Controls/NotebookGallery/GalleryViewerComponent";
import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor"; import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { FileSystemUtil } from "../Notebook/FileSystemUtil"; import * as FileSystemUtil from "../Notebook/FileSystemUtil";
export class PublishNotebookPaneAdapter implements ReactAdapter { export class PublishNotebookPaneAdapter implements ReactAdapter {
parameters: ko.Observable<number>; parameters: ko.Observable<number>;

View File

@@ -1,7 +1,7 @@
import { ITextFieldProps, Stack, Text, TextField, Dropdown, IDropdownProps } from "office-ui-fabric-react"; import { ITextFieldProps, Stack, Text, TextField, Dropdown, IDropdownProps } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { GalleryCardComponent } from "../Controls/NotebookGallery/Cards/GalleryCardComponent"; import { GalleryCardComponent } from "../Controls/NotebookGallery/Cards/GalleryCardComponent";
import { FileSystemUtil } from "../Notebook/FileSystemUtil"; import * as FileSystemUtil from "../Notebook/FileSystemUtil";
import "./PublishNotebookPaneComponent.less"; import "./PublishNotebookPaneComponent.less";
import Html2Canvas from "html2canvas"; import Html2Canvas from "html2canvas";
import { ImmutableNotebook } from "@nteract/commutable/src"; import { ImmutableNotebook } from "@nteract/commutable/src";

View File

@@ -1,268 +0,0 @@
<!-- TODO: Move Pane to REACT -->
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="settingspane">
<!-- Settings Confirmation form - Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Settings Confirmation header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keydown: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Settings Confirmation header - End -->
<!-- Settings Confirmation errors - Start -->
<div
class="warningErrorContainer"
aria-live="assertive"
data-bind="visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error" /></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
<a
class="errorLink"
role="link"
data-bind="
visible: formErrorsDetails() && formErrorsDetails() !== '',
click: showErrorDetails"
>More details</a
>
</span>
</div>
</div>
<!-- Settings Confirmation errors - End -->
<!-- Settings Confirmation inputs - Start -->
<div class="paneMainContent">
<div>
<div class="settingsSection" data-bind="visible: shouldShowQueryPageOptions">
<div class="settingsSectionPart pageOptionsPart">
<div class="settingsSectionLabel">
Page options
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext pageOptionTooltipWidth">
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as
many query results per page.
</span>
</span>
</div>
<div class="tabs" role="radiogroup" aria-label="Page options">
<!-- Fixed option button - Start -->
<div class="tab">
<input
type="radio"
id="customItemPerPage"
name="pageOption"
value="custom"
data-bind="checked: pageOption"
/>
<label
for="customItemPerPage"
id="custom-selection"
tabindex="0"
role="radio"
data-bind=" attr:{
'aria-checked': pageOption() === 'custom' ? 'true' : 'false' },
event: { keydown: onCustomPageOptionsKeyDown
}"
>Custom</label
>
</div>
<!-- Fixed option button - End -->
<!-- Unlimited option button - Start -->
<div class="tab">
<input
type="radio"
id="unlimitedItemPerPage"
name="pageOption"
value="unlimited"
data-bind="checked: pageOption"
/>
<label
for="unlimitedItemPerPage"
id="unlimited-selection"
tabindex="0"
role="radio"
data-bind=" attr:{
'aria-checked': pageOption() === 'unlimited' ? 'true' : 'false' },
event: { keydown: onUnlimitedPageOptionKeyDown
}"
>Unlimited</label
>
</div>
<!-- Unlimited option button - End -->
</div>
</div>
<div class="tabs settingsSectionPart">
<div class="tabcontent" data-bind="visible: isCustomPageOptionSelected()">
<div class="settingsSectionLabel">
Query results per page
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext pageOptionTooltipWidth">
Enter the number of query results that should be shown per page.
</span>
</span>
</div>
<input
type="number"
required
min="1"
step="1"
class="textfontclr collid"
aria-label="Custom query items per page"
data-bind="textInput: customItemPerPage, enable: isCustomPageOptionSelected()"
/>
</div>
</div>
</div>
<div class="settingsSection" data-bind="visible: shouldShowCrossPartitionOption">
<div class="settingsSectionPart">
<div class="settingsSectionLabel">
Enable cross-partition query
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext pageOptionTooltipWidth">
Send more than one request while executing a query. More than one request is necessary if the
query is not scoped to single partition key value.
</span>
</span>
</div>
<input
type="checkbox"
tabindex="0"
aria-label="Enable cross partition query"
data-bind="checked: crossPartitionQueryEnabled"
/>
</div>
</div>
<div class="settingsSection" data-bind="visible: shouldShowParallelismOption">
<div class="settingsSectionPart">
<div class="settingsSectionLabel">
Max degree of parallelism
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext pageOptionTooltipWidth">
Gets or sets the number of concurrent operations run client side during parallel query execution.
A positive property value limits the number of concurrent operations to the set value. If it is
set to less than 0, the system automatically decides the number of concurrent operations to run.
</span>
</span>
</div>
<input
type="number"
required
min="-1"
step="1"
class="textfontclr collid"
role="textbox"
tabindex="0"
id="max-degree"
aria-label="Max degree of parallelism"
data-bind="value: maxDegreeOfParallelism"
autofocus
/>
</div>
</div>
<div class="settingsSection" data-bind="visible: shouldShowGraphAutoVizOption">
<div class="settingsSectionPart">
<div class="settingsSectionLabel">
Display Gremlin query results as:&nbsp;
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext pageOptionTooltipWidth">
Select Graph to automatically visualize the query results as a Graph or JSON to display the
results as JSON.
</span>
</span>
</div>
<div class="tabs" role="radiogroup" aria-label="Graph Auto-visualization">
<!-- Fixed option button - Start -->
<div class="tab">
<input
type="radio"
id="graphAutoVizOn"
name="graphAutoVizOption"
value="false"
data-bind="checked: graphAutoVizDisabled"
/>
<label
for="graphAutoVizOn"
id="graph-display"
tabindex="0"
role="radio"
data-bind="
attr: { 'aria-checked': graphAutoVizDisabled() === 'false' ? 'true' : 'false' },
event: { keypress: onGraphDisplayResultsKeyDown
}"
>Graph</label
>
</div>
<!-- Fixed option button - End -->
<!-- Unlimited option button - Start -->
<div class="tab">
<input
type="radio"
id="graphAutoVizOff"
name="graphAutoVizOption"
value="true"
data-bind="checked: graphAutoVizDisabled"
/>
<label
for="graphAutoVizOff"
tabindex="0"
role="radio"
data-bind="
attr: { 'aria-checked': graphAutoVizDisabled() === 'true' ? 'true' : 'false' },
event: { keypress: onJsonDisplayResultsKeyDown
}"
>JSON</label
>
</div>
<!-- Unlimited option button - End -->
</div>
</div>
</div>
<div class="settingsSection">
<div class="settingsSectionPart">
<div class="settingsSectionLabel">Explorer Version</div>
<div data-bind="text: explorerVersion"></div>
</div>
</div>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="Apply" class="btncreatecoll1" /></div>
</div>
<!-- Settings Confirmation inputs - End -->
</form>
</div>
<!-- Settings Confirmation form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -1,38 +0,0 @@
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
describe("Settings Pane", () => {
describe("shouldShowQueryPageOptions()", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
});
it("should be true for SQL API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB.toLowerCase());
expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(true);
});
it("should be false for Cassandra API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.Cassandra.toLowerCase());
expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false);
});
it("should be false for Tables API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.Table.toLowerCase());
expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false);
});
it("should be false for Graph API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.Graph.toLowerCase());
expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false);
});
it("should be false for Mongo API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB.toLowerCase());
expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false);
});
});
});

View File

@@ -1,185 +0,0 @@
import * as Constants from "../../Common/Constants";
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as StringUtility from "../../Shared/StringUtility";
import { configContext } from "../../ConfigContext";
export class SettingsPane extends ContextualPaneBase {
public pageOption: ko.Observable<string>;
public customItemPerPage: ko.Observable<number>;
public crossPartitionQueryEnabled: ko.Observable<boolean>;
public maxDegreeOfParallelism: ko.Observable<number>;
public explorerVersion: string;
public shouldShowQueryPageOptions: ko.Computed<boolean>;
public shouldShowGraphAutoVizOption: ko.Computed<boolean>;
private graphAutoVizDisabled: ko.Observable<string>;
private shouldShowCrossPartitionOption: ko.Computed<boolean>;
private shouldShowParallelismOption: ko.Computed<boolean>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Settings");
this.resetData();
this.pageOption = ko.observable<string>();
this.customItemPerPage = ko.observable<number>();
const crossPartitionQueryEnabledState: boolean = LocalStorageUtility.hasItem(
StorageKey.IsCrossPartitionQueryEnabled
)
? LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
: true;
this.crossPartitionQueryEnabled = ko.observable<boolean>(crossPartitionQueryEnabledState);
const maxDegreeOfParallelismState: number = LocalStorageUtility.hasItem(StorageKey.MaxDegreeOfParellism)
? LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism)
: Constants.Queries.DefaultMaxDegreeOfParallelism;
this.maxDegreeOfParallelism = ko.observable<number>(maxDegreeOfParallelismState);
const isGraphAutoVizDisabled: boolean = LocalStorageUtility.hasItem(StorageKey.IsGraphAutoVizDisabled)
? LocalStorageUtility.getEntryBoolean(StorageKey.IsGraphAutoVizDisabled)
: false;
this.graphAutoVizDisabled = ko.observable<string>(`${isGraphAutoVizDisabled}`);
this.explorerVersion = configContext.gitSha;
this.shouldShowQueryPageOptions = ko.computed<boolean>(() => this.container.isPreferredApiDocumentDB());
this.shouldShowCrossPartitionOption = ko.computed<boolean>(() => !this.container.isPreferredApiGraph());
this.shouldShowParallelismOption = ko.computed<boolean>(() => !this.container.isPreferredApiGraph());
this.shouldShowGraphAutoVizOption = ko.computed<boolean>(() => this.container.isPreferredApiGraph());
}
public open() {
this._loadSettings();
super.open();
const pageOptionsFocus = document.getElementById("custom-selection");
const displayQueryFocus = document.getElementById("graph-display");
const maxDegreeFocus = document.getElementById("max-degree");
if (this.container.isPreferredApiGraph()) {
displayQueryFocus && displayQueryFocus.focus();
} else if (this.container.isPreferredApiTable()) {
maxDegreeFocus && maxDegreeFocus.focus();
}
pageOptionsFocus && pageOptionsFocus.focus();
}
public submit() {
this.formErrors("");
this.isExecuting(true);
LocalStorageUtility.setEntryNumber(
StorageKey.ActualItemPerPage,
this.isCustomPageOptionSelected() ? this.customItemPerPage() : Constants.Queries.unlimitedItemsPerPage
);
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, this.customItemPerPage());
LocalStorageUtility.setEntryString(
StorageKey.IsCrossPartitionQueryEnabled,
this.crossPartitionQueryEnabled().toString()
);
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, this.maxDegreeOfParallelism());
if (this.shouldShowGraphAutoVizOption()) {
LocalStorageUtility.setEntryBoolean(
StorageKey.IsGraphAutoVizDisabled,
StringUtility.toBoolean(this.graphAutoVizDisabled())
);
}
this.isExecuting(false);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`
);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`${this.crossPartitionQueryEnabled() ? "Enabled" : "Disabled"} cross-partition query feed option`
);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Updated the max degree of parallelism query feed option to ${LocalStorageUtility.getEntryNumber(
StorageKey.MaxDegreeOfParellism
)}`
);
if (this.shouldShowGraphAutoVizOption()) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Graph result will be displayed as ${
LocalStorageUtility.getEntryBoolean(StorageKey.IsGraphAutoVizDisabled) ? "JSON" : "Graph"
}`
);
}
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`
);
this.close();
}
public isCustomPageOptionSelected = (): boolean => {
return this.pageOption() === Constants.Queries.CustomPageOption;
};
public isUnlimitedPageOptionSelected = (): boolean => {
return this.pageOption() === Constants.Queries.UnlimitedPageOption;
};
public onUnlimitedPageOptionKeyDown(source: any, event: KeyboardEvent): boolean {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.pageOption(Constants.Queries.UnlimitedPageOption);
event.stopPropagation();
return false;
}
return true;
}
public onCustomPageOptionsKeyDown(source: any, event: KeyboardEvent): boolean {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.pageOption(Constants.Queries.CustomPageOption);
event.stopPropagation();
return false;
}
return true;
}
public onJsonDisplayResultsKeyDown(source: any, event: KeyboardEvent): boolean {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.graphAutoVizDisabled("true");
event.stopPropagation();
return false;
}
return true;
}
public onGraphDisplayResultsKeyDown(source: any, event: KeyboardEvent): boolean {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.graphAutoVizDisabled("false");
event.stopPropagation();
return false;
}
return true;
}
private _loadSettings() {
this.isExecuting(true);
try {
this.pageOption(
LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage) == Constants.Queries.unlimitedItemsPerPage
? Constants.Queries.UnlimitedPageOption
: Constants.Queries.CustomPageOption
);
this.customItemPerPage(LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage));
} catch (exception) {
this.formErrors("Unable to load your settings");
this.formErrorsDetails(exception);
} finally {
this.isExecuting(false);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
import { shallow } from "enzyme";
import React from "react";
import { SettingsPane } from ".";
import { DatabaseAccount } from "../../../Contracts/DataModels";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
const props = {
explorer: new Explorer(),
closePanel: (): void => undefined,
};
describe("Settings Pane", () => {
it("should render Default properly", () => {
const wrapper = shallow(<SettingsPane {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("should render Gremlin properly", () => {
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const wrapper = shallow(<SettingsPane {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,253 @@
import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "office-ui-fabric-react";
import React, { FunctionComponent, MouseEvent, useState } from "react";
import * as Constants from "../../../Common/Constants";
import { Tooltip } from "../../../Common/Tooltip";
import { configContext } from "../../../ConfigContext";
import { LocalStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
import * as StringUtility from "../../../Shared/StringUtility";
import { userContext } from "../../../UserContext";
import { logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
export interface SettingsPaneProps {
explorer: Explorer;
closePanel: () => void;
}
export const SettingsPane: FunctionComponent<SettingsPaneProps> = ({
explorer: container,
closePanel,
}: SettingsPaneProps) => {
const [formErrors, setFormErrors] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const [pageOption, setPageOption] = useState<string>(
LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage) === Constants.Queries.unlimitedItemsPerPage
? Constants.Queries.UnlimitedPageOption
: Constants.Queries.CustomPageOption
);
const [customItemPerPage, setCustomItemPerPage] = useState<number>(
LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage) || 0
);
const [crossPartitionQueryEnabled, setCrossPartitionQueryEnabled] = useState<boolean>(
LocalStorageUtility.hasItem(StorageKey.IsCrossPartitionQueryEnabled)
? LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
: true
);
const [graphAutoVizDisabled, setGraphAutoVizDisabled] = useState<string>(
LocalStorageUtility.hasItem(StorageKey.IsGraphAutoVizDisabled)
? LocalStorageUtility.getEntryString(StorageKey.IsGraphAutoVizDisabled)
: "false"
);
const [maxDegreeOfParallelism, setMaxDegreeOfParallelism] = useState<number>(
LocalStorageUtility.hasItem(StorageKey.MaxDegreeOfParellism)
? LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism)
: Constants.Queries.DefaultMaxDegreeOfParallelism
);
const explorerVersion = configContext.gitSha;
const shouldShowQueryPageOptions = userContext.apiType === "SQL";
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
const handlerOnSubmit = (e: MouseEvent<HTMLButtonElement>) => {
setFormErrors("");
setIsExecuting(true);
LocalStorageUtility.setEntryNumber(
StorageKey.ActualItemPerPage,
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage
);
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
if (shouldShowGraphAutoVizOption) {
LocalStorageUtility.setEntryBoolean(
StorageKey.IsGraphAutoVizDisabled,
StringUtility.toBoolean(graphAutoVizDisabled)
);
}
setIsExecuting(false);
logConsoleInfo(
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`
);
logConsoleInfo(`${crossPartitionQueryEnabled ? "Enabled" : "Disabled"} cross-partition query feed option`);
logConsoleInfo(
`Updated the max degree of parallelism query feed option to ${LocalStorageUtility.getEntryNumber(
StorageKey.MaxDegreeOfParellism
)}`
);
if (shouldShowGraphAutoVizOption) {
logConsoleInfo(
`Graph result will be displayed as ${
LocalStorageUtility.getEntryBoolean(StorageKey.IsGraphAutoVizDisabled) ? "JSON" : "Graph"
}`
);
}
logConsoleInfo(
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`
);
closePanel();
e.preventDefault();
};
const isCustomPageOptionSelected = () => {
return pageOption === Constants.Queries.CustomPageOption;
};
const handleOnGremlinChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
setGraphAutoVizDisabled(option.key);
};
const genericPaneProps: GenericRightPaneProps = {
container,
formError: formErrors,
formErrorDetail: "",
id: "settingspane",
isExecuting,
title: "Setting",
submitButtonText: "Apply",
onClose: () => closePanel(),
onSubmit: () => handlerOnSubmit(undefined),
};
const pageOptionList: IChoiceGroupOption[] = [
{ key: Constants.Queries.CustomPageOption, text: "Custom" },
{ key: Constants.Queries.UnlimitedPageOption, text: "Unlimited" },
];
const graphAutoOptionList: IChoiceGroupOption[] = [
{ key: "false", text: "Graph" },
{ key: "true", text: "JSON" },
];
const handleOnPageOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
setPageOption(option.key);
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<div className="paneMainContent">
{shouldShowQueryPageOptions && (
<div className="settingsSection">
<div className="settingsSectionPart pageOptionsPart">
<div className="settingsSectionLabel">
Page options
<Tooltip>
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many
query results per page.
</Tooltip>
</div>
<ChoiceGroup selectedKey={pageOption} options={pageOptionList} onChange={handleOnPageOptionChange} />
</div>
<div className="tabs settingsSectionPart">
{isCustomPageOptionSelected() && (
<div className="tabcontent">
<div className="settingsSectionLabel">
Query results per page
<Tooltip>Enter the number of query results that should be shown per page.</Tooltip>
</div>
<SpinButton
ariaLabel="Custom query items per page"
value={"" + customItemPerPage}
onIncrement={(newValue) => {
setCustomItemPerPage(parseInt(newValue) + 1 || customItemPerPage);
}}
onDecrement={(newValue) => setCustomItemPerPage(parseInt(newValue) - 1 || customItemPerPage)}
onValidate={(newValue) => setCustomItemPerPage(parseInt(newValue) || customItemPerPage)}
min={1}
step={1}
className="textfontclr"
incrementButtonAriaLabel="Increase value by 1"
decrementButtonAriaLabel="Decrease value by 1"
/>
</div>
)}
</div>
</div>
)}
{shouldShowCrossPartitionOption && (
<div className="settingsSection">
<div className="settingsSectionPart">
<div className="settingsSectionLabel">
Enable cross-partition query
<Tooltip>
Send more than one request while executing a query. More than one request is necessary if the query is
not scoped to single partition key value.
</Tooltip>
</div>
<Checkbox
styles={{
label: { padding: 0 },
}}
className="padding"
tabIndex={0}
ariaLabel="Enable cross partition query"
checked={crossPartitionQueryEnabled}
onChange={() => setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)}
/>
</div>
</div>
)}
{shouldShowParallelismOption && (
<div className="settingsSection">
<div className="settingsSectionPart">
<div className="settingsSectionLabel">
Max degree of parallelism
<Tooltip>
Gets or sets the number of concurrent operations run client side during parallel query execution. A
positive property value limits the number of concurrent operations to the set value. If it is set to
less than 0, the system automatically decides the number of concurrent operations to run.
</Tooltip>
</div>
<SpinButton
min={-1}
step={1}
className="textfontclr"
role="textbox"
tabIndex={0}
id="max-degree"
value={"" + maxDegreeOfParallelism}
onIncrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)}
onDecrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism)}
onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)}
ariaLabel="Max degree of parallelism"
/>
</div>
</div>
)}
{shouldShowGraphAutoVizOption && (
<div className="settingsSection">
<div className="settingsSectionPart">
<div className="settingsSectionLabel">
Display Gremlin query results as:&nbsp;
<Tooltip>
Select Graph to automatically visualize the query results as a Graph or JSON to display the results as
JSON.
</Tooltip>
</div>
<ChoiceGroup
selectedKey={graphAutoVizDisabled}
options={graphAutoOptionList}
onChange={handleOnGremlinChange}
aria-label="Graph Auto-visualization"
/>
</div>
</div>
)}
<div className="settingsSection">
<div className="settingsSectionPart">
<div className="settingsSectionLabel">Explorer Version</div>
<div>{explorerVersion}</div>
</div>
</div>
</div>
</GenericRightPaneComponent>
);
};

View File

@@ -1,83 +0,0 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" id="uploadFilePane">
<!-- Upload File form -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Upload File header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2" data-bind="text: title"></span>
<div class="closeImg" role="button" aria-label="Close pane" tabindex="0" data-bind="click: cancel">
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Upload File header - End -->
<!-- Upload File errors - Start -->
<div
class="warningErrorContainer"
aria-live="assertive"
data-bind="visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error" /></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
<a
class="errorLink"
role="link"
data-bind="visible: formErrorsDetails() && formErrorsDetails() !== '', click: showErrorDetails"
>More details</a
>
</span>
</div>
</div>
<!-- Upload File errors - End -->
<!-- Upload File inputs - Start -->
<div class="paneMainContent">
<div>
<div class="renewUploadItemsHeader" data-bind="text: selectFileInputLabel"></div>
<input class="importFilesTitle" type="text" disabled data-bind="value: selectedFilesTitle" />
<input
type="file"
id="importFileInput"
style="display: none"
data-bind="event: { change: updateSelectedFiles }, attr: { accept: extensions }"
/>
<a
href="#"
id="fileImportLinkNotebook"
data-bind="event: { click: onImportLinkClick, keypress: onImportLinkKeyPress }"
>
<img
id="importFileButton"
class="fileImportImg"
src="/folder_16x16.svg"
alt="upload files"
title="Upload files"
/>
</a>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input
id="uploadFileButton"
type="submit"
data-bind="attr: { value: submitButtonLabel }"
class="btncreatecoll1"
/>
</div>
</div>
<!-- Upload File inputs - End -->
</form>
</div>
<!-- Upload File form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -1,137 +0,0 @@
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
export interface UploadFilePaneOpenOptions {
paneTitle: string;
selectFileInputLabel: string;
errorMessage: string; // Could not upload notebook
inProgressMessage: string; // Uploading notebook
successMessage: string; // Successfully uploaded notebook
onSubmit: (file: File) => Promise<any>;
extensions?: string; // input accept field. E.g: .ipynb
submitButtonLabel?: string;
}
export class UploadFilePane extends ContextualPaneBase {
public selectedFilesTitle: ko.Observable<string>;
public files: ko.Observable<FileList>;
private openOptions: UploadFilePaneOpenOptions;
private submitButtonLabel: ko.Observable<string>;
private selectFileInputLabel: ko.Observable<string>;
private extensions: ko.Observable<string>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.resetData();
this.selectFileInputLabel = ko.observable("");
this.selectedFilesTitle = ko.observable<string>("");
this.extensions = ko.observable(null);
this.submitButtonLabel = ko.observable("Load");
this.files = ko.observable<FileList>();
this.files.subscribe((newFiles: FileList) => this.updateSelectedFilesTitle(newFiles));
}
public submit() {
this.formErrors("");
this.formErrorsDetails("");
if (!this.files() || this.files().length === 0) {
this.formErrors("No file specified");
this.formErrorsDetails("No file specified. Please input a file.");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`${this.openOptions.errorMessage} -- No file specified. Please input a file.`
);
return;
}
const file: File = this.files().item(0);
const id: string = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`${this.openOptions.inProgressMessage}: ${file.name}`
);
this.isExecuting(true);
this.openOptions
.onSubmit(this.files().item(0))
.then(
() => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`${this.openOptions.successMessage} ${file.name}`
);
this.close();
},
(error: any) => {
this.formErrors(this.openOptions.errorMessage);
this.formErrorsDetails(`${this.openOptions.errorMessage}: ${error}`);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`${this.openOptions.errorMessage} ${file.name}: ${error}`
);
}
)
.finally(() => {
this.isExecuting(false);
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
}
public updateSelectedFiles(element: any, event: any): void {
this.files(event.target.files);
}
public close() {
super.close();
this.resetData();
this.files(undefined);
this.resetFileInput();
}
public openWithOptions(options: UploadFilePaneOpenOptions): void {
this.openOptions = options;
this.title(this.openOptions.paneTitle);
if (this.openOptions.submitButtonLabel) {
this.submitButtonLabel(this.openOptions.submitButtonLabel);
}
this.selectFileInputLabel(this.openOptions.selectFileInputLabel);
if (this.openOptions.extensions) {
this.extensions(this.openOptions.extensions);
}
super.open();
}
public onImportLinkClick(source: any, event: MouseEvent): boolean {
document.getElementById("importFileInput").click();
return false;
}
public onImportLinkKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.onImportLinkClick(source, null);
return false;
}
return true;
};
private updateSelectedFilesTitle(fileList: FileList) {
this.selectedFilesTitle("");
if (!fileList || fileList.length === 0) {
return;
}
for (let i = 0; i < fileList.length; i++) {
const originalTitle = this.selectedFilesTitle();
this.selectedFilesTitle(originalTitle + `"${fileList.item(i).name}"`);
}
}
private resetFileInput(): void {
const inputElement = $("#importFileInput");
inputElement.wrap("<form>").closest("form").get(0).reset();
inputElement.unwrap();
}
}

View File

@@ -0,0 +1,111 @@
import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { Upload } from "../../../Common/Upload";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
export interface UploadFilePanelProps {
explorer: Explorer;
closePanel: () => void;
uploadFile: (name: string, content: string) => Promise<NotebookContentItem>;
}
export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({
explorer: container,
closePanel,
uploadFile,
}: UploadFilePanelProps) => {
const title = "Upload file to notebook server";
const submitButtonLabel = "Upload";
const selectFileInputLabel = "Select file to upload";
const extensions: string = undefined; //ex. ".ipynb"
const errorMessage = "Could not upload file";
const inProgressMessage = "Uploading file to notebook server";
const successMessage = "Successfully uploaded file to notebook server";
const [files, setFiles] = useState<FileList>();
const [formErrors, setFormErrors] = useState<string>("");
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const submit = () => {
setFormErrors("");
setFormErrorsDetails("");
if (!files || files.length === 0) {
setFormErrors("No file specified");
setFormErrorsDetails("No file specified. Please input a file.");
logConsoleError(`${errorMessage} -- No file specified. Please input a file.`);
return;
}
const file: File = files.item(0);
// const id: string = logConsoleProgress(
// `${inProgressMessage}: ${file.name}`
// );
logConsoleProgress(`${inProgressMessage}: ${file.name}`);
setIsExecuting(true);
onSubmit(files.item(0))
.then(
() => {
logConsoleInfo(`${successMessage} ${file.name}`);
closePanel();
},
(error: string) => {
setFormErrors(errorMessage);
setFormErrorsDetails(`${errorMessage}: ${error}`);
logConsoleError(`${errorMessage} ${file.name}: ${error}`);
}
)
.finally(() => {
setIsExecuting(false);
// clearInProgressMessageWithId(id);
});
};
const updateSelectedFiles = (event: ChangeEvent<HTMLInputElement>): void => {
setFiles(event.target.files);
};
const onSubmit = async (file: File): Promise<NotebookContentItem> => {
const readFileAsText = (inputFile: File): Promise<string> => {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onerror = () => {
reader.abort();
reject(`Problem parsing file: ${inputFile}`);
};
reader.onload = () => {
resolve(reader.result as string);
};
reader.readAsText(inputFile);
});
};
const fileContent = await readFileAsText(file);
return uploadFile(file.name, fileContent);
};
const genericPaneProps: GenericRightPaneProps = {
container: container,
formError: formErrors,
formErrorDetail: formErrorsDetails,
id: "uploadFilePane",
isExecuting: isExecuting,
title,
submitButtonText: submitButtonLabel,
onClose: closePanel,
onSubmit: submit,
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<div className="paneMainContent">
<Upload label={selectFileInputLabel} accept={extensions} onUpload={updateSelectedFiles} />
</div>
</GenericRightPaneComponent>
);
};

View File

@@ -1,130 +0,0 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="uploaditemspane">
<!-- Upload items form -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Upload items header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keydown: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Upload items header - End -->
<!-- Upload items errors - Start -->
<div
class="warningErrorContainer"
aria-live="assertive"
data-bind="visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error" /></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
<a
class="errorLink"
role="link"
data-bind="
visible: formErrorsDetails() && formErrorsDetails() !== '',
click: showErrorDetails"
>More details</a
>
</span>
</div>
</div>
<!-- Upload items errors - End -->
<!-- Upload item inputs - Start -->
<div class="paneMainContent">
<div>
<div class="renewUploadItemsHeader">
<span> Select JSON Files </span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext infoTooltipWidth"
>Select one or more JSON files to upload. Each file can contain a single JSON document or an array of
JSON documents. The combined size of all files in an individual upload operation must be less than 2
MB. You can perform multiple upload operations for larger data sets.</span
>
</span>
</div>
<input
class="importFilesTitle"
type="text"
disabled
data-bind="value: selectedFilesTitle"
aria-label="Select JSON Files"
/>
<input
type="file"
id="importDocsInput"
title="Upload Icon"
multiple
accept="application/json"
role="button"
tabindex="0"
style="display: none"
data-bind="event: { change: updateSelectedFiles }"
/>
<a
href="#"
id="fileImportLink"
data-bind="event: { click: onImportLinkClick, keypress: onImportLinkKeyPress }"
autofocus
>
<img
class="fileImportImg"
src="/folder_16x16.svg"
alt="Select JSON files to upload"
title="Select JSON files to upload"
/>
</a>
</div>
<div class="fileUploadSummaryContainer" data-bind="visible: uploadFileDataVisible">
<b>File upload status</b>
<table class="fileUploadSummary">
<thead>
<tr class="fileUploadSummaryHeader fileUploadSummaryTuple">
<th>FILE NAME</th>
<th>STATUS</th>
</tr>
</thead>
<tbody>
<!-- ko foreach: uploadFileData -->
<tr class="fileUploadSummaryTuple">
<td data-bind="text: $data.fileName"></td>
<td data-bind="text: $parent.fileUploadSummaryText($data.numSucceeded, $data.numFailed)"></td>
</tr>
<!-- /ko -->
</tbody>
</table>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="Upload" class="btncreatecoll1" /></div>
</div>
<!-- Upload item inputs - End -->
</form>
</div>
<!-- Upload items form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -1,143 +0,0 @@
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { UploadDetailsRecord, UploadDetails } from "../../workers/upload/definitions";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
const UPLOAD_FILE_SIZE_LIMIT = 2097152;
export class UploadItemsPane extends ContextualPaneBase {
public selectedFilesTitle: ko.Observable<string>;
public files: ko.Observable<FileList>;
public uploadFileDataVisible: ko.Computed<boolean>;
public uploadFileData: ko.ObservableArray<UploadDetailsRecord>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this._initTitle();
this.resetData();
this.selectedFilesTitle = ko.observable<string>("");
this.uploadFileData = ko.observableArray<UploadDetailsRecord>();
this.uploadFileDataVisible = ko.computed<boolean>(
() => !!this.uploadFileData() && this.uploadFileData().length > 0
);
this.files = ko.observable<FileList>();
this.files.subscribe((newFiles: FileList) => this._updateSelectedFilesTitle(newFiles));
}
public submit() {
this.formErrors("");
if (!this.files() || this.files().length === 0) {
this.formErrors("No files specified");
this.formErrorsDetails("No files were specified. Please input at least one file.");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
"Could not upload items -- No files were specified. Please input at least one file."
);
return;
} else if (this._totalFileSizeForFileList(this.files()) > UPLOAD_FILE_SIZE_LIMIT) {
this.formErrors("Upload file size limit exceeded");
this.formErrorsDetails("Total file upload size exceeds the 2 MB file size limit.");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
"Could not upload items -- Total file upload size exceeds the 2 MB file size limit."
);
return;
}
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
this.isExecuting(true);
selectedCollection &&
selectedCollection
.uploadFiles(this.files())
.then(
(uploadDetails: UploadDetails) => {
this.uploadFileData(uploadDetails.data);
this.files(undefined);
this._resetFileInput();
},
(error: any) => {
const errorMessage = getErrorMessage(error);
this.formErrors(errorMessage);
this.formErrorsDetails(errorMessage);
}
)
.finally(() => {
this.isExecuting(false);
});
}
public updateSelectedFiles(element: any, event: any): void {
this.files(event.target.files);
}
public close() {
super.close();
this.resetData();
this.files(undefined);
this.uploadFileData([]);
this._resetFileInput();
}
public onImportLinkClick(source: any, event: MouseEvent): boolean {
document.getElementById("importDocsInput").click();
return false;
}
public onImportLinkKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.onImportLinkClick(source, null);
return false;
}
return true;
};
public fileUploadSummaryText = (numSucceeded: number, numFailed: number): string => {
return `${numSucceeded} items created, ${numFailed} errors`;
};
private _totalFileSizeForFileList(fileList: FileList): number {
let totalFileSize: number = 0;
if (!fileList) {
return totalFileSize;
}
for (let i = 0; i < fileList.length; i++) {
totalFileSize = totalFileSize + fileList.item(i).size;
}
return totalFileSize;
}
private _updateSelectedFilesTitle(fileList: FileList) {
this.selectedFilesTitle("");
if (!fileList || fileList.length === 0) {
return;
}
for (let i = 0; i < fileList.length; i++) {
const originalTitle = this.selectedFilesTitle();
this.selectedFilesTitle(originalTitle + `"${fileList.item(i).name}"`);
}
}
private _initTitle(): void {
if (this.container.isPreferredApiCassandra() || this.container.isPreferredApiTable()) {
this.title("Upload Tables");
} else if (this.container.isPreferredApiGraph()) {
this.title("Upload Graph");
} else {
this.title("Upload Items");
}
}
private _resetFileInput(): void {
const inputElement = $("#importDocsInput");
inputElement.wrap("<form>").closest("form").get(0).reset();
inputElement.unwrap();
}
}

View File

@@ -0,0 +1,995 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Upload Items Pane should render Default properly 1`] = `
<GenericRightPaneComponent
container={
Explorer {
"_closeModalDialog": [Function],
"_closeSynapseLinkModalDialog": [Function],
"_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false,
"_panes": Array [
AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
AddCollectionPane {
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
"collectionWithThroughputInSharedTitle": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNew": [Function],
"databaseCreateNewShared": [Function],
"databaseHasSharedOffer": [Function],
"databaseId": [Function],
"databaseIds": [Function],
"dedicatedRequestUnitsUsageCost": [Function],
"displayCollectionThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFixedStorageSelected": [Function],
"isFreeTierAccount": [Function],
"isNonTableApi": [Function],
"isPreferredApiTable": [Function],
"isSharedAutoPilotSelected": [Function],
"isSynapseLinkSupported": [Function],
"isSynapseLinkUpdating": [Function],
"isTemplateReady": [Function],
"isTryCosmosDBSubscription": [Function],
"isUnlimitedStorageSelected": [Function],
"largePartitionKey": [Function],
"lowerCasePartitionKeyName": [Function],
"maxCollectionsReached": [Function],
"maxCollectionsReachedMessage": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"partitionKey": [Function],
"partitionKeyName": [Function],
"partitionKeyPattern": [Function],
"partitionKeyPlaceholder": [Function],
"partitionKeyTitle": [Function],
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function],
"showAnalyticalStore": [Function],
"showEnableSynapseLink": [Function],
"showIndexingOptionsForSharedThroughput": [Function],
"showUpsellMessage": [Function],
"storage": [Function],
"throughputDatabase": [Function],
"throughputMultiPartition": [Function],
"throughputRangeText": [Function],
"throughputSinglePartition": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"ttl90DaysEnabled": [Function],
"uniqueKeys": [Function],
"uniqueKeysPlaceholder": [Function],
"uniqueKeysVisible": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
AddTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"enterRequiredValueLabel": "Enter identifier value.",
"enterValueLabel": "Enter value to keep property.",
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "addtableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane {
"buildString": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "newvertexpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onMoreDetailsKeyPress": [Function],
"onSubmitCreateCallback": null,
"partitionKeyProperty": [Function],
"tempVertexData": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
LoadQueryPane {
"container": [Circular],
"files": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "loadquerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onImportLinkKeyPress": [Function],
"selectedFilesTitle": [Function],
"title": [Function],
"visible": [Function],
},
SaveQueryPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "savequerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"queryName": [Function],
"setupQueries": [Function],
"setupSaveQueriesText": "For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called “___Cosmos”. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.",
"submit": [Function],
"title": [Function],
"visible": [Function],
},
BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "browsequeriespane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"loadSavedQuery": [Function],
"queriesGridComponentAdapter": QueriesGridComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"setupQueries": [Function],
"title": [Function],
"visible": [Function],
},
StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
SetupNotebooksPane {
"container": [Circular],
"description": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "setupnotebookspane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onCompleteSetupClick": [Function],
"onCompleteSetupKeyPress": [Function],
"title": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
"addCollectionPane": AddCollectionPane {
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
"collectionWithThroughputInSharedTitle": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNew": [Function],
"databaseCreateNewShared": [Function],
"databaseHasSharedOffer": [Function],
"databaseId": [Function],
"databaseIds": [Function],
"dedicatedRequestUnitsUsageCost": [Function],
"displayCollectionThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"formWarnings": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "addcollectionpane",
"isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFixedStorageSelected": [Function],
"isFreeTierAccount": [Function],
"isNonTableApi": [Function],
"isPreferredApiTable": [Function],
"isSharedAutoPilotSelected": [Function],
"isSynapseLinkSupported": [Function],
"isSynapseLinkUpdating": [Function],
"isTemplateReady": [Function],
"isTryCosmosDBSubscription": [Function],
"isUnlimitedStorageSelected": [Function],
"largePartitionKey": [Function],
"lowerCasePartitionKeyName": [Function],
"maxCollectionsReached": [Function],
"maxCollectionsReachedMessage": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"partitionKey": [Function],
"partitionKeyName": [Function],
"partitionKeyPattern": [Function],
"partitionKeyPlaceholder": [Function],
"partitionKeyTitle": [Function],
"partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function],
"showAnalyticalStore": [Function],
"showEnableSynapseLink": [Function],
"showIndexingOptionsForSharedThroughput": [Function],
"showUpsellMessage": [Function],
"storage": [Function],
"throughputDatabase": [Function],
"throughputMultiPartition": [Function],
"throughputRangeText": [Function],
"throughputSinglePartition": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"ttl90DaysEnabled": [Function],
"uniqueKeys": [Function],
"uniqueKeysPlaceholder": [Function],
"uniqueKeysVisible": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"useIndexingForSharedThroughput": [Function],
"visible": [Function],
},
"addCollectionText": [Function],
"addDatabasePane": AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
"addDatabaseText": [Function],
"addTableEntityPane": AddTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"enterRequiredValueLabel": "Enter identifier value.",
"enterValueLabel": "Enter value to keep property.",
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "addtableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
"arcadiaToken": [Function],
"browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "browsequeriespane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"loadSavedQuery": [Function],
"queriesGridComponentAdapter": QueriesGridComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"setupQueries": [Function],
"title": [Function],
"visible": [Function],
},
"canExceedMaximumValue": [Function],
"canSaveQueries": [Function],
"cassandraAddCollectionPane": CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
"clickHostedAccountSwitch": [Function],
"clickHostedDirectorySwitch": [Function],
"closeDialog": undefined,
"closeSidePanel": undefined,
"collapsedResourceTreeWidth": 36,
"collectionCreationDefaults": Object {
"storage": "100",
"throughput": Object {
"fixed": 400,
"shared": 400,
"unlimited": 400,
"unlimitedmax": 1000000,
"unlimitedmin": 400,
},
},
"collectionTitle": [Function],
"collectionTreeNodeAltText": [Function],
"commandBarComponentAdapter": CommandBarComponentAdapter {
"container": [Circular],
"isNotebookTabActive": [Function],
"parameters": [Function],
"tabsButtons": Array [],
},
"databaseAccount": [Function],
"databases": [Function],
"defaultExperience": [Function],
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
"deleteCollectionText": [Function],
"deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
"flight": [Function],
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
"hasStorageAnalyticsAfecFeature": [Function],
"hasWriteAccess": [Function],
"isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isGitHubPaneEnabled": [Function],
"isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"loadQueryPane": LoadQueryPane {
"container": [Circular],
"files": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "loadquerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onImportLinkKeyPress": [Function],
"selectedFilesTitle": [Function],
"title": [Function],
"visible": [Function],
},
"memoryUsageInfo": [Function],
"newVertexPane": NewVertexPane {
"buildString": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "newvertexpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onMoreDetailsKeyPress": [Function],
"onSubmitCreateCallback": null,
"partitionKeyProperty": [Function],
"tempVertexData": [Function],
"title": [Function],
"visible": [Function],
},
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function],
"openDialog": undefined,
"openSidePanel": undefined,
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function],
"refreshTreeTitle": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
"saveQueryPane": SaveQueryPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "savequerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"queryName": [Function],
"setupQueries": [Function],
"setupSaveQueriesText": "For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called “___Cosmos”. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.",
"submit": [Function],
"title": [Function],
"visible": [Function],
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined,
"setNotificationConsoleData": undefined,
"setupNotebooksPane": SetupNotebooksPane {
"container": [Circular],
"description": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "setupnotebookspane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onCompleteSetupClick": [Function],
"onCompleteSetupKeyPress": [Function],
"title": [Function],
"visible": [Function],
},
"signInAad": [Function],
"sparkClusterConnectionInfo": [Function],
"splitter": Splitter {
"bounds": Object {
"max": 400,
"min": 240,
},
"direction": "vertical",
"isCollapsed": [Function],
"leftSideId": "resourcetree",
"onResizeStart": [Function],
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
"subscriptionType": [Function],
"tableColumnOptionsPane": TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
"toggleLeftPaneExpandedKeyPress": [Function],
}
}
formError=""
formErrorDetail=""
id="uploaditemspane"
onClose={[Function]}
onSubmit={[Function]}
submitButtonText="Upload"
title="Upload Items"
>
<div
className="paneMainContent"
>
<Upload
accept="application/json"
label="Select JSON Files"
multiple={true}
onUpload={[Function]}
tabIndex={0}
tooltip="Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB. You can perform multiple upload operations for larger data sets."
/>
</div>
</GenericRightPaneComponent>
`;

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