Compare commits

...

43 Commits

Author SHA1 Message Date
artrejo
2ec9b991e5 Fix table api query projections
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 11:15:31 -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
Jordi Bunster
3530633fa2 Test coverage: include all source files (#551)
In addition this changes the thresholds to meet the existing level
of test coverage. This was motivated by work that imported a lot
of existing yet untested (and unexercised) code, which brought down
the total % without adding any more code.
2021-03-16 14:13:36 -07:00
Steve Faulkner
732d7ce8fa Fix upload worker error display (#547) 2021-03-15 22:47:49 -05:00
Steve Faulkner
254c551999 Test Explorer Improvements (#541) 2021-03-14 22:53:16 -05:00
Jordi Bunster
f86883de6c MostRecentActivity changes (#463)
This changes the public API a bit, so that recording activity (the most common use) is less involved.
2021-03-15 03:10:48 +00:00
Steve Faulkner
62550f8d6a Deprecate Explorer Properties (#535) 2021-03-10 23:02:55 -06:00
hardiknai-techm
184910ee6c Resolve Lint errors in NotebookConfigurationUtils.ts (#490) 2021-03-10 21:47:08 -06:00
Steve Faulkner
9253ab1876 Run Emulator test only on master (#536) 2021-03-10 18:52:32 -06:00
Steve Faulkner
920c95b614 Fix cleanup databases task 2021-03-10 16:03:50 -06:00
Srinath Narayanan
1d98c83be5 Created selfServe landing page (#444)
* 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

* created selfServe.html landing page

* 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

* added spinner on selfserve load

* compile errors fixed

* New changes

* added operationstatus link

* merged sqlxEdits

* undid sqlx changes

* added completed notification

* passed retryInterval in notif options

* more retry changes

* 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

* added selfserve contract to output files

* addressed PR comments

Co-authored-by: Balaji Sridharan <fnbalaji@microsoft.com>
Co-authored-by: fnbalaji <75445927+fnbalaji@users.noreply.github.com>
2021-03-10 13:55:05 -08:00
Sunil Kumar Yadav
b85a20cbea Fix Lint errors in StringUtility.ts (#479) 2021-03-09 19:33:29 -06:00
hardiknai-techm
d0f6923d24 Configure onsave auto format eslint in vscode (#478) 2021-03-09 19:31:38 -06:00
Srinath Narayanan
ecdc41ada9 Added more selfserve changes (#443)
* exposed baselineValues

* added getOnSaveNotification

* disable UI when onSave is taking place

* added optional polling

* Added portal notifications

* minor edits

* added label for description

* Added correlationids and polling of refresh

* added label tooltip

* removed ClassInfo decorator

* Added dynamic decription

* added info and warninf types for description

* promise retry changes

* compile errors fixed

* merged sqlxEdits

* undid sqlx changes

* added completed notification

* passed retryInterval in notif options

* added polling on landing on the page

* edits for error display

* added link generation

* addressed PR comments

* modified test

* fixed compilation error
2021-03-09 16:07:23 -08:00
Tanuj Mittal
c1b74266eb Gallery fixes (#514)
- Fix COC overlay height
- Make standalone gallery usable on mobile devices

Before:
![image](https://user-images.githubusercontent.com/693092/110415215-81cd0680-80b7-11eb-8000-bd0b8536607a.png)

After:
![image](https://user-images.githubusercontent.com/693092/110415236-898cab00-80b7-11eb-8266-94a5718113fe.png)
2021-03-09 18:42:54 +00:00
hardiknai-techm
ef6ecf0a5f Resolve Lint errors in NavBar component (#528) 2021-03-09 12:34:01 -06:00
Sunil Kumar Yadav
b241771e69 fixed eslint of IteratorUtility (#469) 2021-03-09 12:27:24 -06:00
victor-meng
9d63a346e4 Fix issues in the add collection pane (#501) 2021-03-09 10:18:40 -08:00
hardiknai-techm
2e7c7440d3 Resolve Lint errors in CommandBarUtil.test.tsx (#529) 2021-03-09 10:20:14 -06:00
Sunil Kumar Yadav
641dae30a1 fix-eslint-NTeractUtil.ts (#493) 2021-03-09 10:17:02 -06:00
130 changed files with 5239 additions and 29157 deletions

View File

@@ -14,8 +14,6 @@ src/Common/DataAccessUtilityBase.ts
src/Common/EditableUtility.ts src/Common/EditableUtility.ts
src/Common/HashMap.test.ts src/Common/HashMap.test.ts
src/Common/HashMap.ts src/Common/HashMap.ts
src/Common/IteratorUtilities.test.ts
src/Common/IteratorUtilities.ts
src/Common/Logger.test.ts src/Common/Logger.test.ts
src/Common/MessageHandler.test.ts src/Common/MessageHandler.test.ts
src/Common/MessageHandler.ts src/Common/MessageHandler.ts
@@ -26,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
@@ -101,7 +98,6 @@ 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/FileSystemUtil.ts
src/Explorer/Notebook/NTeractUtil.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
@@ -251,8 +247,6 @@ src/Shared/ExplorerSettings.ts
src/Shared/PriceEstimateCalculator.ts src/Shared/PriceEstimateCalculator.ts
src/Shared/StorageUtility.test.ts src/Shared/StorageUtility.test.ts
src/Shared/StorageUtility.ts src/Shared/StorageUtility.ts
src/Shared/StringUtility.test.ts
src/Shared/StringUtility.ts
src/Shared/appInsights.ts src/Shared/appInsights.ts
src/SparkClusterManager/ArcadiaResourceManager.ts src/SparkClusterManager/ArcadiaResourceManager.ts
src/SparkClusterManager/SparkClusterManager.ts src/SparkClusterManager/SparkClusterManager.ts
@@ -263,7 +257,6 @@ src/TokenProviders/PortalTokenProvider.ts
src/TokenProviders/TokenProviderFactory.ts src/TokenProviders/TokenProviderFactory.ts
src/Utils/DatabaseAccountUtils.test.ts src/Utils/DatabaseAccountUtils.test.ts
src/Utils/DatabaseAccountUtils.ts src/Utils/DatabaseAccountUtils.ts
src/Utils/NotebookConfigurationUtils.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/Utils/QueryUtils.ts
@@ -316,15 +309,7 @@ src/Explorer/Graph/GraphExplorerComponent/QueryContainerComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNeighborsComponent.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNeighborsComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx
src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx
src/Explorer/Menus/CommandBar/CommandBarUtil.test.tsx
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
src/Explorer/Menus/CommandBar/MemoryTrackerComponent.tsx
src/Explorer/Menus/NavBar/ControlBarComponent.tsx
src/Explorer/Menus/NavBar/ControlBarComponentAdapter.tsx
src/Explorer/Menus/NavBar/MeControlComponent.test.tsx
src/Explorer/Menus/NavBar/MeControlComponent.tsx
src/Explorer/Menus/NavBar/MeControlComponentAdapter.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponentAdapter.tsx src/Explorer/Menus/NotificationConsole/NotificationConsoleComponentAdapter.tsx

View File

@@ -15,10 +15,10 @@ jobs:
if: github.ref == 'refs/heads/master' if: github.ref == 'refs/heads/master'
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- run: npm ci - run: npm ci
- run: node utils/codeMetrics.js - run: node utils/codeMetrics.js
env: env:
@@ -28,10 +28,10 @@ jobs:
name: "Compile TypeScript" name: "Compile TypeScript"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- run: npm ci - run: npm ci
- run: npm run compile - run: npm run compile
- run: npm run compile:strict - run: npm run compile:strict
@@ -40,10 +40,10 @@ jobs:
name: "Check Format" name: "Check Format"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- run: npm ci - run: npm ci
- run: npm run format:check - run: npm run format:check
lint: lint:
@@ -51,10 +51,10 @@ jobs:
name: "Lint" name: "Lint"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- run: npm ci - run: npm ci
- run: npm run lint - run: npm run lint
unittest: unittest:
@@ -62,10 +62,10 @@ jobs:
name: "Unit Tests" name: "Unit Tests"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- run: npm ci - run: npm ci
- run: npm run test - run: npm run test
build: build:
@@ -74,10 +74,10 @@ jobs:
name: "Build" name: "Build"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- run: npm ci - run: npm ci
- run: npm run build:contracts - run: npm run build:contracts
- name: Restore Build Cache - name: Restore Build Cache
@@ -94,14 +94,14 @@ jobs:
path: dist/ path: dist/
endtoendemulator: endtoendemulator:
name: "End To End Emulator Tests" name: "End To End Emulator Tests"
needs: [lint, format, compile, unittest] if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- uses: southpolesteve/cosmos-emulator-github-action@v1 - uses: southpolesteve/cosmos-emulator-github-action@v1
- name: End to End Tests - name: End to End Tests
run: | run: |
@@ -125,10 +125,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- name: Accessibility Check - name: Accessibility Check
run: | run: |
# Ubuntu gets mad when webpack runs too many files watchers # Ubuntu gets mad when webpack runs too many files watchers
@@ -163,6 +163,7 @@ jobs:
TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }} TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }}
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html" DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html"
strategy: strategy:
fail-fast: false
matrix: matrix:
test-file: test-file:
- ./test/cassandra/container.spec.ts - ./test/cassandra/container.spec.ts

43
.vscode/settings.json vendored
View File

@@ -1,21 +1,26 @@
// Place your settings in this file to overwrite default and user settings. // Place your settings in this file to overwrite default and user settings.
{ {
"files.exclude": { "files.exclude": {
".vs": true, ".vs": true,
".vscode/**": true, ".vscode/**": true,
"*.trx": true, "*.trx": true,
"**/.DS_Store": true, "**/.DS_Store": true,
"**/.git": true, "**/.git": true,
"**/.hg": true, "**/.hg": true,
"**/.svn": true, "**/.svn": true,
"built/**": true, "built/**": true,
"coverage/**": true, "coverage/**": true,
"libs/**": true, "libs/**": true,
"node_modules/**": true, "node_modules/**": true,
"package-lock.json": true, "package-lock.json": true,
"quickstart/**": true, "quickstart/**": true,
"test/out/**": true, "test/out/**": true,
"workers/libs/**": true "workers/libs/**": true
}, },
"typescript.tsdk": "node_modules/typescript/lib" "typescript.tsdk": "node_modules/typescript/lib",
} "editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.organizeImports": true
}
}

View File

@@ -69,7 +69,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

@@ -21,17 +21,13 @@ module.exports = {
collectCoverage: true, collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected // An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: [ collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}"],
// "src/Common/Headers*"
// ],
// The directory where Jest should output its coverage files // The directory where Jest should output its coverage files
coverageDirectory: "coverage", coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection // An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [ coveragePathIgnorePatterns: ["/node_modules/"],
// "/node_modules/"
// ],
// A list of reporter names that Jest uses when writing coverage reports // A list of reporter names that Jest uses when writing coverage reports
coverageReporters: ["json", "text", "cobertura"], coverageReporters: ["json", "text", "cobertura"],
@@ -39,10 +35,10 @@ module.exports = {
// An object that configures minimum threshold enforcement for coverage results // An object that configures minimum threshold enforcement for coverage results
coverageThreshold: { coverageThreshold: {
global: { global: {
branches: 22, branches: 25,
functions: 28, functions: 25,
lines: 33, lines: 30,
statements: 31, statements: 30,
}, },
}, },

26679
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -80,7 +80,7 @@
"ms": "2.1.3", "ms": "2.1.3",
"msal": "1.4.4", "msal": "1.4.4",
"object.entries": "1.1.0", "object.entries": "1.1.0",
"office-ui-fabric-react": "7.134.1", "office-ui-fabric-react": "7.164.2",
"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-polyfill": "8.1.0",
@@ -121,15 +121,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",
@@ -176,7 +176,7 @@
"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",
@@ -238,4 +238,4 @@
"prettier": { "prettier": {
"printWidth": 120 "printWidth": 120
} }
} }

View File

@@ -120,6 +120,7 @@ export class Features {
public static readonly enableDatabaseSettingsTabV1 = "enabledbsettingsv1"; public static readonly enableDatabaseSettingsTabV1 = "enabledbsettingsv1";
public static readonly selfServeType = "selfservetype"; public static readonly selfServeType = "selfservetype";
public static readonly enableKOPanel = "enablekopanel"; public static readonly enableKOPanel = "enablekopanel";
public static readonly enableReactPane = "enablereactpane";
} }
// flight names returned from the portal are always lowercase // flight names returned from the portal are always lowercase

View File

@@ -1,10 +1,10 @@
import * as Cosmos from "@azure/cosmos"; import * as Cosmos from "@azure/cosmos";
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos"; import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
import { configContext, Platform } from "../ConfigContext"; import { configContext, Platform } from "../ConfigContext";
import { getErrorMessage } from "./ErrorHandlingUtils"; import { userContext } from "../UserContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { EmulatorMasterKey, HttpHeaders } from "./Constants"; import { EmulatorMasterKey, HttpHeaders } from "./Constants";
import { userContext } from "../UserContext"; import { getErrorMessage } from "./ErrorHandlingUtils";
const _global = typeof self === "undefined" ? window : self; const _global = typeof self === "undefined" ? window : self;

View File

@@ -1,8 +1,9 @@
import { ARMError } from "../Utils/arm/request";
import { HttpStatusCodes } from "./Constants";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { SubscriptionType } from "../Contracts/SubscriptionType"; import { SubscriptionType } from "../Contracts/SubscriptionType";
import { userContext } from "../UserContext";
import { ARMError } from "../Utils/arm/request";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { HttpStatusCodes } from "./Constants";
import { logError } from "./Logger"; import { logError } from "./Logger";
import { sendMessage } from "./MessageHandler"; import { sendMessage } from "./MessageHandler";
@@ -44,7 +45,7 @@ const sendNotificationForError = (errorMessage: string, errorCode: number | stri
const replaceKnownError = (errorMessage: string): string => { const replaceKnownError = (errorMessage: string): string => {
if ( if (
window.dataExplorer?.subscriptionType() === SubscriptionType.Internal && userContext.subscriptionType === SubscriptionType.Internal &&
errorMessage?.indexOf("SharedOffer is Disabled for your account") >= 0 errorMessage?.indexOf("SharedOffer is Disabled for your account") >= 0
) { ) {
return "Database throughput is not supported for internal subscriptions."; return "Database throughput is not supported for internal subscriptions.";

View File

@@ -1,6 +1,8 @@
import { QueryResults } from "../Contracts/ViewModels"; import { QueryResults } from "../Contracts/ViewModels";
interface QueryResponse { interface QueryResponse {
// [Todo] remove any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resources: any[]; resources: any[];
hasMoreResults: boolean; hasMoreResults: boolean;
activityId: string; activityId: string;
@@ -16,6 +18,7 @@ export interface MinimalQueryIterator {
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> { export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> {
return documentsIterator.fetchNext().then((response) => { return documentsIterator.fetchNext().then((response) => {
const documents = response.resources; const documents = response.resources;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any const headers = (response as any).headers || {}; // TODO this is a private key. Remove any
const itemCount = (documents && documents.length) || 0; const itemCount = (documents && documents.length) || 0;
return { return {

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

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

@@ -9,10 +9,10 @@ export interface DatabaseAccount {
} }
export interface DatabaseAccountExtendedProperties { export interface DatabaseAccountExtendedProperties {
documentEndpoint: string; documentEndpoint?: string;
tableEndpoint: string; tableEndpoint?: string;
gremlinEndpoint: string; gremlinEndpoint?: string;
cassandraEndpoint: string; cassandraEndpoint?: string;
configurationOverrides?: ConfigurationOverrides; configurationOverrides?: ConfigurationOverrides;
capabilities?: Capability[]; capabilities?: Capability[];
enableMultipleWriteLocations?: boolean; enableMultipleWriteLocations?: boolean;

View File

@@ -0,0 +1,9 @@
/**
* Messaging types used with SelfServe Component <-> Portal communication
* and Hosted <-> SelfServe Component communication
*/
export enum SelfServeMessageTypes {
TelemetryInfo = "TelemetryInfo",
Notification = "Notification",
}

View File

@@ -390,10 +390,18 @@ 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[];
selfServeType?: SelfServeType; }
export interface SelfServeFrameInputs {
selfServeType: SelfServeType;
databaseAccount: any;
subscriptionId: string;
resourceGroup: string;
authorizationToken: string;
csmEndpoint: string;
flights?: readonly string[];
} }
export interface CollectionCreationDefaults { export interface CollectionCreationDefaults {

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

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

@@ -354,7 +354,6 @@ exports[`test render renders with filters 1`] = `
data-is-scrollable="true" data-is-scrollable="true"
> >
<div <div
aria-hidden="true"
className="stickyAbove-42" className="stickyAbove-42"
style={ style={
Object { Object {
@@ -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]}
@@ -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-57"
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-58"
data-automationid="splitbuttonprimary" data-automationid="splitbuttonprimary"
> >
<div <div
@@ -1936,7 +1943,6 @@ exports[`test render renders with filters 1`] = `
</List> </List>
</div> </div>
<div <div
aria-hidden="true"
className="stickyBelow-43" className="stickyBelow-43"
style={ style={
Object { Object {

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

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

@@ -11,7 +11,7 @@
.publicGalleryTabContainer { .publicGalleryTabContainer {
position: relative; position: relative;
height: 100vh; min-height: 100vh;
} }
.publicGalleryTabOverlayContent { .publicGalleryTabOverlayContent {

View File

@@ -388,7 +388,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private createSearchBarHeader(content: JSX.Element): JSX.Element { private createSearchBarHeader(content: JSX.Element): JSX.Element {
return ( return (
<Stack tokens={{ childrenGap: 10 }}> <Stack tokens={{ childrenGap: 10 }}>
<Stack horizontal tokens={{ childrenGap: 20, padding: 10 }}> <Stack horizontal wrap tokens={{ childrenGap: 20, padding: 10 }}>
<Stack.Item grow> <Stack.Item grow>
<SearchBox value={this.state.searchText} placeholder="Search" onChange={this.onSearchBoxChange} /> <SearchBox value={this.state.searchText} placeholder="Search" onChange={this.onSearchBoxChange} />
</Stack.Item> </Stack.Item>

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

@@ -36,6 +36,7 @@ exports[`GalleryViewerComponent renders 1`] = `
"padding": 10, "padding": 10,
} }
} }
wrap={true}
> >
<StackItem <StackItem
grow={true} grow={true}
@@ -121,6 +122,7 @@ exports[`GalleryViewerComponent renders 1`] = `
"padding": 10, "padding": 10,
} }
} }
wrap={true}
> >
<StackItem <StackItem
grow={true} grow={true}

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,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;
@@ -325,7 +327,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 +700,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 +862,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 +876,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 +1005,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,53 @@
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 { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import { userContext } from "../../../../../UserContext";
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
import { usageInGB, calculateEstimateNumber } from "../../../../../Utils/PricingUtils";
import { Features } from "../../../../../Common/Constants"; import { Features } from "../../../../../Common/Constants";
import { minAutoPilotThroughput } from "../../../../../Utils/AutoPilotUtils"; import * as DataModels from "../../../../../Contracts/DataModels";
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor"; import * as SharedConstants from "../../../../../Shared/Constants";
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 +180,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 +189,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 +197,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

View File

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

View File

@@ -920,7 +920,6 @@ exports[`SettingsComponent renders 1`] = `
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"hasWriteAccess": [Function], "hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAuthWithResourceToken": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
@@ -937,7 +936,6 @@ exports[`SettingsComponent renders 1`] = `
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function], "isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
"isRefreshingExplorer": [Function],
"isResourceTokenCollectionNodeSelected": [Function], "isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function], "isRightPanelV2Enabled": [Function],
"isSchemaEnabled": [Function], "isSchemaEnabled": [Function],
@@ -946,7 +944,6 @@ exports[`SettingsComponent renders 1`] = `
"isSparkEnabledForAccount": [Function], "isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"isTryCosmosDBSubscription": [Function],
"loadQueryPane": LoadQueryPane { "loadQueryPane": LoadQueryPane {
"container": [Circular], "container": [Circular],
"files": [Function], "files": [Function],
@@ -1060,15 +1057,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined, "setIsNotificationConsoleExpanded": undefined,
"setNotificationConsoleData": undefined, "setNotificationConsoleData": undefined,
@@ -2129,7 +2117,6 @@ exports[`SettingsComponent renders 1`] = `
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"hasWriteAccess": [Function], "hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAuthWithResourceToken": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
@@ -2146,7 +2133,6 @@ exports[`SettingsComponent renders 1`] = `
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function], "isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
"isRefreshingExplorer": [Function],
"isResourceTokenCollectionNodeSelected": [Function], "isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function], "isRightPanelV2Enabled": [Function],
"isSchemaEnabled": [Function], "isSchemaEnabled": [Function],
@@ -2155,7 +2141,6 @@ exports[`SettingsComponent renders 1`] = `
"isSparkEnabledForAccount": [Function], "isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"isTryCosmosDBSubscription": [Function],
"loadQueryPane": LoadQueryPane { "loadQueryPane": LoadQueryPane {
"container": [Circular], "container": [Circular],
"files": [Function], "files": [Function],
@@ -2269,15 +2254,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined, "setIsNotificationConsoleExpanded": undefined,
"setNotificationConsoleData": undefined, "setNotificationConsoleData": undefined,
@@ -3351,7 +3327,6 @@ exports[`SettingsComponent renders 1`] = `
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"hasWriteAccess": [Function], "hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAuthWithResourceToken": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
@@ -3368,7 +3343,6 @@ exports[`SettingsComponent renders 1`] = `
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function], "isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
"isRefreshingExplorer": [Function],
"isResourceTokenCollectionNodeSelected": [Function], "isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function], "isRightPanelV2Enabled": [Function],
"isSchemaEnabled": [Function], "isSchemaEnabled": [Function],
@@ -3377,7 +3351,6 @@ exports[`SettingsComponent renders 1`] = `
"isSparkEnabledForAccount": [Function], "isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"isTryCosmosDBSubscription": [Function],
"loadQueryPane": LoadQueryPane { "loadQueryPane": LoadQueryPane {
"container": [Circular], "container": [Circular],
"files": [Function], "files": [Function],
@@ -3491,15 +3464,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined, "setIsNotificationConsoleExpanded": undefined,
"setNotificationConsoleData": undefined, "setNotificationConsoleData": undefined,
@@ -4560,7 +4524,6 @@ exports[`SettingsComponent renders 1`] = `
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"hasWriteAccess": [Function], "hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAuthWithResourceToken": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
@@ -4577,7 +4540,6 @@ exports[`SettingsComponent renders 1`] = `
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function], "isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
"isRefreshingExplorer": [Function],
"isResourceTokenCollectionNodeSelected": [Function], "isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function], "isRightPanelV2Enabled": [Function],
"isSchemaEnabled": [Function], "isSchemaEnabled": [Function],
@@ -4586,7 +4548,6 @@ exports[`SettingsComponent renders 1`] = `
"isSparkEnabledForAccount": [Function], "isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"isTryCosmosDBSubscription": [Function],
"loadQueryPane": LoadQueryPane { "loadQueryPane": LoadQueryPane {
"container": [Circular], "container": [Circular],
"files": [Function], "files": [Function],
@@ -4700,15 +4661,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined, "setIsNotificationConsoleExpanded": undefined,
"setNotificationConsoleData": undefined, "setNotificationConsoleData": undefined,

View File

@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent"; import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent";
import { NumberUiType, SmartUiInput } from "../../../SelfServe/SelfServeTypes"; import { NumberUiType, SmartUiInput, DescriptionType } from "../../../SelfServe/SelfServeTypes";
describe("SmartUiComponent", () => { describe("SmartUiComponent", () => {
const exampleData: SmartUiDescriptor = { const exampleData: SmartUiDescriptor = {
@@ -18,10 +18,12 @@ describe("SmartUiComponent", () => {
{ {
id: "description", id: "description",
input: { input: {
labelTKey: undefined,
dataFieldName: "description", dataFieldName: "description",
type: "string", type: "string",
description: { description: {
textTKey: "this is an example description text.", textTKey: "this is an example description text.",
type: DescriptionType.Text,
link: { link: {
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
textTKey: "Click here for more information.", textTKey: "Click here for more information.",

View File

@@ -1,24 +1,26 @@
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 { 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,
DescriptionType,
Info, Info,
InputType, InputType,
InputTypeValue, InputTypeValue,
NumberUiType, NumberUiType,
SmartUiInput, SmartUiInput,
} from "../../../SelfServe/SelfServeTypes"; } from "../../../SelfServe/SelfServeTypes";
import { TFunction } from "i18next"; import { ToolTipLabelComponent } from "../Settings/SettingsSubComponents/ToolTipLabelComponent";
import * as InputUtils from "./InputUtils";
import "./SmartUiComponent.less";
/** /**
* Generic UX renderer * Generic UX renderer
@@ -29,15 +31,14 @@ import { TFunction } from "i18next";
*/ */
interface BaseDisplay { interface BaseDisplay {
labelTKey: string;
dataFieldName: string; dataFieldName: string;
errorMessage?: string; errorMessage?: string;
type: InputTypeValue; type: InputTypeValue;
} }
interface BaseInput extends BaseDisplay { interface BaseInput extends BaseDisplay {
labelTKey: string;
placeholderTKey?: string; placeholderTKey?: string;
errorMessage?: string;
} }
/** /**
@@ -67,7 +68,8 @@ interface ChoiceInput extends BaseInput {
} }
interface DescriptionDisplay extends BaseDisplay { interface DescriptionDisplay extends BaseDisplay {
description: Description; description?: Description;
isDynamicDescription?: boolean;
} }
type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay; type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay;
@@ -123,25 +125,28 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
private renderInfo(info: Info): JSX.Element { private renderInfo(info: Info): JSX.Element {
return ( return (
<MessageBar styles={{ root: { width: 400 } }}> info && (
{this.props.getTranslation(info.messageTKey)} <Text>
{info.link && ( {this.props.getTranslation(info.messageTKey)}{" "}
<Link href={info.link.href} target="_blank"> {info.link && (
{this.props.getTranslation(info.link.textTKey)} <Link href={info.link.href} target="_blank">
</Link> {this.props.getTranslation(info.link.textTKey)}
)} </Link>
</MessageBar> )}
</Text>
)
); );
} }
private renderTextInput(input: StringInput): 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`}
label={this.props.getTranslation(input.labelTKey)} aria-labelledby={labelId}
type="text" type="text"
value={value || ""} value={value || ""}
placeholder={this.props.getTranslation(input.placeholderTKey)} placeholder={this.props.getTranslation(input.placeholderTKey)}
@@ -149,32 +154,42 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
onChange={(_, newValue) => this.props.onInputChange(input, newValue)} onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
styles={{ styles={{
root: { width: 400 }, root: { width: 400 },
subComponentStyles: {
label: {
root: {
...SmartUiComponent.labelStyle,
fontWeight: 600,
},
},
},
}} }}
/> />
</div> </Stack>
); );
} }
private renderDescription(input: DescriptionDisplay): JSX.Element { private renderDescription(input: DescriptionDisplay, labelId: string, labelElement: JSX.Element): JSX.Element {
const description = input.description; const dataFieldName = input.dataFieldName;
return ( const description = input.description || (this.props.currentValues.get(dataFieldName)?.value as Description);
<Text id={`${input.dataFieldName}-text-display`}> if (!description) {
{this.props.getTranslation(input.description.textTKey)}{" "} if (!input.isDynamicDescription) {
{description.link && ( return this.renderError("Description is not provided.");
<Link target="_blank" href={input.description.link.href}> }
{this.props.getTranslation(input.description.link.textTKey)} // If input is a dynamic description and description is not available, empty element is rendered
</Link> return <></>;
)} }
</Text> const descriptionElement = (
<Stack>
{labelElement}
<Text id={`${dataFieldName}-text-display`} aria-labelledby={labelId}>
{this.props.getTranslation(description.textTKey)}{" "}
{description.link && (
<Link target="_blank" href={description.link.href}>
{this.props.getTranslation(description.link.textTKey)}
</Link>
)}
</Text>
</Stack>
); );
if (description.type === DescriptionType.Text) {
return descriptionElement;
}
const messageBarType =
description.type === DescriptionType.InfoMessageBar ? MessageBarType.info : MessageBarType.warning;
return <MessageBar messageBarType={messageBarType}>{descriptionElement}</MessageBar>;
} }
private clearError(dataFieldName: string): void { private clearError(dataFieldName: string): void {
@@ -220,13 +235,12 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return undefined; return undefined;
}; };
private renderNumberInput(input: NumberInput): 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 = {
label: this.props.getTranslation(labelTKey),
min: min, min: min,
max: max, max: max,
ariaLabel: labelTKey, ariaLabel: this.props.getTranslation(labelTKey),
step: step, step: step,
}; };
@@ -234,71 +248,73 @@ 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)}
disabled={disabled} onDecrement={(newValue) => this.onDecrement(input, newValue, props.step, props.min)}
styles={{ labelPosition={Position.top}
label: { aria-labelledby={labelId}
...SmartUiComponent.labelStyle, disabled={disabled}
fontWeight: 600, />
}, {this.state.errors.has(dataFieldName) && (
}} <MessageBar messageBarType={MessageBarType.error}>
/> Error: {this.state.errors.get(dataFieldName)}
{this.state.errors.has(dataFieldName) && ( </MessageBar>
<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)}
titleLabel: { styles={{
...SmartUiComponent.labelStyle, root: { width: 400 },
fontWeight: 600, valueLabel: SmartUiComponent.labelStyle,
}, }}
valueLabel: SmartUiComponent.labelStyle, />
}} </div>
/> </Stack>
</div>
); );
} else { } else {
return <>Unsupported number UI type {input.uiType}</>; return <>Unsupported number UI type {input.uiType}</>;
} }
} }
private renderBooleanInput(input: BooleanInput): 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}
label={this.props.getTranslation(input.labelTKey)} <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): JSX.Element { private renderChoiceInput(input: ChoiceInput, labelId: string, labelElement: JSX.Element): JSX.Element {
const { labelTKey, 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;
let selectedKey = value ? value : defaultKey; let selectedKey = value ? value : defaultKey;
@@ -306,53 +322,67 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
selectedKey = ""; selectedKey = "";
} }
return ( return (
<Dropdown <Stack>
id={`${input.dataFieldName}-dropdown-input`} {labelElement}
label={this.props.getTranslation(labelTKey)} <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), dropdownWidth="auto"
}))} options={choices.map((c) => ({
styles={{ key: c.key,
root: { width: 400 }, text: this.props.getTranslation(c.label),
label: { }))}
...SmartUiComponent.labelStyle, styles={{
fontWeight: 600, root: { width: 400 },
}, dropdown: SmartUiComponent.labelStyle,
dropdown: SmartUiComponent.labelStyle, }}
}} />
/> </Stack>
); );
} }
private renderError(input: AnyDisplay): JSX.Element { private renderError(errorMessage: string): JSX.Element {
return <MessageBar messageBarType={MessageBarType.error}>Error: {input.errorMessage}</MessageBar>; return <MessageBar messageBarType={MessageBarType.error}>Error: {errorMessage}</MessageBar>;
} }
private renderDisplay(input: AnyDisplay): JSX.Element { private renderElement(input: AnyDisplay, info: Info): JSX.Element {
if (input.errorMessage) { if (input.errorMessage) {
return this.renderError(input); return this.renderError(input.errorMessage);
} }
const inputHidden = this.props.currentValues.get(input.dataFieldName)?.hidden; const inputHidden = this.props.currentValues.get(input.dataFieldName)?.hidden;
if (inputHidden) { if (inputHidden) {
return <></>; return <></>;
} }
const labelId = `${input.dataFieldName}-label`;
const labelElement: JSX.Element = input.labelTKey && (
<Label id={labelId}>
<ToolTipLabelComponent
label={this.props.getTranslation(input.labelTKey)}
toolTipElement={this.renderInfo(info)}
/>
</Label>
);
return <Stack>{this.renderControl(input, labelId, labelElement)}</Stack>;
}
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) { if ("description" in input || "isDynamicDescription" in input) {
return this.renderDescription(input as DescriptionDisplay); return this.renderDescription(input as DescriptionDisplay, labelId, labelElement);
} }
return this.renderTextInput(input as StringInput); return this.renderTextInput(input as StringInput, labelId, labelElement);
case "number": case "number":
return this.renderNumberInput(input as NumberInput); return this.renderNumberInput(input as NumberInput, labelId, labelElement);
case "boolean": case "boolean":
return this.renderBooleanInput(input as BooleanInput); return this.renderBooleanInput(input as BooleanInput, labelId, labelElement);
case "object": case "object":
return this.renderChoiceInput(input as ChoiceInput); 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}`);
} }
@@ -363,10 +393,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return ( return (
<Stack tokens={containerStackTokens} className="widgetRendererContainer"> <Stack tokens={containerStackTokens} className="widgetRendererContainer">
<Stack.Item> <Stack.Item>{node.input && this.renderElement(node.input, node.info as Info)}</Stack.Item>
{node.info && this.renderInfo(node.info as Info)}
{node.input && this.renderDisplay(node.input)}
</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

@@ -9,25 +9,7 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
} }
> >
<StackItem> <StackItem />
<StyledMessageBarBase
styles={
Object {
"root": Object {
"width": 400,
},
}
}
>
Start at $24/mo per database
<StyledLinkBase
href="https://aka.ms/azure-cosmos-db-pricing"
target="_blank"
>
More Details
</StyledLinkBase>
</StyledMessageBarBase>
</StackItem>
<div <div
key="description" key="description"
> >
@@ -40,18 +22,23 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<StackItem> <StackItem>
<Text <Stack>
id="description-text-display" <Stack>
> <Text
this is an example description text. aria-labelledby="description-label"
id="description-text-display"
<StyledLinkBase >
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction" this is an example description text.
target="_blank"
> <StyledLinkBase
Click here for more information. href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
</StyledLinkBase> target="_blank"
</Text> >
Click here for more information.
</StyledLinkBase>
</Text>
</Stack>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@@ -67,53 +54,55 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<StackItem> <StackItem>
<Stack <Stack>
styles={ <Stack>
Object { <StyledLabelBase
"root": Object { id="throughput-label"
"width": 400, >
}, <ToolTipLabelComponent
} label="Throughput (input)"
} />
tokens={ </StyledLabelBase>
Object { <Stack
"childrenGap": 2, styles={
} Object {
} "root": Object {
> "width": 400,
<CustomizedSpinButton },
ariaLabel="Throughput (input)" }
decrementButtonIcon={
Object {
"iconName": "ChevronDownSmall",
} }
} tokens={
disabled={true} Object {
id="throughput-spinner-input" "childrenGap": 2,
incrementButtonIcon={ }
Object {
"iconName": "ChevronUpSmall",
} }
} >
label="Throughput (input)" <CustomizedSpinButton
labelPosition={0} aria-labelledby="throughput-label"
max={500} ariaLabel="Throughput (input)"
min={400} decrementButtonIcon={
onDecrement={[Function]} Object {
onIncrement={[Function]} "iconName": "ChevronDownSmall",
onValidate={[Function]} }
step={10} }
styles={ disabled={true}
Object { id="throughput-spinner-input"
"label": Object { incrementButtonIcon={
"color": "#393939", Object {
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "iconName": "ChevronUpSmall",
"fontSize": 12, }
"fontWeight": 600, }
}, label=""
} labelPosition={0}
} max={500}
/> min={400}
onDecrement={[Function]}
onIncrement={[Function]}
onValidate={[Function]}
step={10}
/>
</Stack>
</Stack>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -130,37 +119,41 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<StackItem> <StackItem>
<div <Stack>
id="throughput2-slider-input" <Stack>
> <StyledLabelBase
<StyledSliderBase id="throughput2-label"
ariaLabel="Throughput (Slider)" >
disabled={true} <ToolTipLabelComponent
label="Throughput (Slider)" label="Throughput (Slider)"
max={500} />
min={400} </StyledLabelBase>
onChange={[Function]} <div
step={10} id="throughput2-slider-input"
styles={ >
Object { <StyledSliderBase
"root": Object { ariaLabel="Throughput (Slider)"
"width": 400, disabled={true}
}, max={500}
"titleLabel": Object { min={400}
"color": "#393939", onChange={[Function]}
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", step={10}
"fontSize": 12, styles={
"fontWeight": 600, Object {
}, "root": Object {
"valueLabel": Object { "width": 400,
"color": "#393939", },
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "valueLabel": Object {
"fontSize": 12, "color": "#393939",
}, "fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
} "fontSize": 12,
} },
/> }
</div> }
/>
</div>
</Stack>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@@ -197,35 +190,32 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<StackItem> <StackItem>
<div <Stack>
className="stringInputContainer" <Stack>
> <StyledLabelBase
<StyledTextFieldBase id="containerId-label"
disabled={true} >
id="containerId-textField-input" <ToolTipLabelComponent
label="Container id" label="Container id"
onChange={[Function]} />
styles={ </StyledLabelBase>
Object { <StyledTextFieldBase
"root": Object { aria-labelledby="containerId-label"
"width": 400, disabled={true}
}, id="containerId-textField-input"
"subComponentStyles": Object { onChange={[Function]}
"label": Object { styles={
"root": Object { Object {
"color": "#393939", "root": Object {
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "width": 400,
"fontSize": 12,
"fontWeight": 600,
},
}, },
}, }
} }
} type="text"
type="text" value=""
value="" />
/> </Stack>
</div> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@@ -241,22 +231,33 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<StackItem> <StackItem>
<StyledToggleBase <Stack>
checked={false} <Stack>
disabled={true} <StyledLabelBase
id="analyticalStore-toggle-input" id="analyticalStore-label"
label="Analytical Store" >
offText="Disabled" <ToolTipLabelComponent
onChange={[Function]} label="Analytical Store"
onText="Enabled" />
styles={ </StyledLabelBase>
Object { <StyledToggleBase
"root": Object { aria-labelledby="analyticalStore-label"
"width": 400, checked={false}
}, disabled={true}
} id="analyticalStore-toggle-input"
} offText="Disabled"
/> onChange={[Function]}
onText="Enabled"
styles={
Object {
"root": Object {
"width": 400,
},
}
}
/>
</Stack>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@@ -272,47 +273,53 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<StackItem> <StackItem>
<StyledWithResponsiveMode <Stack>
disabled={true} <Stack>
id="database-dropdown-input" <StyledLabelBase
label="Database" id="database-label"
onChange={[Function]} >
options={ <ToolTipLabelComponent
Array [ label="Database"
Object { />
"key": "db1", </StyledLabelBase>
"text": "Database 1", <StyledWithResponsiveMode
}, aria-labelledby="database-label"
Object { disabled={true}
"key": "db2", dropdownWidth="auto"
"text": "Database 2", id="database-dropdown-input"
}, onChange={[Function]}
Object { options={
"key": "db3", Array [
"text": "Database 3", Object {
}, "key": "db1",
] "text": "Database 1",
} },
selectedKey="db2" Object {
styles={ "key": "db2",
Object { "text": "Database 2",
"dropdown": Object { },
"color": "#393939", Object {
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "key": "db3",
"fontSize": 12, "text": "Database 3",
}, },
"label": Object { ]
"color": "#393939", }
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", selectedKey="db2"
"fontSize": 12, styles={
"fontWeight": 600, Object {
}, "dropdown": Object {
"root": Object { "color": "#393939",
"width": 400, "fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
}, "fontSize": 12,
} },
} "root": Object {
/> "width": 400,
},
}
}
/>
</Stack>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@@ -328,25 +335,7 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
} }
> >
<StackItem> <StackItem />
<StyledMessageBarBase
styles={
Object {
"root": Object {
"width": 400,
},
}
}
>
Start at $24/mo per database
<StyledLinkBase
href="https://aka.ms/azure-cosmos-db-pricing"
target="_blank"
>
More Details
</StyledLinkBase>
</StyledMessageBarBase>
</StackItem>
<div <div
key="description" key="description"
> >
@@ -359,18 +348,23 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<StackItem> <StackItem>
<Text <Stack>
id="description-text-display" <Stack>
> <Text
this is an example description text. aria-labelledby="description-label"
id="description-text-display"
<StyledLinkBase >
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction" this is an example description text.
target="_blank"
> <StyledLinkBase
Click here for more information. href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
</StyledLinkBase> target="_blank"
</Text> >
Click here for more information.
</StyledLinkBase>
</Text>
</Stack>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@@ -386,53 +380,55 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<StackItem> <StackItem>
<Stack <Stack>
styles={ <Stack>
Object { <StyledLabelBase
"root": Object { id="throughput-label"
"width": 400, >
}, <ToolTipLabelComponent
} label="Throughput (input)"
} />
tokens={ </StyledLabelBase>
Object { <Stack
"childrenGap": 2, styles={
} Object {
} "root": Object {
> "width": 400,
<CustomizedSpinButton },
ariaLabel="Throughput (input)" }
decrementButtonIcon={
Object {
"iconName": "ChevronDownSmall",
} }
} tokens={
disabled={false} Object {
id="throughput-spinner-input" "childrenGap": 2,
incrementButtonIcon={ }
Object {
"iconName": "ChevronUpSmall",
} }
} >
label="Throughput (input)" <CustomizedSpinButton
labelPosition={0} aria-labelledby="throughput-label"
max={500} ariaLabel="Throughput (input)"
min={400} decrementButtonIcon={
onDecrement={[Function]} Object {
onIncrement={[Function]} "iconName": "ChevronDownSmall",
onValidate={[Function]} }
step={10} }
styles={ disabled={false}
Object { id="throughput-spinner-input"
"label": Object { incrementButtonIcon={
"color": "#393939", Object {
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "iconName": "ChevronUpSmall",
"fontSize": 12, }
"fontWeight": 600, }
}, label=""
} labelPosition={0}
} max={500}
/> min={400}
onDecrement={[Function]}
onIncrement={[Function]}
onValidate={[Function]}
step={10}
/>
</Stack>
</Stack>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -449,36 +445,40 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<StackItem> <StackItem>
<div <Stack>
id="throughput2-slider-input" <Stack>
> <StyledLabelBase
<StyledSliderBase id="throughput2-label"
ariaLabel="Throughput (Slider)" >
label="Throughput (Slider)" <ToolTipLabelComponent
max={500} label="Throughput (Slider)"
min={400} />
onChange={[Function]} </StyledLabelBase>
step={10} <div
styles={ id="throughput2-slider-input"
Object { >
"root": Object { <StyledSliderBase
"width": 400, ariaLabel="Throughput (Slider)"
}, max={500}
"titleLabel": Object { min={400}
"color": "#393939", onChange={[Function]}
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", step={10}
"fontSize": 12, styles={
"fontWeight": 600, Object {
}, "root": Object {
"valueLabel": Object { "width": 400,
"color": "#393939", },
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "valueLabel": Object {
"fontSize": 12, "color": "#393939",
}, "fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
} "fontSize": 12,
} },
/> }
</div> }
/>
</div>
</Stack>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@@ -515,34 +515,31 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<StackItem> <StackItem>
<div <Stack>
className="stringInputContainer" <Stack>
> <StyledLabelBase
<StyledTextFieldBase id="containerId-label"
id="containerId-textField-input" >
label="Container id" <ToolTipLabelComponent
onChange={[Function]} label="Container id"
styles={ />
Object { </StyledLabelBase>
"root": Object { <StyledTextFieldBase
"width": 400, aria-labelledby="containerId-label"
}, id="containerId-textField-input"
"subComponentStyles": Object { onChange={[Function]}
"label": Object { styles={
"root": Object { Object {
"color": "#393939", "root": Object {
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "width": 400,
"fontSize": 12,
"fontWeight": 600,
},
}, },
}, }
} }
} type="text"
type="text" value=""
value="" />
/> </Stack>
</div> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@@ -558,21 +555,32 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<StackItem> <StackItem>
<StyledToggleBase <Stack>
checked={false} <Stack>
id="analyticalStore-toggle-input" <StyledLabelBase
label="Analytical Store" id="analyticalStore-label"
offText="Disabled" >
onChange={[Function]} <ToolTipLabelComponent
onText="Enabled" label="Analytical Store"
styles={ />
Object { </StyledLabelBase>
"root": Object { <StyledToggleBase
"width": 400, aria-labelledby="analyticalStore-label"
}, checked={false}
} id="analyticalStore-toggle-input"
} offText="Disabled"
/> onChange={[Function]}
onText="Enabled"
styles={
Object {
"root": Object {
"width": 400,
},
}
}
/>
</Stack>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@@ -588,46 +596,52 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<StackItem> <StackItem>
<StyledWithResponsiveMode <Stack>
id="database-dropdown-input" <Stack>
label="Database" <StyledLabelBase
onChange={[Function]} id="database-label"
options={ >
Array [ <ToolTipLabelComponent
Object { label="Database"
"key": "db1", />
"text": "Database 1", </StyledLabelBase>
}, <StyledWithResponsiveMode
Object { aria-labelledby="database-label"
"key": "db2", dropdownWidth="auto"
"text": "Database 2", id="database-dropdown-input"
}, onChange={[Function]}
Object { options={
"key": "db3", Array [
"text": "Database 3", Object {
}, "key": "db1",
] "text": "Database 1",
} },
selectedKey="db2" Object {
styles={ "key": "db2",
Object { "text": "Database 2",
"dropdown": Object { },
"color": "#393939", Object {
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "key": "db3",
"fontSize": 12, "text": "Database 3",
}, },
"label": Object { ]
"color": "#393939", }
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", selectedKey="db2"
"fontSize": 12, styles={
"fontWeight": 600, Object {
}, "dropdown": Object {
"root": Object { "color": "#393939",
"width": 400, "fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
}, "fontSize": 12,
} },
} "root": Object {
/> "width": 400,
},
}
}
/>
</Stack>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>

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

@@ -129,7 +129,6 @@ export interface ThroughputInputParams {
throughputModeRadioName: string; throughputModeRadioName: string;
maxAutoPilotThroughputSet: ViewModels.Editable<number>; maxAutoPilotThroughputSet: ViewModels.Editable<number>;
autoPilotUsageCost: ko.Computed<string>; autoPilotUsageCost: ko.Computed<string>;
showAutoPilot?: ko.Observable<boolean>;
overrideWithAutoPilotSettings: ko.Observable<boolean>; overrideWithAutoPilotSettings: ko.Observable<boolean>;
overrideWithProvisionedThroughputSettings: ko.Observable<boolean>; overrideWithProvisionedThroughputSettings: ko.Observable<boolean>;
freeTierExceedThroughputTooltip?: ko.Observable<string>; freeTierExceedThroughputTooltip?: ko.Observable<string>;
@@ -158,7 +157,6 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
public infoBubbleText: string | ko.Observable<string>; public infoBubbleText: string | ko.Observable<string>;
public label: ko.Observable<string>; public label: ko.Observable<string>;
public isFixed: boolean; public isFixed: boolean;
public showAutoPilot: ko.Observable<boolean>;
public isAutoPilotSelected: ko.Observable<boolean>; public isAutoPilotSelected: ko.Observable<boolean>;
public throughputAutoPilotRadioId: string; public throughputAutoPilotRadioId: string;
public throughputProvisionedRadioId: string; public throughputProvisionedRadioId: string;
@@ -202,7 +200,6 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
this.isFixed = !!options.isFixed; this.isFixed = !!options.isFixed;
this.infoBubbleText = options.infoBubbleText || ko.observable<string>(); this.infoBubbleText = options.infoBubbleText || ko.observable<string>();
this.label = options.label || ko.observable<string>(); this.label = options.label || ko.observable<string>();
this.showAutoPilot = options.showAutoPilot !== undefined ? options.showAutoPilot : ko.observable<boolean>(true);
this.isAutoPilotSelected = options.isAutoPilotSelected || ko.observable<boolean>(false); this.isAutoPilotSelected = options.isAutoPilotSelected || ko.observable<boolean>(false);
this.isAutoPilotSelected.subscribe((value) => { this.isAutoPilotSelected.subscribe((value) => {
TelemetryProcessor.trace(Action.ToggleAutoscaleSetting, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.ToggleAutoscaleSetting, ActionModifiers.Mark, {

View File

@@ -17,7 +17,7 @@
</div> </div>
<!-- ko if: !isFixed --> <!-- ko if: !isFixed -->
<div data-bind="visible: showAutoPilot" class="throughputModeContainer"> <div class="throughputModeContainer">
<input <input
class="throughputModeRadio" class="throughputModeRadio"
aria-label="Autopilot mode" aria-label="Autopilot mode"

View File

@@ -1,93 +1,88 @@
import React from "react";
import * as ComponentRegisterer from "./ComponentRegisterer";
import * as Constants from "../Common/Constants";
import * as DataModels from "../Contracts/DataModels";
import * as ko from "knockout"; import * as ko from "knockout";
import { IChoiceGroupProps } from "office-ui-fabric-react";
import * as path from "path"; import * as path from "path";
import * as SharedConstants from "../Shared/Constants";
import * as ViewModels from "../Contracts/ViewModels";
import _ from "underscore";
import AddCollectionPane from "./Panes/AddCollectionPane";
import AddDatabasePane from "./Panes/AddDatabasePane";
import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane";
import AuthHeadersUtil from "../Platform/Hosted/Authorization";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import Database from "./Tree/Database";
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
import GraphStylingPane from "./Panes/GraphStylingPane";
import NewVertexPane from "./Panes/NewVertexPane";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import Q from "q"; import Q from "q";
import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; import React from "react";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import _ from "underscore";
import TerminalTab from "./Tabs/TerminalTab";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager";
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane"; import { ReactAdapter } from "../Bindings/ReactBindingHandler";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import * as Constants from "../Common/Constants";
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
import { configContext, Platform, updateConfigContext } from "../ConfigContext";
import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { DialogProps, TextFieldProps } from "./Controls/Dialog";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane";
import { ExplorerMetrics } from "../Common/Constants"; import { ExplorerMetrics } from "../Common/Constants";
import { ExplorerSettings } from "../Shared/ExplorerSettings"; import { readCollection } from "../Common/dataAccess/readCollection";
import { FileSystemUtil } from "./Notebook/FileSystemUtil"; import { readDatabases } from "../Common/dataAccess/readDatabases";
import { IGalleryItem } from "../Juno/JunoClient"; import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
import { LoadQueryPane } from "./Panes/LoadQueryPane";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { sendMessage, sendCachedDataMessage } from "../Common/MessageHandler"; import { sendCachedDataMessage, sendMessage } from "../Common/MessageHandler";
import { QueriesClient } from "../Common/QueriesClient";
import { Splitter, SplitterBounds, SplitterDirection } from "../Common/Splitter";
import { configContext, Platform } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { SubscriptionType } from "../Contracts/SubscriptionType";
import * as ViewModels from "../Contracts/ViewModels";
import { IGalleryItem } from "../Juno/JunoClient";
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory";
import { RouteHandler } from "../RouteHandlers/RouteHandler";
import { appInsights } from "../Shared/appInsights";
import * as SharedConstants from "../Shared/Constants";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { ExplorerSettings } from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager";
import { updateUserContext, userContext } from "../UserContext";
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { stringToBlob } from "../Utils/BlobUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import * as ComponentRegisterer from "./ComponentRegisterer";
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
import { DialogProps, TextFieldProps } from "./Controls/Dialog";
import { GalleryTab } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import { 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 { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager"; import AddCollectionPane from "./Panes/AddCollectionPane";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import { QueriesClient } from "../Common/QueriesClient"; import AddDatabasePane from "./Panes/AddDatabasePane";
import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane"; import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane";
import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import { RouteHandler } from "../RouteHandlers/RouteHandler"; import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel";
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane";
import GraphStylingPane from "./Panes/GraphStylingPane";
import { LoadQueryPane } from "./Panes/LoadQueryPane";
import NewVertexPane from "./Panes/NewVertexPane";
import { SaveQueryPane } from "./Panes/SaveQueryPane"; import { SaveQueryPane } from "./Panes/SaveQueryPane";
import { SettingsPane } from "./Panes/SettingsPane"; import { SettingsPane } from "./Panes/SettingsPane";
import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane"; import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane";
import { SplashScreen } from "./SplashScreen/SplashScreen";
import { Splitter, SplitterBounds, SplitterDirection } from "../Common/Splitter";
import { StringInputPane } from "./Panes/StringInputPane"; import { StringInputPane } from "./Panes/StringInputPane";
import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane";
import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane"; import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane";
import { TabsManager } from "./Tabs/TabsManager";
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 { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
import { ReactAdapter } from "../Bindings/ReactBindingHandler"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import { toRawContentUri, fromContentUri } from "../Utils/GitHubUtils"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import UserDefinedFunction from "./Tree/UserDefinedFunction"; import TabsBase from "./Tabs/TabsBase";
import { TabsManager } from "./Tabs/TabsManager";
import TerminalTab from "./Tabs/TerminalTab";
import Database from "./Tree/Database";
import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
import StoredProcedure from "./Tree/StoredProcedure"; import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger"; import Trigger from "./Tree/Trigger";
import { ContextualPaneBase } from "./Panes/ContextualPaneBase"; import UserDefinedFunction from "./Tree/UserDefinedFunction";
import TabsBase from "./Tabs/TabsBase";
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
import { updateUserContext, userContext } from "../UserContext";
import { stringToBlob } from "../Utils/BlobUtils";
import { IChoiceGroupProps } from "office-ui-fabric-react";
import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils";
import { SubscriptionType } from "../Contracts/SubscriptionType";
import { appInsights } from "../Shared/appInsights";
import { SelfServeLoadingComponentAdapter } from "../SelfServe/SelfServeLoadingComponentAdapter";
import { SelfServeType } from "../SelfServe/SelfServeUtils";
import { SelfServeComponentAdapter } from "../SelfServe/SelfServeComponentAdapter";
import { GalleryTab } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel";
BindingHandlersRegisterer.registerBindingHandlers(); BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import // Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@@ -118,24 +113,57 @@ export default class Explorer {
public hasWriteAccess: ko.Observable<boolean>; public hasWriteAccess: ko.Observable<boolean>;
public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth; public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth;
/**
* @deprecated
* Use userContext.databaseAccount instead
* */
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>; public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults; public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
/**
* @deprecated
* Use userContext.subscriptionType instead
* */
public subscriptionType: ko.Observable<SubscriptionType>; public subscriptionType: ko.Observable<SubscriptionType>;
/**
* @deprecated
* Use userContext.apiType instead
* */
public defaultExperience: ko.Observable<string>; public defaultExperience: ko.Observable<string>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "SQL"
* */
public isPreferredApiDocumentDB: ko.Computed<boolean>; public isPreferredApiDocumentDB: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Cassandra"
* */
public isPreferredApiCassandra: ko.Computed<boolean>; public isPreferredApiCassandra: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Mongo"
* */
public isPreferredApiMongoDB: ko.Computed<boolean>; public isPreferredApiMongoDB: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Gremlin"
* */
public isPreferredApiGraph: ko.Computed<boolean>; public isPreferredApiGraph: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Tables"
* */
public isPreferredApiTable: ko.Computed<boolean>; public isPreferredApiTable: ko.Computed<boolean>;
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>; public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Mongo"
* */
public isEnableMongoCapabilityPresent: ko.Computed<boolean>; public isEnableMongoCapabilityPresent: ko.Computed<boolean>;
public isServerlessEnabled: ko.Computed<boolean>; public isServerlessEnabled: ko.Computed<boolean>;
public isAccountReady: ko.Observable<boolean>; public isAccountReady: ko.Observable<boolean>;
public selfServeType: ko.Observable<SelfServeType>;
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;
@@ -157,18 +185,15 @@ export default class Explorer {
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>;
public isRefreshingExplorer: ko.Observable<boolean>;
private resourceTree: ResourceTreeAdapter; private resourceTree: ResourceTreeAdapter;
private selfServeComponentAdapter: SelfServeComponentAdapter;
// Resource Token // Resource Token
public resourceTokenDatabaseId: ko.Observable<string>; public resourceTokenDatabaseId: ko.Observable<string>;
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>;
@@ -243,7 +268,6 @@ export default class Explorer {
// React adapters // React adapters
private commandBarComponentAdapter: CommandBarComponentAdapter; private commandBarComponentAdapter: CommandBarComponentAdapter;
private selfServeLoadingComponentAdapter: SelfServeLoadingComponentAdapter;
private static readonly MaxNbDatabasesToAutoExpand = 5; private static readonly MaxNbDatabasesToAutoExpand = 5;
@@ -270,24 +294,7 @@ 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.selfServeType = ko.observable<SelfServeType>(undefined);
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
this.arcadiaToken = ko.observable<string>(); this.arcadiaToken = ko.observable<string>();
this.arcadiaToken.subscribe((token: string) => { this.arcadiaToken.subscribe((token: string) => {
@@ -307,7 +314,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();
@@ -318,7 +327,7 @@ 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)) this.isFeatureEnabled(Constants.Features.enableNotebooks))
); );
@@ -367,15 +376,12 @@ export default class Explorer {
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>(); this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
this.features = ko.observable(); 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);
@@ -439,6 +445,7 @@ export default class Explorer {
databaseAccount databaseAccount
); );
this.defaultExperience(defaultExperience); this.defaultExperience(defaultExperience);
// TODO. Remove this entirely
updateUserContext({ updateUserContext({
defaultExperience: DefaultExperienceUtility.mapDefaultExperienceStringToEnum(defaultExperience), defaultExperience: DefaultExperienceUtility.mapDefaultExperienceStringToEnum(defaultExperience),
}); });
@@ -662,7 +669,6 @@ export default class Explorer {
}); });
this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this); this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this);
this.selfServeComponentAdapter = new SelfServeComponentAdapter(this);
this.loadQueryPane = new LoadQueryPane({ this.loadQueryPane = new LoadQueryPane({
id: "loadquerypane", id: "loadquerypane",
@@ -745,99 +751,90 @@ export default class Explorer {
$(document.body).click(() => $(".commandDropdownContainer").hide()); $(document.body).click(() => $(".commandDropdownContainer").hide());
}); });
// TODO move this to API customization class switch (userContext.apiType) {
this.defaultExperience.subscribe((defaultExperience) => { case "SQL":
const defaultExperienceNormalizedString = ( this.addCollectionText("New Container");
defaultExperience || Constants.DefaultAccountExperience.Default this.addDatabaseText("New Database");
).toLowerCase(); this.collectionTitle("SQL API");
this.collectionTreeNodeAltText("Container");
switch (defaultExperienceNormalizedString) { this.deleteCollectionText("Delete Container");
case Constants.DefaultAccountExperience.DocumentDB.toLowerCase(): this.deleteDatabaseText("Delete Database");
this.addCollectionText("New Container"); this.addCollectionPane.title("Add Container");
this.addDatabaseText("New Database"); this.addCollectionPane.collectionIdTitle("Container id");
this.collectionTitle("SQL API"); this.addCollectionPane.collectionWithThroughputInSharedTitle(
this.collectionTreeNodeAltText("Container"); "Provision dedicated throughput for this container"
this.deleteCollectionText("Delete Container"); );
this.deleteDatabaseText("Delete Database"); this.deleteCollectionConfirmationPane.title("Delete Container");
this.addCollectionPane.title("Add Container"); this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the container id");
this.addCollectionPane.collectionIdTitle("Container id"); this.refreshTreeTitle("Refresh containers");
this.addCollectionPane.collectionWithThroughputInSharedTitle( break;
"Provision dedicated throughput for this container" case "Mongo":
); this.addCollectionText("New Collection");
this.deleteCollectionConfirmationPane.title("Delete Container"); this.addDatabaseText("New Database");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the container id"); this.collectionTitle("Collections");
this.refreshTreeTitle("Refresh containers"); this.collectionTreeNodeAltText("Collection");
break; this.deleteCollectionText("Delete Collection");
case Constants.DefaultAccountExperience.MongoDB.toLowerCase(): this.deleteDatabaseText("Delete Database");
case Constants.DefaultAccountExperience.ApiForMongoDB.toLowerCase(): this.addCollectionPane.title("Add Collection");
this.addCollectionText("New Collection"); this.addCollectionPane.collectionIdTitle("Collection id");
this.addDatabaseText("New Database"); this.addCollectionPane.collectionWithThroughputInSharedTitle(
this.collectionTitle("Collections"); "Provision dedicated throughput for this collection"
this.collectionTreeNodeAltText("Collection"); );
this.deleteCollectionText("Delete Collection"); this.refreshTreeTitle("Refresh collections");
this.deleteDatabaseText("Delete Database"); break;
this.addCollectionPane.title("Add Collection"); case "Gremlin":
this.addCollectionPane.collectionIdTitle("Collection id"); this.addCollectionText("New Graph");
this.addCollectionPane.collectionWithThroughputInSharedTitle( this.addDatabaseText("New Database");
"Provision dedicated throughput for this collection" this.deleteCollectionText("Delete Graph");
); this.deleteDatabaseText("Delete Database");
this.refreshTreeTitle("Refresh collections"); this.collectionTitle("Gremlin API");
break; this.collectionTreeNodeAltText("Graph");
case Constants.DefaultAccountExperience.Graph.toLowerCase(): this.addCollectionPane.title("Add Graph");
this.addCollectionText("New Graph"); this.addCollectionPane.collectionIdTitle("Graph id");
this.addDatabaseText("New Database"); this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this graph");
this.deleteCollectionText("Delete Graph"); this.deleteCollectionConfirmationPane.title("Delete Graph");
this.deleteDatabaseText("Delete Database"); this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the graph id");
this.collectionTitle("Gremlin API"); this.refreshTreeTitle("Refresh graphs");
this.collectionTreeNodeAltText("Graph"); break;
this.addCollectionPane.title("Add Graph"); case "Tables":
this.addCollectionPane.collectionIdTitle("Graph id"); this.addCollectionText("New Table");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this graph"); this.addDatabaseText("New Database");
this.deleteCollectionConfirmationPane.title("Delete Graph"); this.deleteCollectionText("Delete Table");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the graph id"); this.deleteDatabaseText("Delete Database");
this.refreshTreeTitle("Refresh graphs"); this.collectionTitle("Azure Table API");
break; this.collectionTreeNodeAltText("Table");
case Constants.DefaultAccountExperience.Table.toLowerCase(): this.addCollectionPane.title("Add Table");
this.addCollectionText("New Table"); this.addCollectionPane.collectionIdTitle("Table id");
this.addDatabaseText("New Database"); this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
this.deleteCollectionText("Delete Table"); this.refreshTreeTitle("Refresh tables");
this.deleteDatabaseText("Delete Database"); this.addTableEntityPane.title("Add Table Entity");
this.collectionTitle("Azure Table API"); this.editTableEntityPane.title("Edit Table Entity");
this.collectionTreeNodeAltText("Table"); this.deleteCollectionConfirmationPane.title("Delete Table");
this.addCollectionPane.title("Add Table"); this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.addCollectionPane.collectionIdTitle("Table id"); this.tableDataClient = new TablesAPIDataClient();
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table"); break;
this.refreshTreeTitle("Refresh tables"); case "Cassandra":
this.addTableEntityPane.title("Add Table Entity"); this.addCollectionText("New Table");
this.editTableEntityPane.title("Edit Table Entity"); this.addDatabaseText("New Keyspace");
this.deleteCollectionConfirmationPane.title("Delete Table"); this.deleteCollectionText("Delete Table");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id"); this.deleteDatabaseText("Delete Keyspace");
this.tableDataClient = new TablesAPIDataClient(); this.collectionTitle("Cassandra API");
break; this.collectionTreeNodeAltText("Table");
case Constants.DefaultAccountExperience.Cassandra.toLowerCase(): this.addCollectionPane.title("Add Table");
this.addCollectionText("New Table"); this.addCollectionPane.collectionIdTitle("Table id");
this.addDatabaseText("New Keyspace"); this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
this.deleteCollectionText("Delete Table"); this.refreshTreeTitle("Refresh tables");
this.deleteDatabaseText("Delete Keyspace"); this.addTableEntityPane.title("Add Table Row");
this.collectionTitle("Cassandra API"); this.editTableEntityPane.title("Edit Table Row");
this.collectionTreeNodeAltText("Table"); this.deleteCollectionConfirmationPane.title("Delete Table");
this.addCollectionPane.title("Add Table"); this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.addCollectionPane.collectionIdTitle("Table id"); this.deleteDatabaseConfirmationPane.title("Delete Keyspace");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table"); this.deleteDatabaseConfirmationPane.databaseIdConfirmationText("Confirm by typing the keyspace id");
this.refreshTreeTitle("Refresh tables"); this.tableDataClient = new CassandraAPIDataClient();
this.addTableEntityPane.title("Add Table Row"); break;
this.editTableEntityPane.title("Edit Table Row"); }
this.deleteCollectionConfirmationPane.title("Delete Table");
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();
break;
}
});
this.commandBarComponentAdapter = new CommandBarComponentAdapter(this); this.commandBarComponentAdapter = new CommandBarComponentAdapter(this);
this.selfServeLoadingComponentAdapter = new SelfServeLoadingComponentAdapter();
this._initSettings(); this._initSettings();
@@ -1065,7 +1062,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,
}); });
@@ -1095,22 +1091,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(
@@ -1170,8 +1163,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();
}; };
@@ -1407,20 +1401,6 @@ export default class Explorer {
return false; return false;
} }
public setSelfServeType(inputs: ViewModels.DataExplorerInputsFrame): void {
const selfServeFeature = inputs.features[Constants.Features.selfServeType];
if (selfServeFeature) {
// self serve type received from query string
const selfServeType = SelfServeType[selfServeFeature?.toLowerCase() as keyof typeof SelfServeType];
this.selfServeType(selfServeType ? selfServeType : SelfServeType.invalid);
} else if (inputs.selfServeType) {
// self serve type received from portal
this.selfServeType(inputs.selfServeType);
} else {
this.selfServeType(SelfServeType.none);
}
}
public configure(inputs: ViewModels.DataExplorerInputsFrame): void { public configure(inputs: ViewModels.DataExplorerInputsFrame): void {
if (inputs != null) { if (inputs != null) {
// In development mode, save the iframe message from the portal in session storage. // In development mode, save the iframe message from the portal in session storage.
@@ -1429,39 +1409,18 @@ export default class Explorer {
sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs)); sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs));
} }
const authorizationToken = inputs.authorizationToken || "";
const masterKey = inputs.masterKey || "";
const databaseAccount = inputs.databaseAccount || null; const databaseAccount = inputs.databaseAccount || null;
if (inputs.defaultCollectionThroughput) { if (inputs.defaultCollectionThroughput) {
this.collectionCreationDefaults = inputs.defaultCollectionThroughput; this.collectionCreationDefaults = inputs.defaultCollectionThroughput;
} }
this.features(inputs.features); 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);
this.setSelfServeType(inputs);
updateConfigContext({
BACKEND_ENDPOINT: inputs.extensionEndpoint || configContext.BACKEND_ENDPOINT,
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
});
updateUserContext({
authorizationToken,
masterKey,
databaseAccount,
resourceGroup: inputs.resourceGroup,
subscriptionId: inputs.subscriptionId,
subscriptionType: inputs.subscriptionType,
quotaId: inputs.quotaId,
});
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.LoadDatabaseAccount, Action.LoadDatabaseAccount,
{ {
@@ -1541,9 +1500,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"
); );
} }
@@ -2408,10 +2367,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 (this.isFeatureEnabled(Constants.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 {
@@ -2551,4 +2512,16 @@ export default class Explorer {
/> />
); );
} }
public async openAddCollectionPanel(): Promise<void> {
await this.loadDatabaseOffers();
this.openSidePanel(
"New Collection",
<AddCollectionPanel
explorer={this}
closePanel={() => this.closeSidePanel()}
openNotificationConsole={() => this.expandConsole()}
/>
);
}
} }

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

View File

@@ -1,5 +1,4 @@
import * as CommandBarUtil from "./CommandBarUtil"; import * as CommandBarUtil from "./CommandBarUtil";
import * as ViewModels from "../../../Contracts/ViewModels";
import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar"; import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
@@ -26,7 +25,7 @@ describe("CommandBarUtil tests", () => {
const converteds = CommandBarUtil.convertButton([btn], backgroundColor); const converteds = CommandBarUtil.convertButton([btn], backgroundColor);
expect(converteds.length).toBe(1); expect(converteds.length).toBe(1);
const converted = converteds[0]; const converted = converteds[0];
expect(!converted.split); expect(converted.split).toBe(undefined);
expect(converted.iconProps.imageProps.src).toEqual(btn.iconSrc); expect(converted.iconProps.imageProps.src).toEqual(btn.iconSrc);
expect(converted.iconProps.imageProps.alt).toEqual(btn.iconAlt); expect(converted.iconProps.imageProps.alt).toEqual(btn.iconAlt);
expect(converted.text).toEqual(btn.commandButtonLabel); expect(converted.text).toEqual(btn.commandButtonLabel);
@@ -50,7 +49,7 @@ describe("CommandBarUtil tests", () => {
const converteds = CommandBarUtil.convertButton([btn], "backgroundColor"); const converteds = CommandBarUtil.convertButton([btn], "backgroundColor");
expect(converteds.length).toBe(1); expect(converteds.length).toBe(1);
const converted = converteds[0]; const converted = converteds[0];
expect(converted.split); expect(converted.split).toBe(true);
expect(converted.subMenuProps.items.length).toBe(btn.children.length); expect(converted.subMenuProps.items.length).toBe(btn.children.length);
for (let i = 0; i < converted.subMenuProps.items.length; i++) { for (let i = 0; i < converted.subMenuProps.items.length; i++) {
expect(converted.subMenuProps.items[i].text).toEqual(btn.children[i].commandButtonLabel); expect(converted.subMenuProps.items[i].text).toEqual(btn.children[i].commandButtonLabel);
@@ -64,7 +63,6 @@ describe("CommandBarUtil tests", () => {
} }
const converteds = CommandBarUtil.convertButton(btns, "backgroundColor"); const converteds = CommandBarUtil.convertButton(btns, "backgroundColor");
const keys = converteds.map((btn: ICommandBarItemProps) => btn.key);
const uniqueKeys = converteds const uniqueKeys = converteds
.map((btn: ICommandBarItemProps) => btn.key) .map((btn: ICommandBarItemProps) => btn.key)
.filter((value: string, index: number, self: string[]) => self.indexOf(value) === index); .filter((value: string, index: number, self: string[]) => self.indexOf(value) === index);
@@ -75,7 +73,7 @@ describe("CommandBarUtil tests", () => {
const btn = createButton(); const btn = createButton();
const backgroundColor = "backgroundColor"; const backgroundColor = "backgroundColor";
btn.commandButtonLabel = null; btn.commandButtonLabel = undefined;
let converted = CommandBarUtil.convertButton([btn], backgroundColor)[0]; let converted = CommandBarUtil.convertButton([btn], backgroundColor)[0];
expect(converted.text).toEqual(btn.tooltipText); expect(converted.text).toEqual(btn.tooltipText);

View File

@@ -17,7 +17,7 @@ export class ControlBarComponent extends React.Component<ControlBarComponentProp
return commandButtonOptions.map( return commandButtonOptions.map(
(btn: CommandButtonComponentProps, index: number): JSX.Element => { (btn: CommandButtonComponentProps, index: number): JSX.Element => {
// Remove label // Remove label
btn.commandButtonLabel = null; btn.commandButtonLabel = undefined;
return CommandButtonComponent.renderButton(btn, `${index}`); return CommandButtonComponent.renderButton(btn, `${index}`);
} }
); );

View File

@@ -0,0 +1,86 @@
import { observable } from "knockout";
import { mostRecentActivity } from "./MostRecentActivity";
describe("MostRecentActivity", () => {
const accountId = "some account";
beforeEach(() => mostRecentActivity.clear(accountId));
it("Has no items at first", () => {
expect(mostRecentActivity.getItems(accountId)).toStrictEqual([]);
});
it("Can record collections being opened", () => {
const collectionId = "some collection";
const databaseId = "some database";
const collection = {
id: observable(collectionId),
databaseId,
};
mostRecentActivity.collectionWasOpened(accountId, collection);
const activity = mostRecentActivity.getItems(accountId);
expect(activity).toEqual([
expect.objectContaining({
collectionId,
databaseId,
}),
]);
});
it("Can record notebooks being opened", () => {
const name = "some notebook";
const path = "some path";
const notebook = { name, path };
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
const activity = mostRecentActivity.getItems(accountId);
expect(activity).toEqual([expect.objectContaining(notebook)]);
});
it("Filters out duplicates", () => {
const name = "some notebook";
const path = "some path";
const notebook = { name, path };
const sameNotebook = { name, path };
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
mostRecentActivity.notebookWasItemOpened(accountId, sameNotebook);
const activity = mostRecentActivity.getItems(accountId);
expect(activity.length).toEqual(1);
expect(activity).toEqual([expect.objectContaining(notebook)]);
});
it("Allows for multiple accounts", () => {
const name = "some notebook";
const path = "some path";
const notebook = { name, path };
const anotherNotebook = { name: "Another " + name, path };
const anotherAccountId = "Another " + accountId;
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
mostRecentActivity.notebookWasItemOpened(anotherAccountId, anotherNotebook);
expect(mostRecentActivity.getItems(accountId)).toEqual([expect.objectContaining(notebook)]);
expect(mostRecentActivity.getItems(anotherAccountId)).toEqual([expect.objectContaining(anotherNotebook)]);
});
it("Can store multiple distinct elements, in FIFO order", () => {
const name = "some notebook";
const path = "some path";
const first = { name, path };
const second = { name: "Another " + name, path };
const third = { name, path: "Another " + path };
mostRecentActivity.notebookWasItemOpened(accountId, first);
mostRecentActivity.notebookWasItemOpened(accountId, second);
mostRecentActivity.notebookWasItemOpened(accountId, third);
const activity = mostRecentActivity.getItems(accountId);
expect(activity).toEqual([third, second, first].map(expect.objectContaining));
});
});

View File

@@ -1,4 +1,6 @@
import { CollectionBase } from "../../Contracts/ViewModels";
import { StorageKey, LocalStorageUtility } from "../../Shared/StorageUtility"; import { StorageKey, LocalStorageUtility } from "../../Shared/StorageUtility";
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
export enum Type { export enum Type {
OpenCollection, OpenCollection,
@@ -6,21 +8,18 @@ export enum Type {
} }
export interface OpenNotebookItem { export interface OpenNotebookItem {
type: Type.OpenNotebook;
name: string; name: string;
path: string; path: string;
} }
export interface OpenCollectionItem { export interface OpenCollectionItem {
type: Type.OpenCollection;
databaseId: string; databaseId: string;
collectionId: string; collectionId: string;
} }
export interface Item { type Item = OpenNotebookItem | OpenCollectionItem;
type: Type;
title: string;
description: string;
data: OpenNotebookItem | OpenCollectionItem;
}
// Update schemaVersion if you are going to change this interface // Update schemaVersion if you are going to change this interface
interface StoredData { interface StoredData {
@@ -32,7 +31,7 @@ interface StoredData {
* Stores most recent activity * Stores most recent activity
*/ */
class MostRecentActivity { class MostRecentActivity {
private static readonly schemaVersion: string = "1"; private static readonly schemaVersion: string = "2";
private static itemsMaxNumber: number = 5; private static itemsMaxNumber: number = 5;
private storedData: StoredData; private storedData: StoredData;
constructor() { constructor() {
@@ -92,7 +91,7 @@ class MostRecentActivity {
LocalStorageUtility.setEntryString(StorageKey.MostRecentActivity, JSON.stringify(this.storedData)); LocalStorageUtility.setEntryString(StorageKey.MostRecentActivity, JSON.stringify(this.storedData));
} }
public addItem(accountId: string, newItem: Item): void { private addItem(accountId: string, newItem: Item): void {
// When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable. // When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable.
// if (!accountId) { // if (!accountId) {
// return; // return;
@@ -111,6 +110,23 @@ class MostRecentActivity {
return this.storedData.itemsMap[accountId] || []; return this.storedData.itemsMap[accountId] || [];
} }
public collectionWasOpened(accountId: string, { id, databaseId }: Pick<CollectionBase, "id" | "databaseId">) {
const collectionId = id();
this.addItem(accountId, {
type: Type.OpenCollection,
databaseId,
collectionId,
});
}
public notebookWasItemOpened(accountId: string, { name, path }: Pick<NotebookContentItem, "name" | "path">) {
this.addItem(accountId, {
type: Type.OpenNotebook,
name,
path,
});
}
public clear(accountId: string): void { public clear(accountId: string): void {
delete this.storedData.itemsMap[accountId]; delete this.storedData.itemsMap[accountId];
this.saveToLocalStorage(); this.saveToLocalStorage();
@@ -128,11 +144,7 @@ class MostRecentActivity {
let index = -1; let index = -1;
for (let i = 0; i < itemsArray.length; i++) { for (let i = 0; i < itemsArray.length; i++) {
const currentItem = itemsArray[i]; const currentItem = itemsArray[i];
if ( if (JSON.stringify(currentItem) === JSON.stringify(item)) {
currentItem.title === item.title &&
currentItem.description === item.description &&
JSON.stringify(currentItem.data) === JSON.stringify(item.data)
) {
index = i; index = i;
break; break;
} }

View File

@@ -3,20 +3,18 @@ import { NotebookContentRecordProps, selectors } from "@nteract/core";
/** /**
* A bunch of utilities to interact with nteract * A bunch of utilities to interact with nteract
*/ */
export default class NTeractUtil { export function getCurrentCellType(content: NotebookContentRecordProps): "markdown" | "code" | "raw" | undefined {
public static getCurrentCellType(content: NotebookContentRecordProps): "markdown" | "code" | "raw" | undefined { if (!content) {
if (!content) {
return undefined;
}
const cellFocusedId = selectors.notebook.cellFocused(content.model);
if (cellFocusedId) {
const cell = selectors.notebook.cellById(content.model, { id: cellFocusedId });
if (cell) {
return cell.cell_type;
}
}
return undefined; return undefined;
} }
const cellFocusedId = selectors.notebook.cellFocused(content.model);
if (cellFocusedId) {
const cell = selectors.notebook.cellById(content.model, { id: cellFocusedId });
if (cell) {
return cell.cell_type;
}
}
return undefined;
} }

View File

@@ -29,7 +29,7 @@ import "@nteract/styles/global-variables.css";
import "react-table/react-table.css"; import "react-table/react-table.css";
import * as CdbActions from "./actions"; import * as CdbActions from "./actions";
import NteractUtil from "../NTeractUtil"; import * as NteractUtil from "../NTeractUtil";
export interface NotebookComponentBootstrapperOptions { export interface NotebookComponentBootstrapperOptions {
notebookClient: NotebookClientV2; notebookClient: NotebookClientV2;

View File

@@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import { AppState, ContentRef, selectors } from "@nteract/core"; import { AppState, ContentRef, selectors } from "@nteract/core";
import { connect } from "react-redux"; import { connect } from "react-redux";
import NteractUtil from "../NTeractUtil"; import * as NteractUtil from "../NTeractUtil";
interface VirtualCommandBarComponentProps { interface VirtualCommandBarComponentProps {
kernelSpecName: string; kernelSpecName: string;

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

@@ -214,7 +214,6 @@
maxAutoPilotThroughputSet: sharedAutoPilotThroughput, maxAutoPilotThroughputSet: sharedAutoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost, autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue, canExceedMaximumValue: canExceedMaximumValue,
showAutoPilot: !isFreeTierAccount(),
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}" }"
> >
@@ -435,7 +434,6 @@
maxAutoPilotThroughputSet: autoPilotThroughput, maxAutoPilotThroughputSet: autoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost, autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue, canExceedMaximumValue: canExceedMaximumValue,
showAutoPilot: !isFixedStorageSelected(),
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}" }"
> >

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(),
@@ -749,12 +750,16 @@ export default class AddCollectionPane extends ContextualPaneBase {
return undefined; return undefined;
} }
if (this.isAutoPilotSelected()) { // return undefined if autopilot is selected for the new database/collection
return undefined; if (this.databaseCreateNew()) {
} // database is shared and autopilot is sleected for the database
if (this.databaseCreateNewShared() && this.isSharedAutoPilotSelected()) {
if (this.databaseCreateNewShared() && this.isSharedAutoPilotSelected()) { return undefined;
return undefined; }
// database is not shared and autopilot is selected for the collection
if (!this.databaseCreateNewShared() && this.isAutoPilotSelected()) {
return undefined;
}
} }
return this._getThroughput(); return this._getThroughput();

File diff suppressed because it is too large Load Diff

View File

@@ -149,7 +149,6 @@
maxAutoPilotThroughputSet: maxAutoPilotThroughputSet, maxAutoPilotThroughputSet: maxAutoPilotThroughputSet,
autoPilotUsageCost: autoPilotUsageCost, autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue, canExceedMaximumValue: canExceedMaximumValue,
showAutoPilot: !isFreeTierAccount(),
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}" }"
> >

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

@@ -166,7 +166,6 @@
autoPilotUsageCost: autoPilotUsageCost, autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue, canExceedMaximumValue: canExceedMaximumValue,
costsVisible: costsVisible, costsVisible: costsVisible,
showAutoPilot: !isFreeTierAccount()
}" }"
> >
</throughput-input-autopilot-v3> </throughput-input-autopilot-v3>

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

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,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,19 @@
} }
} }
.panelFooter button { .panelFooter {
height: 30px; padding: 20px 34px;
border-top: solid 1px #bbbbbb;
& button {
height: 30px;
}
} }
.deleteCollectionFeedback { .deleteCollectionFeedback {
margin-top: 12px; margin-top: 12px;
} }
.panelGroupSpacing > * {
margin-bottom: @SmallSpace;
}

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

@@ -5,7 +5,7 @@ import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsol
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { StringUtility } from "../../Shared/StringUtility"; import * as StringUtility from "../../Shared/StringUtility";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
export class SettingsPane extends ContextualPaneBase { export class SettingsPane extends ContextualPaneBase {

View File

@@ -16,9 +16,15 @@ exports[`PaneContainerComponent test should be resize if notification console is
} }
styles={ styles={
Object { Object {
"commands": Object {
"marginTop": 8,
},
"content": Object { "content": Object {
"height": "100%", "height": "100%",
"padding": "24px 34px 20px 34px", "padding": 0,
},
"header": Object {
"padding": "0 0 8px 34px",
}, },
"navigation": Object { "navigation": Object {
"borderBottom": "1px solid #cccccc", "borderBottom": "1px solid #cccccc",
@@ -52,9 +58,15 @@ exports[`PaneContainerComponent test should render with panel content and header
} }
styles={ styles={
Object { Object {
"commands": Object {
"marginTop": 8,
},
"content": Object { "content": Object {
"height": "100%", "height": "100%",
"padding": "24px 34px 20px 34px", "padding": 0,
},
"header": Object {
"padding": "0 0 8px 34px",
}, },
"navigation": Object { "navigation": Object {
"borderBottom": "1px solid #cccccc", "borderBottom": "1px solid #cccccc",

View File

@@ -1,25 +1,26 @@
/** /**
* Accordion top class * Accordion top class
*/ */
import * as React from "react";
import * as ViewModels from "../../Contracts/ViewModels";
import * as Constants from "../../Common/Constants";
import { Link } from "office-ui-fabric-react/lib/Link"; import { Link } from "office-ui-fabric-react/lib/Link";
import * as React from "react";
import AddDatabaseIcon from "../../../images/AddDatabase.svg";
import NewQueryIcon from "../../../images/AddSqlQuery_16x16.svg";
import NewStoredProcedureIcon from "../../../images/AddStoredProcedure.svg";
import OpenQueryIcon from "../../../images/BrowseQuery.svg";
import NewContainerIcon from "../../../images/Hero-new-container.svg"; import NewContainerIcon from "../../../images/Hero-new-container.svg";
import NewNotebookIcon from "../../../images/Hero-new-notebook.svg"; import NewNotebookIcon from "../../../images/Hero-new-notebook.svg";
import NewQueryIcon from "../../../images/AddSqlQuery_16x16.svg";
import OpenQueryIcon from "../../../images/BrowseQuery.svg";
import NewStoredProcedureIcon from "../../../images/AddStoredProcedure.svg";
import ScaleAndSettingsIcon from "../../../images/Scale_15x15.svg";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
import AddDatabaseIcon from "../../../images/AddDatabase.svg";
import SampleIcon from "../../../images/Hero-sample.svg"; import SampleIcon from "../../../images/Hero-sample.svg";
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil"; import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import Explorer from "../Explorer"; import ScaleAndSettingsIcon from "../../../images/Scale_15x15.svg";
import CollectionIcon from "../../../images/tree-collection.svg";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLauncher"; import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLauncher";
import CollectionIcon from "../../../images/tree-collection.svg"; import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; import Explorer from "../Explorer";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
export interface SplashScreenItem { export interface SplashScreenItem {
iconSrc: string; iconSrc: string;
@@ -217,46 +218,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
return heroes; return heroes;
} }
private getItemIcon(item: MostRecentActivity.Item): string {
switch (item.type) {
case MostRecentActivity.Type.OpenCollection:
return CollectionIcon;
case MostRecentActivity.Type.OpenNotebook:
return NotebookIcon;
default:
return null;
}
}
private onItemClicked(item: MostRecentActivity.Item) {
switch (item.type) {
case MostRecentActivity.Type.OpenCollection: {
const openCollectionitem = item.data as MostRecentActivity.OpenCollectionItem;
const collection = this.container.findCollection(
openCollectionitem.databaseId,
openCollectionitem.collectionId
);
if (collection) {
collection.openTab();
}
break;
}
case MostRecentActivity.Type.OpenNotebook: {
const openNotebookItem = item.data as MostRecentActivity.OpenNotebookItem;
const notebookItem = this.container.createNotebookContentItemFile(openNotebookItem.name, openNotebookItem.path);
notebookItem && this.container.openNotebook(notebookItem);
break;
}
default:
console.error("Unknown item type", item);
break;
}
}
private createCommonTaskItems(): SplashScreenItem[] { private createCommonTaskItems(): SplashScreenItem[] {
const items: SplashScreenItem[] = []; const items: SplashScreenItem[] = [];
if (this.container.isAuthWithResourceToken()) { if (userContext.authType === AuthType.ResourceToken) {
return items; return items;
} }
@@ -333,23 +298,45 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
return items; return items;
} }
private static getInfo(item: MostRecentActivity.Item): string { private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
if (item.type === MostRecentActivity.Type.OpenNotebook) { return {
const data = item.data as MostRecentActivity.OpenNotebookItem; iconSrc: NotebookIcon,
return data.path; title: collectionId,
} else { description: "Data",
return undefined; onClick: () => {
} const collection = this.container.findCollection(databaseId, collectionId);
collection && collection.openTab();
},
};
}
private decorateOpenNotebookActivity({ name, path }: MostRecentActivity.OpenNotebookItem) {
return {
info: path,
iconSrc: CollectionIcon,
title: name,
description: "Notebook",
onClick: () => {
const notebookItem = this.container.createNotebookContentItemFile(name, path);
notebookItem && this.container.openNotebook(notebookItem);
},
};
} }
private createRecentItems(): SplashScreenItem[] { private createRecentItems(): SplashScreenItem[] {
return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((item) => ({ return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((activity) => {
iconSrc: this.getItemIcon(item), switch (activity.type) {
title: item.title, default: {
description: item.description, const unknownActivity: never = activity;
info: SplashScreen.getInfo(item), throw new Error(`Unknown activity: ${unknownActivity}`);
onClick: () => this.onItemClicked(item), }
})); case MostRecentActivity.Type.OpenNotebook:
return this.decorateOpenNotebookActivity(activity);
case MostRecentActivity.Type.OpenCollection:
return this.decorateOpenCollectionActivity(activity);
}
});
} }
private createTipsItems(): SplashScreenItem[] { private createTipsItems(): SplashScreenItem[] {

View File

@@ -1,22 +1,21 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as _ from "underscore";
import Q from "q"; import Q from "q";
import * as _ from "underscore";
import { Areas } from "../../../Common/Constants";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { CassandraTableKey, CassandraAPIDataClient } from "../TableDataClient"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import DataTableViewModel from "./DataTableViewModel"; import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as DataTableUtilities from "./DataTableUtilities"; import * as Constants from "../Constants";
import { getQuotedCqlIdentifier } from "../CqlUtilities"; import { getQuotedCqlIdentifier } from "../CqlUtilities";
import * as Entities from "../Entities";
import { CassandraAPIDataClient, CassandraTableKey } from "../TableDataClient";
import * as TableEntityProcessor from "../TableEntityProcessor";
import * as Utilities from "../Utilities";
import * as DataTableUtilities from "./DataTableUtilities";
import DataTableViewModel from "./DataTableViewModel";
import TableCommands from "./TableCommands"; import TableCommands from "./TableCommands";
import TableEntityCache from "./TableEntityCache"; import TableEntityCache from "./TableEntityCache";
import * as Constants from "../Constants";
import { Areas } from "../../../Common/Constants";
import * as Utilities from "../Utilities";
import * as Entities from "../Entities";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as TableEntityProcessor from "../TableEntityProcessor";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as ViewModels from "../../../Contracts/ViewModels";
interface IListTableEntitiesSegmentedResult extends Entities.IListTableEntitiesResult { interface IListTableEntitiesSegmentedResult extends Entities.IListTableEntitiesResult {
ExceedMaximumRetries?: boolean; ExceedMaximumRetries?: boolean;
@@ -354,8 +353,8 @@ export default class TableEntityListViewModel extends DataTableViewModel {
itemB = new Date(<string>(<any>rowB[col])._); itemB = new Date(<string>(<any>rowB[col])._);
break; break;
default: default:
itemA = <string>(<any>rowA[col])._.toLowerCase(); itemA = <string>(<any>rowA[col])._?.toLowerCase();
itemB = <string>(<any>rowB[col])._.toLowerCase(); itemB = <string>(<any>rowB[col])._?.toLowerCase();
} }
var compareResult: number = itemA < itemB ? -1 : itemA > itemB ? 1 : 0; var compareResult: number = itemA < itemB ? -1 : itemA > itemB ? 1 : 0;
if (compareResult !== 0) { if (compareResult !== 0) {

View File

@@ -1,17 +1,17 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as CustomTimestampHelper from "./CustomTimestampHelper"; import { KeyCodes } from "../../../Common/Constants";
import { getQuotedCqlIdentifier } from "../CqlUtilities";
import QueryClauseViewModel from "./QueryClauseViewModel";
import ClauseGroup from "./ClauseGroup";
import ClauseGroupViewModel from "./ClauseGroupViewModel";
import QueryViewModel from "./QueryViewModel";
import * as Constants from "../Constants"; import * as Constants from "../Constants";
import TableEntityListViewModel from "../DataTable/TableEntityListViewModel"; import { getQuotedCqlIdentifier } from "../CqlUtilities";
import * as DateTimeUtilities from "./DateTimeUtilities";
import * as DataTableUtilities from "../DataTable/DataTableUtilities"; import * as DataTableUtilities from "../DataTable/DataTableUtilities";
import TableEntityListViewModel from "../DataTable/TableEntityListViewModel";
import * as TableEntityProcessor from "../TableEntityProcessor"; import * as TableEntityProcessor from "../TableEntityProcessor";
import * as Utilities from "../Utilities"; import * as Utilities from "../Utilities";
import { KeyCodes } from "../../../Common/Constants"; import ClauseGroup from "./ClauseGroup";
import ClauseGroupViewModel from "./ClauseGroupViewModel";
import * as CustomTimestampHelper from "./CustomTimestampHelper";
import * as DateTimeUtilities from "./DateTimeUtilities";
import QueryClauseViewModel from "./QueryClauseViewModel";
import QueryViewModel from "./QueryViewModel";
export default class QueryBuilderViewModel { export default class QueryBuilderViewModel {
/* Labels */ /* Labels */
@@ -182,7 +182,7 @@ export default class QueryBuilderViewModel {
value = `["${TableEntityProcessor.keyProperties.PartitionKey}"]`; value = `["${TableEntityProcessor.keyProperties.PartitionKey}"]`;
filterString = filterString.concat(filterString === "SELECT" ? " c" : ", c"); filterString = filterString.concat(filterString === "SELECT" ? " c" : ", c");
} else if (value === Constants.EntityKeyNames.RowKey) { } else if (value === Constants.EntityKeyNames.RowKey) {
value = `["${TableEntityProcessor.keyProperties.Id2}"]`; value = `["${TableEntityProcessor.keyProperties.Id}"]`;
filterString = filterString.concat(filterString === "SELECT" ? " c" : ", c"); filterString = filterString.concat(filterString === "SELECT" ? " c" : ", c");
} else { } else {
if (value === Constants.EntityKeyNames.Timestamp) { if (value === Constants.EntityKeyNames.Timestamp) {

View File

@@ -1,6 +1,6 @@
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import * as Entities from "./Entities";
import * as Constants from "./Constants"; import * as Constants from "./Constants";
import * as Entities from "./Entities";
import * as DateTimeUtilities from "./QueryBuilder/DateTimeUtilities"; import * as DateTimeUtilities from "./QueryBuilder/DateTimeUtilities";
// For use exclusively with Tables API. // For use exclusively with Tables API.
@@ -36,7 +36,7 @@ export function convertDocumentsToEntities(documents: any[]): Entities.ITableEnt
let results: Entities.ITableEntityForTablesAPI[] = []; let results: Entities.ITableEntityForTablesAPI[] = [];
documents && documents &&
documents.forEach((document) => { documents.forEach((document) => {
if (!document.hasOwnProperty(keyProperties.PartitionKey) || !document.hasOwnProperty(keyProperties.Id2)) { if (!document.hasOwnProperty(keyProperties.PartitionKey) || !document.hasOwnProperty(keyProperties.Id)) {
//Document does not match the current required format for Tables, so we ignore it //Document does not match the current required format for Tables, so we ignore it
return; // The rest of the key properties should be guaranteed as DocumentDB properties return; // The rest of the key properties should be guaranteed as DocumentDB properties
} }

View File

@@ -53,7 +53,6 @@
throughputAutoPilotRadioId: throughputAutoPilotRadioId, throughputAutoPilotRadioId: throughputAutoPilotRadioId,
throughputProvisionedRadioId: throughputProvisionedRadioId, throughputProvisionedRadioId: throughputProvisionedRadioId,
throughputModeRadioName: throughputModeRadioName, throughputModeRadioName: throughputModeRadioName,
showAutoPilot: userCanChangeProvisioningTypes,
isAutoPilotSelected: isAutoPilotSelected, isAutoPilotSelected: isAutoPilotSelected,
maxAutoPilotThroughputSet: autoPilotThroughput, maxAutoPilotThroughputSet: autoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost, autoPilotUsageCost: autoPilotUsageCost,

View File

@@ -1,23 +1,23 @@
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 SharedConstants from "../../Shared/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import DiscardIcon from "../../../images/discard.svg";
import editable from "../../Common/EditableUtility";
import Q from "q"; import Q from "q";
import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg"; import SaveIcon from "../../../images/save-cosmos.svg";
import TabsBase from "./TabsBase"; import * as Constants from "../../Common/Constants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import Explorer from "../Explorer";
import { updateOffer } from "../../Common/dataAccess/updateOffer"; import { updateOffer } from "../../Common/dataAccess/updateOffer";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import editable from "../../Common/EditableUtility";
import { configContext, Platform } from "../../ConfigContext";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { configContext, Platform } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import * as SharedConstants from "../../Shared/Constants";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../Utils/PricingUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import TabsBase from "./TabsBase";
const updateThroughputBeyondLimitWarningMessage: string = ` const updateThroughputBeyondLimitWarningMessage: string = `
You are about to request an increase in throughput beyond the pre-allocated capacity. You are about to request an increase in throughput beyond the pre-allocated capacity.
@@ -73,7 +73,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
public shouldShowStatusBar: ko.Computed<boolean>; public shouldShowStatusBar: ko.Computed<boolean>;
public throughputTitle: ko.PureComputed<string>; public throughputTitle: ko.PureComputed<string>;
public throughputAriaLabel: ko.PureComputed<string>; public throughputAriaLabel: ko.PureComputed<string>;
public userCanChangeProvisioningTypes: ko.Observable<boolean>;
public autoPilotUsageCost: ko.PureComputed<string>; public autoPilotUsageCost: ko.PureComputed<string>;
public warningMessage: ko.Computed<string>; public warningMessage: ko.Computed<string>;
public canExceedMaximumValue: ko.PureComputed<boolean>; public canExceedMaximumValue: ko.PureComputed<boolean>;
@@ -106,7 +105,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
this._wasAutopilotOriginallySet = ko.observable(false); this._wasAutopilotOriginallySet = ko.observable(false);
this.isAutoPilotSelected = editable.observable(false); this.isAutoPilotSelected = editable.observable(false);
this.autoPilotThroughput = editable.observable<number>(); this.autoPilotThroughput = editable.observable<number>();
this.userCanChangeProvisioningTypes = ko.observable(true);
const autoscaleMaxThroughput = this.database?.offer()?.autoscaleMaxThroughput; const autoscaleMaxThroughput = this.database?.offer()?.autoscaleMaxThroughput;
if (autoscaleMaxThroughput) { if (autoscaleMaxThroughput) {
@@ -118,9 +116,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
} }
this._hasProvisioningTypeChanged = ko.pureComputed<boolean>(() => { this._hasProvisioningTypeChanged = ko.pureComputed<boolean>(() => {
if (!this.userCanChangeProvisioningTypes()) {
return false;
}
if (this._wasAutopilotOriginallySet() !== this.isAutoPilotSelected()) { if (this._wasAutopilotOriginallySet() !== this.isAutoPilotSelected()) {
return true; return true;
} }
@@ -136,12 +131,11 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
}); });
this.requestUnitsUsageCost = ko.pureComputed(() => { this.requestUnitsUsageCost = ko.pureComputed(() => {
const account = this.container.databaseAccount(); const account = userContext.databaseAccount;
if (!account) { if (!account) {
return ""; return "";
} }
const serverId = this.container.serverId();
const regions = const regions =
(account && (account &&
account.properties && account.properties &&
@@ -155,14 +149,14 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
estimatedSpend = PricingUtils.getEstimatedSpendHtml( estimatedSpend = PricingUtils.getEstimatedSpendHtml(
// 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.autoPilotThroughput() : this.throughput(), this.overrideWithAutoPilotSettings() ? this.autoPilotThroughput() : this.throughput(),
serverId, userContext.portalEnv,
regions, regions,
multimaster multimaster
); );
} else { } else {
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.autoPilotThroughput(), this.autoPilotThroughput(),
serverId, userContext.portalEnv,
regions, regions,
multimaster multimaster
); );
@@ -362,7 +356,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
this.isTemplateReady = ko.observable<boolean>(false); this.isTemplateReady = ko.observable<boolean>(false);
this.isFreeTierAccount = ko.computed<boolean>(() => { this.isFreeTierAccount = ko.computed<boolean>(() => {
const databaseAccount = this.container?.databaseAccount(); const databaseAccount = userContext.databaseAccount;
return databaseAccount?.properties?.enableFreeTier; return databaseAccount?.properties?.enableFreeTier;
}); });
@@ -407,7 +401,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
this._setBaseline(); this._setBaseline();
this._wasAutopilotOriginallySet(this.isAutoPilotSelected()); this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
} catch (error) { } catch (error) {
this.container.isRefreshingExplorer(false);
this.isExecutionError(true); this.isExecutionError(true);
console.error(error); console.error(error);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
@@ -448,7 +441,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(offer.autoscaleMaxThroughput)); this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(offer.autoscaleMaxThroughput));
this.autoPilotThroughput.setBaseline(offer.autoscaleMaxThroughput); this.autoPilotThroughput.setBaseline(offer.autoscaleMaxThroughput);
this.throughput.setBaseline(offer.manualThroughput); this.throughput.setBaseline(offer.manualThroughput);
this.userCanChangeProvisioningTypes(true);
} }
protected getTabsButtons(): CommandButtonComponentProps[] { protected getTabsButtons(): CommandButtonComponentProps[] {

View File

@@ -36,7 +36,7 @@ export default class GalleryTab extends TabsBase {
galleryItem: options.galleryItem, galleryItem: options.galleryItem,
isFavorite: options.isFavorite, isFavorite: options.isFavorite,
selectedTab: options.selectedTab, selectedTab: options.selectedTab,
sortBy: SortBy.MostViewed, sortBy: SortBy.MostRecent,
searchText: undefined, searchText: undefined,
}; };
this.galleryAndNotebookViewerComponentAdapter = new GalleryAndNotebookViewerComponentAdapter( this.galleryAndNotebookViewerComponentAdapter = new GalleryAndNotebookViewerComponentAdapter(

View File

@@ -1,18 +1,16 @@
import * as Constants from "../../Common/Constants";
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels"; import * as Constants from "../../Common/Constants";
import AuthHeadersUtil from "../../Platform/Hosted/Authorization";
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import Q from "q";
import TabsBase from "./TabsBase";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { HashMap } from "../../Common/HashMap"; import { HashMap } from "../../Common/HashMap";
import { configContext, Platform } from "../../ConfigContext";
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 { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { userContext } from "../../UserContext"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { configContext, Platform } from "../../ConfigContext"; import TabsBase from "./TabsBase";
export default class MongoShellTab extends TabsBase { export default class MongoShellTab extends TabsBase {
public url: ko.Computed<string>; public url: ko.Computed<string>;
@@ -33,7 +31,7 @@ export default class MongoShellTab extends TabsBase {
this._runtimeEndpoint = configContext.platform === Platform.Hosted ? configContext.BACKEND_ENDPOINT : ""; this._runtimeEndpoint = configContext.platform === Platform.Hosted ? configContext.BACKEND_ENDPOINT : "";
const extensionEndpoint: string = configContext.BACKEND_ENDPOINT || this._runtimeEndpoint || ""; const extensionEndpoint: string = configContext.BACKEND_ENDPOINT || this._runtimeEndpoint || "";
let baseUrl = "/content/mongoshell/dist/"; let baseUrl = "/content/mongoshell/dist/";
if (this._container.serverId() === "localhost") { if (userContext.portalEnv === "localhost") {
baseUrl = "/content/mongoshell/"; baseUrl = "/content/mongoshell/";
} }
@@ -85,10 +83,10 @@ export default class MongoShellTab extends TabsBase {
} }
private handleReadyMessage(event: MessageEvent, shellIframe: HTMLIFrameElement) { private handleReadyMessage(event: MessageEvent, shellIframe: HTMLIFrameElement) {
if (typeof event.data["data"] !== "string") { if (typeof event.data["kind"] !== "string") {
return; return;
} }
if (event.data.data !== "ready") { if (event.data.kind !== "ready") {
return; return;
} }

View File

@@ -24,7 +24,7 @@ import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBa
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter"; import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
import { NotebookConfigurationUtils } from "../../Utils/NotebookConfigurationUtils"; import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils";
import { KernelSpecsDisplay, NotebookClientV2 } from "../Notebook/NotebookClientV2"; import { KernelSpecsDisplay, NotebookClientV2 } from "../Notebook/NotebookClientV2";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import Explorer from "../Explorer"; import Explorer from "../Explorer";

View File

@@ -4,18 +4,24 @@ import * as _ from "underscore";
import UploadWorker from "worker-loader!../../workers/upload"; import UploadWorker from "worker-loader!../../workers/upload";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures"; import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures";
import { readTriggers } from "../../Common/dataAccess/readTriggers"; import { readTriggers } from "../../Common/dataAccess/readTriggers";
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions"; import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import { configContext, Platform } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { StartUploadMessageParams, UploadDetails, UploadDetailsRecord } from "../../workers/upload/definitions"; import { StartUploadMessageParams, UploadDetails, UploadDetailsRecord } from "../../workers/upload/definitions";
import Explorer from "../Explorer";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient"; import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
import ConflictsTab from "../Tabs/ConflictsTab"; import ConflictsTab from "../Tabs/ConflictsTab";
@@ -32,12 +38,6 @@ import DocumentId from "./DocumentId";
import StoredProcedure from "./StoredProcedure"; import StoredProcedure from "./StoredProcedure";
import Trigger from "./Trigger"; import Trigger from "./Trigger";
import UserDefinedFunction from "./UserDefinedFunction"; import UserDefinedFunction from "./UserDefinedFunction";
import { configContext, Platform } from "../../ConfigContext";
import Explorer from "../Explorer";
import { userContext } from "../../UserContext";
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { createDocument } from "../../Common/dataAccess/createDocument";
export default class Collection implements ViewModels.Collection { export default class Collection implements ViewModels.Collection {
public nodeKind: string; public nodeKind: string;
@@ -1200,7 +1200,6 @@ export default class Collection implements ViewModels.Collection {
public async loadOffer(): Promise<void> { public async loadOffer(): Promise<void> {
if (!this.container.isServerlessEnabled() && !this.offer()) { if (!this.container.isServerlessEnabled() && !this.offer()) {
this.container.isRefreshingExplorer(true);
const startKey: number = TelemetryProcessor.traceStart(Action.LoadOffers, { const startKey: number = TelemetryProcessor.traceStart(Action.LoadOffers, {
databaseName: this.databaseId, databaseName: this.databaseId,
collectionName: this.id(), collectionName: this.id(),
@@ -1237,8 +1236,6 @@ export default class Collection implements ViewModels.Collection {
startKey startKey
); );
throw error; throw error;
} finally {
this.container.isRefreshingExplorer(false);
} }
} }
} }

View File

@@ -6,7 +6,7 @@ import { TreeComponent, TreeNode, TreeNodeMenuItem, TreeNodeComponent } from "..
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory"; import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import CollectionIcon from "../../../images/tree-collection.svg"; import CollectionIcon from "../../../images/tree-collection.svg";
@@ -264,15 +264,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
onClick: () => { onClick: () => {
collection.openTab(); collection.openTab();
// push to most recent // push to most recent
MostRecentActivity.mostRecentActivity.addItem(userContext.databaseAccount?.id, { mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
type: MostRecentActivity.Type.OpenCollection,
title: collection.id(),
description: "Data",
data: {
databaseId: collection.databaseId,
collectionId: collection.id(),
},
});
}, },
isSelected: () => isSelected: () =>
this.isDataNodeSelected(collection.databaseId, collection.id(), [ this.isDataNodeSelected(collection.databaseId, collection.id(), [
@@ -573,7 +565,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
(item: NotebookContentItem) => { (item: NotebookContentItem) => {
this.container.openNotebook(item).then((hasOpened) => { this.container.openNotebook(item).then((hasOpened) => {
if (hasOpened) { if (hasOpened) {
this.pushItemToMostRecent(item); mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
} }
}); });
}, },
@@ -594,7 +586,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
(item: NotebookContentItem) => { (item: NotebookContentItem) => {
this.container.openNotebook(item).then((hasOpened) => { this.container.openNotebook(item).then((hasOpened) => {
if (hasOpened) { if (hasOpened) {
this.pushItemToMostRecent(item); mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
} }
}); });
}, },
@@ -624,18 +616,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
return gitHubNotebooksTree; return gitHubNotebooksTree;
} }
private pushItemToMostRecent(item: NotebookContentItem) {
MostRecentActivity.mostRecentActivity.addItem(userContext.databaseAccount?.id, {
type: MostRecentActivity.Type.OpenNotebook,
title: item.name,
description: "Notebook",
data: {
name: item.name,
path: item.path,
},
});
}
private buildChildNodes( private buildChildNodes(
item: NotebookContentItem, item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void, onFileClick: (item: NotebookContentItem) => void,

View File

@@ -1,5 +1,5 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import * as React from "react"; import * as React from "react";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { NotebookContentItem } from "../Notebook/NotebookContentItem"; import { NotebookContentItem } from "../Notebook/NotebookContentItem";
@@ -44,15 +44,7 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
onClick: () => { onClick: () => {
collection.onDocumentDBDocumentsClick(); collection.onDocumentDBDocumentsClick();
// push to most recent // push to most recent
MostRecentActivity.mostRecentActivity.addItem(userContext.databaseAccount?.id, { mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
type: MostRecentActivity.Type.OpenCollection,
title: collection.id(),
description: "Data",
data: {
databaseId: collection.databaseId,
collectionId: collection.id(),
},
});
}, },
isSelected: () => isSelected: () =>
this.isDataNodeSelected(collection.databaseId, collection.id(), ViewModels.CollectionTabKind.Documents), this.isDataNodeSelected(collection.databaseId, collection.id(), ViewModels.CollectionTabKind.Documents),

View File

@@ -27,7 +27,7 @@ const onInit = async () => {
const props: GalleryAndNotebookViewerComponentProps = { const props: GalleryAndNotebookViewerComponentProps = {
junoClient: new JunoClient(), junoClient: new JunoClient(),
selectedTab: galleryViewerProps.selectedTab || GalleryTab.PublicGallery, selectedTab: galleryViewerProps.selectedTab || GalleryTab.PublicGallery,
sortBy: galleryViewerProps.sortBy || SortBy.MostViewed, sortBy: galleryViewerProps.sortBy || SortBy.MostRecent,
searchText: galleryViewerProps.searchText, searchText: galleryViewerProps.searchText,
}; };
@@ -36,7 +36,7 @@ const onInit = async () => {
<header> <header>
<GalleryHeaderComponent /> <GalleryHeaderComponent />
</header> </header>
<div style={{ marginLeft: 138, marginRight: 138 }}> <div style={{ margin: "auto", width: "85%" }}>
<div style={{ paddingLeft: 26, paddingRight: 26, paddingTop: 20 }}> <div style={{ paddingLeft: 26, paddingRight: 26, paddingTop: 20 }}>
<Text block> <Text block>
Welcome to the Azure Cosmos DB notebooks gallery! View the sample notebooks to learn about use cases, best Welcome to the Azure Cosmos DB notebooks gallery! View the sample notebooks to learn about use cases, best

View File

@@ -1,7 +1,7 @@
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import { HttpStatusCodes } from "../Common/Constants"; import { HttpStatusCodes } from "../Common/Constants";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import UrlUtility from "../Common/UrlUtility"; import * as UrlUtility from "../Common/UrlUtility";
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil"; import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
import { getErrorMessage } from "../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../Common/ErrorHandlingUtils";

View File

@@ -8,7 +8,7 @@ import * as Logger from "../Common/Logger";
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil"; import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
import { GitHubClient, IGitHubFile, IGitHubResponse } from "./GitHubClient"; import { GitHubClient, IGitHubFile, IGitHubResponse } from "./GitHubClient";
import * as GitHubUtils from "../Utils/GitHubUtils"; import * as GitHubUtils from "../Utils/GitHubUtils";
import UrlUtility from "../Common/UrlUtility"; import * as UrlUtility from "../Common/UrlUtility";
import { getErrorMessage } from "../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../Common/ErrorHandlingUtils";
export interface GitHubContentProviderParams { export interface GitHubContentProviderParams {

View File

@@ -9,9 +9,11 @@
"North Central US": "North Central US", "North Central US": "North Central US",
"West US": "West US", "West US": "West US",
"East US 2": "East US 2", "East US 2": "East US 2",
"ClassInfo": "This is a self serve class", "Current Region": "Current Region",
"RegionDropdownInfo": "More regions can be added in the future.", "RegionDropdownInfo": "More regions can be added in the future.",
"ValidationError": "Regions and AccountName should not be empty.", "RegionsAndAccountNameValidationError": "Regions and account name should not be empty.",
"DbThroughputValidationError": "Please update throughput for database.",
"DescriptionLabel": "Description",
"DescriptionText": "This class sets collection and database throughput.", "DescriptionText": "This class sets collection and database throughput.",
"DecriptionLinkText": "Click here for more information", "DecriptionLinkText": "Click here for more information",
"Regions": "Regions", "Regions": "Regions",
@@ -22,12 +24,69 @@
"Account Name": "Account Name", "Account Name": "Account Name",
"AccountNamePlaceHolder": "Enter the account name", "AccountNamePlaceHolder": "Enter the account name",
"Collection Throughput": "Collection Throughput", "Collection Throughput": "Collection Throughput",
"Enable DB level throughput": "Enable DB level throughput", "Enable DB level throughput": "Enable Database Level Throughput",
"Database Throughput": "Database Throughput", "Database Throughput": "Database Throughput",
"RefreshMessage": "Self Serve Example successfully refreshing", "UpdateInProgressMessage": "Data is being updated",
"SubmissionMessage": "Submitted successfully" "UpdateCompletedMessageTitle": "Update succeeded",
"UpdateCompletedMessageText": "Data update completed.",
"SubmissionMessageSuccessTitle": "Update started",
"SubmissionMessageForNewRegionText": "Data update started. Region changed.",
"SubmissionMessageForSameRegionText": "Data update started. Region not changed.",
"SubmissionMessageErrorTitle": "Data update failed",
"SubmissionMessageErrorText": "Data update failed because of errors.",
"OnSaveFailureMessage": "Data save operation not currently permitted."
}, },
"SqlX": { "SqlX": {
"DedicatedGatewayDescription": "Provision a dedicated gateway cluster for your Azure Cosmos DB account. A dedicated gateway is compute that is a front-end to data in your Azure Cosmos DB account. Your dedicated gateway automatically includes the integrated cache, which can improve read performance. ",
"DedicatedGateway": "Dedicated Gateway",
"Enable": "Enable",
"Disable": "Disable",
"LearnAboutDedicatedGateway": "Learn more about dedicated gateway.",
"DeprovisioningDetailsText": "Learn more about deprovisioning the dedicated gateway.",
"DedicatedGatewayPricing": "Learn more about dedicated gateway pricing",
"SKUs": "SKUs",
"SKUsPlaceHolder": "Select SKUs",
"NumberOfInstances": "Number of instances",
"CosmosD4s": "Cosmos.D4s (General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory)",
"CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)",
"CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)",
"CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)",
"CreateMessage": "Dedicated gateway resource is being created.",
"CreateInitializeTitle": "Provisioning resource",
"CreateInitializeMessage": "Dedicated gateway resource will be provisioned.",
"CreateSuccessTitle": "Resource provisioned",
"CreateSuccesseMessage": "Dedicated gateway resource provisioned.",
"CreateFailureTitle": "Failed to provision resource",
"CreateFailureMessage": "Dedicated gateway resource provisioning failed.",
"UpdateMessage": "Dedicated gateway resource is being updated.",
"UpdateInitializeTitle": "Updating resource",
"UpdateInitializeMessage": "Dedicated gateway resource will be updated.",
"UpdateSuccessTitle": "Resource updated",
"UpdateSuccesseMessage": "Dedicated gateway resource updated.",
"UpdateFailureTitle": "Failed to update resource",
"UpdateFailureMessage": "Dedicated gateway resource updation failed.",
"DeleteMessage": "Dedicated gateway resource is being deleted.",
"DeleteInitializeTitle": "Deleting resource",
"DeleteInitializeMessage": "Dedicated gateway resource will be deleted.",
"DeleteSuccessTitle": "Resource deleted",
"DeleteSuccesseMessage": "Dedicated gateway resource deleted.",
"DeleteFailureTitle": "Failed to delete resource",
"DeleteFailureMessage": "Dedicated gateway resource deletion failed.",
"CannotSave": "Cannot save the changes to the Dedicated gateway resource at the moment",
"DedicatedGatewayEndpoint": "Dedicated gatewayEndpoint",
"NoValue": "",
"SKUDetails": "SKU Details: ",
"CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory",
"CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory",
"CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory",
"CosmosD32Details": "General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory",
"Cost": "Cost",
"CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.",
"ConnectionString": "Connection String",
"ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ",
"KeysBlade": "the keys blade",
"WarningBannerOnUpdate": "Adding or modifying dedicated gateway instances may affect your bill.",
"WarningBannerOnDelete": "After deprovisioning the dedicated gateway, you must update any applications using the old dedicated gateway connection string."
} }
} }
} }

View File

@@ -1,74 +1,73 @@
// CSS Dependencies // CSS Dependencies
import "abort-controller/polyfill";
import "babel-polyfill";
import "bootstrap/dist/css/bootstrap.css"; import "bootstrap/dist/css/bootstrap.css";
import "../less/documentDB.less"; import "es6-object-assign/auto";
import "../less/tree.less"; import "es6-symbol/implement";
import "../less/forms.less"; import "object.entries/auto";
import "../less/menus.less"; import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import "../less/infobox.less"; import "promise-polyfill/src/polyfill";
import "../less/messagebox.less"; import "promise.prototype.finally/auto";
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less"; import React, { useState } from "react";
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less"; import ReactDOM from "react-dom";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; import "url-polyfill/url-polyfill.min";
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less"; import "webcrypto-liner/build/webcrypto-liner.shim.min";
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less"; import "whatwg-fetch";
import "./Explorer/Controls/DynamicList/DynamicListComponent.less";
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "./Explorer/Panes/PanelComponent.less";
import "../less/TableStyles/queryBuilder.less";
import "../externals/jquery.dataTables.min.css";
import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/EntityEditor.less";
import "../less/TableStyles/CustomizeColumns.less";
import "../less/resourceTree.less";
import "../externals/jquery.typeahead.min.css";
import "../externals/jquery-ui.min.css"; import "../externals/jquery-ui.min.css";
import "../externals/jquery-ui.min.js";
import "../externals/jquery-ui.structure.min.css"; import "../externals/jquery-ui.structure.min.css";
import "../externals/jquery-ui.theme.min.css"; import "../externals/jquery-ui.theme.min.css";
import "./Explorer/Graph/NewVertexComponent/newVertexComponent.less"; import "../externals/jquery.dataTables.min.css";
import "./Explorer/Panes/GraphNewVertexPane.less"; import "../externals/jquery.typeahead.min.css";
import "./Explorer/Tabs/QueryTab.less"; import "../externals/jquery.typeahead.min.js";
import "./Explorer/Controls/TreeComponent/treeComponent.less";
import "./Explorer/Controls/Accordion/AccordionComponent.less";
import "./Explorer/SplashScreen/SplashScreen.less";
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
// Image Dependencies // Image Dependencies
import "../images/CosmosDB_rgb_ui_lighttheme.ico"; import "../images/CosmosDB_rgb_ui_lighttheme.ico";
import "../images/favicon.ico"; import "../images/favicon.ico";
import "./Shared/appInsights";
import "babel-polyfill";
import "es6-symbol/implement";
import "webcrypto-liner/build/webcrypto-liner.shim.min";
import "./Libs/jquery";
import "bootstrap/dist/js/npm";
import "../externals/jquery.typeahead.min.js";
import "../externals/jquery-ui.min.js";
import "promise-polyfill/src/polyfill";
import "abort-controller/polyfill";
import "whatwg-fetch";
import "es6-object-assign/auto";
import "promise.prototype.finally/auto";
import "object.entries/auto";
import "./Libs/is-integer-polyfill";
import "url-polyfill/url-polyfill.min";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import { ExplorerParams } from "./Explorer/Explorer";
import React, { useState } from "react";
import ReactDOM from "react-dom";
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
import refreshImg from "../images/refresh-cosmos.svg";
import arrowLeftImg from "../images/imgarrowlefticon.svg"; import arrowLeftImg from "../images/imgarrowlefticon.svg";
import { KOCommentEnd, KOCommentIfStart } from "./koComment"; import refreshImg from "../images/refresh-cosmos.svg";
import "../less/documentDB.less";
import "../less/forms.less";
import "../less/infobox.less";
import "../less/menus.less";
import "../less/messagebox.less";
import "../less/resourceTree.less";
import "../less/TableStyles/CustomizeColumns.less";
import "../less/TableStyles/EntityEditor.less";
import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/queryBuilder.less";
import "../less/tree.less";
import { AuthType } from "./AuthType";
import "./Explorer/Controls/Accordion/AccordionComponent.less";
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
import { Dialog, DialogProps } from "./Explorer/Controls/Dialog";
import "./Explorer/Controls/DynamicList/DynamicListComponent.less";
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
import "./Explorer/Controls/ThroughputInput/ThroughputInput.less";
import "./Explorer/Controls/TreeComponent/treeComponent.less";
import { ExplorerParams } from "./Explorer/Explorer";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "./Explorer/Graph/NewVertexComponent/newVertexComponent.less";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
import { NotificationConsoleComponent } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import "./Explorer/Panes/GraphNewVertexPane.less";
import "./Explorer/Panes/PanelComponent.less";
import { PanelContainerComponent } from "./Explorer/Panes/PanelContainerComponent";
import { SplashScreen } from "./Explorer/SplashScreen/SplashScreen";
import "./Explorer/SplashScreen/SplashScreen.less";
import "./Explorer/Tabs/QueryTab.less";
import { useConfig } from "./hooks/useConfig"; import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer"; import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import { useSidePanel } from "./hooks/useSidePanel"; import { useSidePanel } from "./hooks/useSidePanel";
import { NotificationConsoleComponent } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { KOCommentEnd, KOCommentIfStart } from "./koComment";
import { PanelContainerComponent } from "./Explorer/Panes/PanelContainerComponent"; import "./Libs/is-integer-polyfill";
import { SplashScreen } from "./Explorer/SplashScreen/SplashScreen"; import "./Libs/jquery";
import { Dialog, DialogProps } from "./Explorer/Controls/Dialog"; import "./Shared/appInsights";
import { userContext } from "./UserContext";
initializeIcons(); initializeIcons();
@@ -103,19 +102,14 @@ const App: React.FunctionComponent = () => {
const config = useConfig(); const config = useConfig();
const explorer = useKnockoutExplorer(config?.platform, explorerParams); const explorer = useKnockoutExplorer(config?.platform, explorerParams);
if (!explorer) {
return <LoadingExplorer />;
}
return ( return (
<div className="flexContainer"> <div className="flexContainer">
<div <div id="divExplorer" className="flexContainer hideOverflows" style={{ display: "none" }}>
id="divSelfServe" {/* Main Command Bar - Start */}
className="flexContainer"
data-bind="visible: selfServeType() && selfServeType() !== 'none', react: selfServeComponentAdapter"
></div>
<div
id="divExplorer"
data-bind="if: selfServeType() === 'none'"
className="flexContainer hideOverflows"
style={{ display: "none" }}
>
<div data-bind="react: commandBarComponentAdapter" /> <div data-bind="react: commandBarComponentAdapter" />
{/* Collections Tree and Tabs - Begin */} {/* Collections Tree and Tabs - Begin */}
<div className="resourceTreeAndTabs"> <div className="resourceTreeAndTabs">
@@ -163,11 +157,11 @@ const App: React.FunctionComponent = () => {
</div> </div>
</div> </div>
</div> </div>
<div {userContext.authType === AuthType.ResourceToken ? (
style={{ overflowY: "auto" }} <div style={{ overflowY: "auto" }} data-bind="react:resourceTreeForResourceToken" />
data-bind="if: isAuthWithResourceToken(), react:resourceTreeForResourceToken" ) : (
/> <div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
<div style={{ overflowY: "auto" }} data-bind="if: !isAuthWithResourceToken(), react:resourceTree" /> )}
</div> </div>
{/* Collections Window - End */} {/* Collections Window - End */}
</div> </div>
@@ -217,10 +211,7 @@ const App: React.FunctionComponent = () => {
{/* Splitter - End */} {/* Splitter - End */}
</div> </div>
{/* Collections Tree - End */} {/* Collections Tree - End */}
<div <div className="connectExplorerContainer" data-bind="visible: tabsManager.openedTabs().length === 0">
className="connectExplorerContainer"
data-bind="visible: !isRefreshingExplorer() && tabsManager.openedTabs().length === 0"
>
<form className="connectExplorerFormContainer"> <form className="connectExplorerFormContainer">
<SplashScreen explorer={explorer} /> <SplashScreen explorer={explorer} />
</form> </form>
@@ -245,25 +236,6 @@ const App: React.FunctionComponent = () => {
/> />
</div> </div>
</div> </div>
{/* Global loader - Start */}
<div className="splashLoaderContainer" data-bind="visible: isRefreshingExplorer">
<div className="splashLoaderContentContainer">
<div data-bind="visible: selfServeType() === undefined, react: selfServeLoadingComponentAdapter"></div>
<div data-bind="if: selfServeType() === 'none'" style={{ display: "none" }}>
<p className="connectExplorerContent">
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
</p>
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle">
Welcome to Azure Cosmos DB
</p>
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert">
Connecting...
</p>
</div>
</div>
</div>
{/* Global loader - End */}
<PanelContainerComponent <PanelContainerComponent
isOpen={isPanelOpen} isOpen={isPanelOpen}
panelContent={panelContent} panelContent={panelContent}
@@ -307,3 +279,21 @@ const App: React.FunctionComponent = () => {
}; };
ReactDOM.render(<App />, document.body); ReactDOM.render(<App />, document.body);
function LoadingExplorer(): JSX.Element {
return (
<div className="splashLoaderContainer">
<div className="splashLoaderContentContainer">
<p className="connectExplorerContent">
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
</p>
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle">
Welcome to Azure Cosmos DB
</p>
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert">
Connecting...
</p>
</div>
</div>
);
}

View File

@@ -3,7 +3,7 @@ import { HttpStatusCodes } from "../Common/Constants";
import { IResourceProviderClient, IResourceProviderRequestOptions } from "./IResourceProviderClient"; import { IResourceProviderClient, IResourceProviderRequestOptions } from "./IResourceProviderClient";
import { OperationStatus } from "../Contracts/DataModels"; import { OperationStatus } from "../Contracts/DataModels";
import { TokenProviderFactory } from "../TokenProviders/TokenProviderFactory"; import { TokenProviderFactory } from "../TokenProviders/TokenProviderFactory";
import UrlUtility from "../Common/UrlUtility"; import * as UrlUtility from "../Common/UrlUtility";
export class ResourceProviderClient<T> implements IResourceProviderClient<T> { export class ResourceProviderClient<T> implements IResourceProviderClient<T> {
private httpClient: HttpClient; private httpClient: HttpClient;

View File

@@ -1,9 +1,8 @@
import crossroads from "crossroads";
import hasher from "hasher";
import * as _ from "underscore"; import * as _ from "underscore";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import crossroads from "crossroads";
import hasher from "hasher";
import ScriptTabBase from "../Explorer/Tabs/ScriptTabBase"; import ScriptTabBase from "../Explorer/Tabs/ScriptTabBase";
import TabsBase from "../Explorer/Tabs/TabsBase"; import TabsBase from "../Explorer/Tabs/TabsBase";
@@ -398,14 +397,7 @@ export class TabRouteHandler {
private _executeActionHelper(action: () => void): void { private _executeActionHelper(action: () => void): void {
const explorer = window.dataExplorer; const explorer = window.dataExplorer;
if (!!explorer && (explorer.isRefreshingExplorer() || !explorer.isAccountReady())) { if (explorer && explorer.isAccountReady()) {
const refreshSubscription = explorer.isRefreshingExplorer.subscribe((isRefreshing: boolean) => {
if (!isRefreshing) {
action();
refreshSubscription.dispose();
}
});
} else {
action(); action();
} }
} }

View File

@@ -1,4 +1,4 @@
import { ChoiceItem, Description, Info, InputType, NumberUiType, SmartUiInput } from "./SelfServeTypes"; import { ChoiceItem, Description, Info, InputType, NumberUiType, SmartUiInput, RefreshParams } from "./SelfServeTypes";
import { addPropertyToMap, DecoratorProperties, buildSmartUiDescriptor } from "./SelfServeUtils"; import { addPropertyToMap, DecoratorProperties, buildSmartUiDescriptor } from "./SelfServeUtils";
type ValueOf<T> = T[keyof T]; type ValueOf<T> = T[keyof T];
@@ -33,7 +33,9 @@ export interface ChoiceInputOptions extends InputOptionsBase {
} }
export interface DescriptionDisplayOptions { export interface DescriptionDisplayOptions {
labelTKey?: string;
description?: (() => Promise<Description>) | Description; description?: (() => Promise<Description>) | Description;
isDynamicDescription?: boolean;
} }
type InputOptions = type InputOptions =
@@ -56,7 +58,7 @@ const isChoiceInputOptions = (inputOptions: InputOptions): inputOptions is Choic
}; };
const isDescriptionDisplayOptions = (inputOptions: InputOptions): inputOptions is DescriptionDisplayOptions => { const isDescriptionDisplayOptions = (inputOptions: InputOptions): inputOptions is DescriptionDisplayOptions => {
return "description" in inputOptions; return "description" in inputOptions || "isDynamicDescription" in inputOptions;
}; };
const addToMap = (...decorators: Decorator[]): PropertyDecorator => { const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
@@ -80,7 +82,11 @@ const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
}; };
export const OnChange = ( export const OnChange = (
onChange: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput> onChange: (
newValue: InputType,
currentState: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
) => Map<string, SmartUiInput>
): PropertyDecorator => { ): PropertyDecorator => {
return addToMap({ name: "onChange", value: onChange }); return addToMap({ name: "onChange", value: onChange });
}; };
@@ -111,7 +117,11 @@ export const Values = (inputOptions: InputOptions): PropertyDecorator => {
{ name: "choices", value: inputOptions.choices } { name: "choices", value: inputOptions.choices }
); );
} else if (isDescriptionDisplayOptions(inputOptions)) { } else if (isDescriptionDisplayOptions(inputOptions)) {
return addToMap({ name: "description", value: inputOptions.description }); return addToMap(
{ name: "labelTKey", value: inputOptions.labelTKey },
{ name: "description", value: inputOptions.description },
{ name: "isDynamicDescription", value: inputOptions.isDynamicDescription }
);
} else { } else {
return addToMap( return addToMap(
{ name: "labelTKey", value: inputOptions.labelTKey }, { name: "labelTKey", value: inputOptions.labelTKey },
@@ -126,8 +136,8 @@ export const IsDisplayable = (): ClassDecorator => {
}; };
}; };
export const ClassInfo = (info: (() => Promise<Info>) | Info): ClassDecorator => { export const RefreshOptions = (refreshParams: RefreshParams): ClassDecorator => {
return (target) => { return (target) => {
addPropertyToMap(target.prototype, "root", target.name, "info", info); addPropertyToMap(target.prototype, "root", target.name, "refreshParams", refreshParams);
}; };
}; };

View File

@@ -64,13 +64,20 @@ export const initialize = async (): Promise<InitializeResponse> => {
}; };
export const onRefreshSelfServeExample = async (): Promise<RefreshResult> => { export const onRefreshSelfServeExample = async (): Promise<RefreshResult> => {
const refreshCountString = SessionStorageUtility.getEntry("refreshCount");
const refreshCount = refreshCountString ? parseInt(refreshCountString) : 0;
const subscriptionId = userContext.subscriptionId; const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup; const resourceGroup = userContext.resourceGroup;
const databaseAccountName = userContext.databaseAccount.name; const databaseAccountName = userContext.databaseAccount.name;
const databaseAccountGetResults = await get(subscriptionId, resourceGroup, databaseAccountName); const databaseAccountGetResults = await get(subscriptionId, resourceGroup, databaseAccountName);
const isUpdateInProgress = databaseAccountGetResults.properties.provisioningState !== "Succeeded"; const isUpdateInProgress = databaseAccountGetResults.properties.provisioningState !== "Succeeded";
const progressToBeSent = refreshCount % 5 === 0 ? isUpdateInProgress : true;
SessionStorageUtility.setEntry("refreshCount", (refreshCount + 1).toString());
return { return {
isUpdateInProgress: isUpdateInProgress, isUpdateInProgress: progressToBeSent,
notificationMessage: "RefreshMessage", updateInProgressMessageTKey: "UpdateInProgressMessage",
}; };
}; };

View File

@@ -1,24 +1,25 @@
import { PropertyInfo, OnChange, Values, IsDisplayable, ClassInfo } from "../Decorators"; import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators";
import { import {
ChoiceItem, ChoiceItem,
Description,
DescriptionType,
Info, Info,
InputType, InputType,
NumberUiType, NumberUiType,
OnSaveResult,
RefreshResult, RefreshResult,
SelfServeBaseClass, SelfServeBaseClass,
SelfServeNotification,
SelfServeNotificationType,
SmartUiInput, SmartUiInput,
} from "../SelfServeTypes"; } from "../SelfServeTypes";
import { import {
getMaxCollectionThroughput,
getMaxDatabaseThroughput,
getMinCollectionThroughput,
getMinDatabaseThroughput,
initialize,
onRefreshSelfServeExample, onRefreshSelfServeExample,
Regions, Regions,
update, update,
initialize,
getMinDatabaseThroughput,
getMaxDatabaseThroughput,
getMinCollectionThroughput,
getMaxCollectionThroughput,
} from "./SelfServeExample.rp"; } from "./SelfServeExample.rp";
const regionDropdownItems: ChoiceItem[] = [ const regionDropdownItems: ChoiceItem[] = [
@@ -27,16 +28,19 @@ const regionDropdownItems: ChoiceItem[] = [
{ label: "East US 2", key: Regions.EastUS2 }, { label: "East US 2", key: Regions.EastUS2 },
]; ];
const selfServeExampleInfo: Info = {
messageTKey: "ClassInfo",
};
const regionDropdownInfo: Info = { const regionDropdownInfo: Info = {
messageTKey: "RegionDropdownInfo", messageTKey: "RegionDropdownInfo",
}; };
const onRegionsChange = (currentState: Map<string, SmartUiInput>, newValue: InputType): Map<string, SmartUiInput> => { const onRegionsChange = (newValue: InputType, currentState: Map<string, SmartUiInput>): Map<string, SmartUiInput> => {
currentState.set("regions", { value: newValue }); currentState.set("regions", { value: newValue });
const currentRegionText = `current region selected is ${newValue}`;
currentState.set("currentRegionText", {
value: { textTKey: currentRegionText, type: DescriptionType.Text } as Description,
hidden: false,
});
const currentEnableLogging = currentState.get("enableLogging"); const currentEnableLogging = currentState.get("enableLogging");
if (newValue === Regions.NorthCentralUS) { if (newValue === Regions.NorthCentralUS) {
currentState.set("enableLogging", { value: false, disabled: true }); currentState.set("enableLogging", { value: false, disabled: true });
@@ -47,8 +51,8 @@ const onRegionsChange = (currentState: Map<string, SmartUiInput>, newValue: Inpu
}; };
const onEnableDbLevelThroughputChange = ( const onEnableDbLevelThroughputChange = (
currentState: Map<string, SmartUiInput>, newValue: InputType,
newValue: InputType currentState: Map<string, SmartUiInput>
): Map<string, SmartUiInput> => { ): Map<string, SmartUiInput> => {
currentState.set("enableDbLevelThroughput", { value: newValue }); currentState.set("enableDbLevelThroughput", { value: newValue });
const currentDbThroughput = currentState.get("dbThroughput"); const currentDbThroughput = currentState.get("dbThroughput");
@@ -57,9 +61,15 @@ const onEnableDbLevelThroughputChange = (
return currentState; return currentState;
}; };
const validate = (currentvalues: Map<string, SmartUiInput>): void => { const validate = (
currentvalues: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
): void => {
if (currentvalues.get("dbThroughput") === baselineValues.get("dbThroughput")) {
throw new Error("DbThroughputValidationError");
}
if (!currentvalues.get("regions").value || !currentvalues.get("accountName").value) { if (!currentvalues.get("regions").value || !currentvalues.get("accountName").value) {
throw new Error("ValidationError"); throw new Error("RegionsAndAccountNameValidationError");
} }
}; };
@@ -86,12 +96,12 @@ const validate = (currentvalues: Map<string, SmartUiInput>): void => {
*/ */
@IsDisplayable() @IsDisplayable()
/* /*
@ClassInfo() @RefreshOptions()
- optional - role: Passes the refresh options to be used by the self serve model.
- input: Info | () => Promise<Info> - inputs:
- role: Display an Info bar as the first element of the UI. retryIntervalInMs - The time interval between refresh attempts when an update in ongoing.
*/ */
@ClassInfo(selfServeExampleInfo) @RefreshOptions({ retryIntervalInMs: 2000 })
export default class SelfServeExample extends SelfServeBaseClass { export default class SelfServeExample extends SelfServeBaseClass {
/* /*
onRefresh() onRefresh()
@@ -109,18 +119,21 @@ export default class SelfServeExample extends SelfServeBaseClass {
/* /*
onSave() onSave()
- input: (currentValues: Map<string, InputType>) => Promise<void> - input: (currentValues: Map<string, InputType>, baselineValues: ReadonlyMap<string, SmartUiInput>) => Promise<string>
- role: Callback that is triggerred when the submit button is clicked. You should perform your rest API - role: Callback that is triggerred when the submit button is clicked. You should perform your rest API
calls here using the data from the different inputs passed as a Map to this callback function. calls here using the data from the different inputs passed as a Map to this callback function.
In this example, the onSave callback simply sets the value for keys corresponding to the field name In this example, the onSave callback simply sets the value for keys corresponding to the field name
in the SessionStorage. in the SessionStorage. It uses the currentValues and baselineValues maps to perform custom validations
- returns: SelfServeNotification - as well.
message: The message to be displayed in the message bar after the onSave is completed
type: The type of message bar to be used (info, warning, error) - returns: The initialize, success and failure messages to be displayed in the Portal Notification blade after the operation is completed.
*/ */
public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<SelfServeNotification> => { public onSave = async (
validate(currentValues); currentValues: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
): Promise<OnSaveResult> => {
validate(currentValues, baselineValues);
const regions = Regions[currentValues.get("regions")?.value as keyof typeof Regions]; const regions = Regions[currentValues.get("regions")?.value as keyof typeof Regions];
const enableLogging = currentValues.get("enableLogging")?.value as boolean; const enableLogging = currentValues.get("enableLogging")?.value as boolean;
const accountName = currentValues.get("accountName")?.value as string; const accountName = currentValues.get("accountName")?.value as string;
@@ -128,8 +141,48 @@ export default class SelfServeExample extends SelfServeBaseClass {
const enableDbLevelThroughput = currentValues.get("enableDbLevelThroughput")?.value as boolean; const enableDbLevelThroughput = currentValues.get("enableDbLevelThroughput")?.value as boolean;
let dbThroughput = currentValues.get("dbThroughput")?.value as number; let dbThroughput = currentValues.get("dbThroughput")?.value as number;
dbThroughput = enableDbLevelThroughput ? dbThroughput : undefined; dbThroughput = enableDbLevelThroughput ? dbThroughput : undefined;
await update(regions, enableLogging, accountName, collectionThroughput, dbThroughput); try {
return { message: "SubmissionMessage", type: SelfServeNotificationType.info }; await update(regions, enableLogging, accountName, collectionThroughput, dbThroughput);
if (currentValues.get("regions") === baselineValues.get("regions")) {
return {
operationStatusUrl: undefined,
portalNotification: {
initialize: {
titleTKey: "SubmissionMessageSuccessTitle",
messageTKey: "SubmissionMessageForSameRegionText",
},
success: {
titleTKey: "UpdateCompletedMessageTitle",
messageTKey: "UpdateCompletedMessageText",
},
failure: {
titleTKey: "SubmissionMessageErrorTitle",
messageTKey: "SubmissionMessageErrorText",
},
},
};
} else {
return {
operationStatusUrl: undefined,
portalNotification: {
initialize: {
titleTKey: "SubmissionMessageSuccessTitle",
messageTKey: "SubmissionMessageForNewRegionText",
},
success: {
titleTKey: "UpdateCompletedMessageTitle",
messageTKey: "UpdateCompletedMessageText",
},
failure: {
titleTKey: "SubmissionMessageErrorTitle",
messageTKey: "SubmissionMessageErrorText",
},
},
};
}
} catch (error) {
throw new Error("OnSaveFailureMessage");
}
}; };
/* /*
@@ -150,6 +203,7 @@ export default class SelfServeExample extends SelfServeBaseClass {
public initialize = async (): Promise<Map<string, SmartUiInput>> => { public initialize = async (): Promise<Map<string, SmartUiInput>> => {
const initializeResponse = await initialize(); const initializeResponse = await initialize();
const defaults = new Map<string, SmartUiInput>(); const defaults = new Map<string, SmartUiInput>();
defaults.set("currentRegionText", undefined);
defaults.set("regions", { value: initializeResponse.regions }); defaults.set("regions", { value: initializeResponse.regions });
defaults.set("enableLogging", { value: initializeResponse.enableLogging }); defaults.set("enableLogging", { value: initializeResponse.enableLogging });
const accountName = initializeResponse.accountName; const accountName = initializeResponse.accountName;
@@ -172,15 +226,24 @@ export default class SelfServeExample extends SelfServeBaseClass {
e) Text (with optional hyperlink) for descriptions e) Text (with optional hyperlink) for descriptions
*/ */
@Values({ @Values({
labelTKey: "DescriptionLabel",
description: { description: {
textTKey: "DescriptionText", textTKey: "DescriptionText",
type: DescriptionType.Text,
link: { link: {
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", href: "https://aka.ms/cosmos-create-account-portal",
textTKey: "DecriptionLinkText", textTKey: "DecriptionLinkText",
}, },
}, },
}) })
description: string; description: string;
@Values({
labelTKey: "Current Region",
isDynamicDescription: true,
})
currentRegionText: string;
/* /*
@PropertyInfo() @PropertyInfo()
- optional - optional
@@ -192,8 +255,8 @@ export default class SelfServeExample extends SelfServeBaseClass {
/* /*
@OnChange() @OnChange()
- optional - optional
- input: (currentValues: Map<string, InputType>, newValue: InputType) => Map<string, InputType> - input: (currentValues: Map<string, InputType>, newValue: InputType, baselineValues: ReadonlyMap<string, SmartUiInput>) => Map<string, InputType>
- role: Takes a Map of current values and the newValue for this property as inputs. This is called when a property, - role: Takes a Map of current values, the newValue for this property and a ReadonlyMap of baselineValues as inputs. This is called when a property,
say prop1, changes its value in the UI. This can be used to say prop1, changes its value in the UI. This can be used to
a) Change the value (and reflect it in the UI) for prop2 based on prop1. a) Change the value (and reflect it in the UI) for prop2 based on prop1.
b) Change the visibility for prop2 in the UI, based on prop1 b) Change the visibility for prop2 in the UI, based on prop1

View File

@@ -0,0 +1,16 @@
.selfServeComponentContainer {
text-transform: none;
line-height: 1.28581;
letter-spacing: 0;
font-size: 14px;
font-weight: 400;
color: #182026;
height: 100%;
min-height: 100vh;
width: 100%;
background-color: #FFFFFF;
}
body {
margin: 0;
}

View File

@@ -0,0 +1,92 @@
import { Spinner, SpinnerSize } from "office-ui-fabric-react";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import * as React from "react";
import ReactDOM from "react-dom";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
import { sendReadyMessage } from "../Common/MessageHandler";
import { configContext, updateConfigContext } from "../ConfigContext";
import { SelfServeFrameInputs } from "../Contracts/ViewModels";
import { updateUserContext } from "../UserContext";
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
import "./SelfServe.less";
import { SelfServeComponent } from "./SelfServeComponent";
import { SelfServeDescriptor } from "./SelfServeTypes";
import { SelfServeType } from "./SelfServeUtils";
initializeIcons();
const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDescriptor> => {
switch (selfServeType) {
case SelfServeType.example: {
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
return new SelfServeExample.default().toSelfServeDescriptor();
}
case SelfServeType.sqlx: {
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
return new SqlX.default().toSelfServeDescriptor();
}
default:
return undefined;
}
};
const renderComponent = (selfServeDescriptor: SelfServeDescriptor): JSX.Element => {
if (!selfServeDescriptor) {
return <h1>Invalid self serve type!</h1>;
}
return <SelfServeComponent descriptor={selfServeDescriptor} />;
};
const renderSpinner = (): JSX.Element => {
return <Spinner size={SpinnerSize.large}></Spinner>;
};
const handleMessage = async (event: MessageEvent): Promise<void> => {
if (isInvalidParentFrameOrigin(event)) {
return;
}
if (event.data["signature"] !== "pcIframe") {
return;
}
if (typeof event.data !== "object") {
return;
}
const inputs = event.data.data.inputs as SelfServeFrameInputs;
if (!inputs) {
return;
}
const urlSearchParams = new URLSearchParams(window.location.search);
const selfServeTypeText = inputs.selfServeType || urlSearchParams.get("selfServeType");
const selfServeType = SelfServeType[selfServeTypeText?.toLowerCase() as keyof typeof SelfServeType];
if (
!inputs.subscriptionId ||
!inputs.resourceGroup ||
!inputs.databaseAccount ||
!inputs.authorizationToken ||
!inputs.csmEndpoint ||
!selfServeType
) {
return;
}
updateConfigContext({
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
});
updateUserContext({
authorizationToken: inputs.authorizationToken,
databaseAccount: inputs.databaseAccount,
resourceGroup: inputs.resourceGroup,
subscriptionId: inputs.subscriptionId,
});
const descriptor = await getDescriptor(selfServeType);
ReactDOM.render(renderComponent(descriptor), document.getElementById("selfServeContent"));
};
ReactDOM.render(renderSpinner(), document.getElementById("selfServeContent"));
window.addEventListener("message", handleMessage, false);
sendReadyMessage();

View File

@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import { SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent"; import { SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent";
import { NumberUiType, SelfServeDescriptor, SelfServeNotificationType, SmartUiInput } from "./SelfServeTypes"; import { NumberUiType, OnSaveResult, SelfServeDescriptor, SmartUiInput } from "./SelfServeTypes";
describe("SelfServeComponent", () => { describe("SelfServeComponent", () => {
const defaultValues = new Map<string, SmartUiInput>([ const defaultValues = new Map<string, SmartUiInput>([
@@ -17,13 +17,20 @@ describe("SelfServeComponent", () => {
const initializeMock = jest.fn(async () => new Map(defaultValues)); const initializeMock = jest.fn(async () => new Map(defaultValues));
const onSaveMock = jest.fn(async () => { const onSaveMock = jest.fn(async () => {
return { message: "submitted successfully", type: SelfServeNotificationType.info }; return {
operationStatusUrl: undefined,
} as OnSaveResult;
}); });
const refreshResult = {
isUpdateInProgress: false,
updateInProgressMessageTKey: "refresh performed successfully",
};
const onRefreshMock = jest.fn(async () => { const onRefreshMock = jest.fn(async () => {
return { isUpdateInProgress: false, notificationMessage: "refresh performed successfully" }; return { ...refreshResult };
}); });
const onRefreshIsUpdatingMock = jest.fn(async () => { const onRefreshIsUpdatingMock = jest.fn(async () => {
return { isUpdateInProgress: true, notificationMessage: "refresh performed successfully" }; return { ...refreshResult, isUpdateInProgress: true };
}); });
const exampleData: SelfServeDescriptor = { const exampleData: SelfServeDescriptor = {
@@ -136,16 +143,15 @@ describe("SelfServeComponent", () => {
wrapper.update(); wrapper.update();
state = wrapper.state() as SelfServeComponentState; state = wrapper.state() as SelfServeComponentState;
isEqual(state.baselineValues, updatedValues); isEqual(state.baselineValues, updatedValues);
selfServeComponent.resetBaselineValues(); selfServeComponent.updateBaselineValues();
state = wrapper.state() as SelfServeComponentState; state = wrapper.state() as SelfServeComponentState;
isEqual(state.baselineValues, defaultValues); isEqual(state.baselineValues, defaultValues);
isEqual(state.currentValues, state.baselineValues); isEqual(state.currentValues, state.baselineValues);
// clicking refresh calls onRefresh. If component is not updating, it calls initialize() as well // clicking refresh calls onRefresh.
selfServeComponent.onRefreshClicked(); selfServeComponent.onRefreshClicked();
await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0));
expect(onRefreshMock).toHaveBeenCalledTimes(2); expect(onRefreshMock).toHaveBeenCalledTimes(2);
expect(initializeMock).toHaveBeenCalledTimes(2);
selfServeComponent.onSaveButtonClick(); selfServeComponent.onSaveButtonClick();
expect(onSaveMock).toHaveBeenCalledTimes(1); expect(onSaveMock).toHaveBeenCalledTimes(1);

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