Compare commits

...

54 Commits

Author SHA1 Message Date
Steve Faulkner
e752f21ec7 Add command 2021-02-28 23:19:51 -06:00
Steve Faulkner
9f34bff755 WIP 2021-02-26 17:12:12 -06:00
Srinath Narayanan
cf01ffa957 removed enableGalleryPublish, enableLinkInjection feature flags (#454)
* removed enableGalleryPublish, enableLinkInjection

* removed ENABLE_GALLERY_PUBLISH
2021-02-26 13:07:44 -08:00
victor-meng
3cc1945140 Fix serverless create issue (#453) 2021-02-25 19:29:29 -06:00
Garrett Ausfeldt
864d9393f2 Change schema endpoints (#447)
* fix resource bug (id is a method)

* change schema endpoints

* fix test

Co-authored-by: REDMOND\gaausfel <gaausfel@microsoft.com>
2021-02-25 11:36:01 -08:00
Steve Faulkner
8629bcbe2d Move "Open Full Screen" to React Panel (#449) 2021-02-24 19:04:28 -06:00
Steve Faulkner
6c90ef2e62 Remove Dialog Adapter (#446) 2021-02-24 18:41:28 -06:00
Steve Faulkner
2d2d8b6efe Remove Unused Tabs (#450) 2021-02-24 17:48:33 -06:00
victor-meng
7cbf7202b0 Fix start with samples in Gremlin API (#448) 2021-02-24 12:17:15 -06:00
Jordi Bunster
e8e5eb55cb Remove SplashScreenComponentAdapter (#440)
Merge SplashScreenComponentAdapter and SplashScreenComponent into one SplashScreen React component.
2021-02-23 11:15:57 -08:00
Steve Faulkner
f0c82a430b Remove AdHoc Access and Token Renewal Pane (#445) 2021-02-23 11:16:00 -06:00
Steve Faulkner
3777b6922e Revert "ci: add Azure Static Web Apps workflow file " This reverts commit aec951694a. # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # # On branch master # Your branch is up to date with 'origin/master'. # # Changes to be committed: # deleted: .github/workflows/azure-static-web-apps-victorious-sea-0ea0a4710.yml # 2021-02-22 20:21:40 -06:00
Azure Static Web Apps
aec951694a ci: add Azure Static Web Apps workflow file
on-behalf-of: @Azure opensource@microsoft.com
2021-02-22 19:58:22 -06:00
Steve Faulkner
07474b8271 Remove window.authType (#437) 2021-02-22 14:43:58 -06:00
Deborah Chen
e092e5140f Fixing issue with autoscale A/B test (#442)
Currently, the first time a user opens the New Container pane, the autoscale setting is not being read from the flight info. This is because the flight into is being set in resetData(), which is not called the 1st time a user opens the pane. 

This change adds logic to set the flight info on the open call.
2021-02-22 18:54:55 +00:00
Steve Faulkner
1f4074f3e8 Update Coding Guidelines (#441)
Co-authored-by: Christopher Anderson <anderson.chris.john@gmail.com>
2021-02-18 13:18:50 -06:00
Chris-MS-896
1ec0d9a0be End to end testcase for mongoDb index policy (#426)
* 'update for index policy'

* input

* 'refactor for ci/cd'

* 'refactor for ci/cd'

* no message

* 'refractor'

* 'format change'

* added changes

* "refactor"

* ‘test lint’

* "format"

* “push setting back”

* ‘refractor’

* Update test/utils/shared.ts

Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>

Co-authored-by: Srinath Narayanan <srnara@microsoft.com>
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-02-18 05:40:58 -08:00
Srinath Narayanan
eddc334cb5 mongo index editor for AAD login + hostedExplorer (#438) 2021-02-17 12:52:47 -08:00
victor-meng
22d8a7a1be Move database settings tab to react (#386)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-02-10 16:06:14 -06:00
victor-meng
4210e0752b Move delete collection confirmation pane to react (#417) 2021-02-10 13:44:00 -08:00
Steve Faulkner
b217d4be1b Delete Cassandra tables/keyspaces via ARM (#436) 2021-02-08 18:52:53 -06:00
victor-meng
81fd442fad Make getCollectionDataUsageSize call fail gracefully (#434) 2021-02-08 16:02:02 -08:00
Steve Faulkner
87f7dd2230 Rename Feedback -> Report Issue (#425)
Co-authored-by: victor-meng <56978073+victor-meng@users.noreply.github.com>
2021-02-08 14:23:55 -06:00
Srinath Narayanan
9926fd97a2 Test explorer changes (#420)
* Changes to publish pane

* fixed format errors

* fixed failing test

* added test explorer changes for mongo accounts

* added log for test

* fixed lit errors

* added secrets to ci.yml file

* fixed failing self serve test
2021-02-08 09:42:16 -08:00
Tanuj Mittal
2a7546e0de Skip SelfServe e2e test (#432) 2021-02-06 02:30:46 +05:30
Tanuj Mittal
4b442dd869 Use GET instead of PATCH for some Juno endpoints (#431) 2021-02-06 02:01:34 +05:30
Tanuj Mittal
f0b4737313 Update gallery colors (#430)
* Update gallery colors

* fix lint error

* Fix test
2021-02-06 01:19:36 +05:30
Srinath Narayanan
8dc5ed590a Added Spinner for public gallery (#427)
* Added more publish changes

* addressed PR comments

* fixed lint errors

Co-authored-by: Tanuj Mittal <tamitta@microsoft.com>
2021-02-05 11:32:26 -08:00
Tanuj Mittal
afaa844d28 Telemetry updates (#429) 2021-02-06 00:26:20 +05:30
Tanuj Mittal
3e5a876ef2 Fix setting isGalleryPublishEnabled when flight is enabled (#428) 2021-02-05 20:55:08 +05:30
Tanuj Mittal
51abf1560a Disable dark overlay for Dialog (#423) 2021-02-05 19:28:57 +05:30
Srinath Narayanan
1c0fed88c0 Changes to cards in notebook gallery (#422)
* added gallery changes

* addressed PR comments
2021-02-05 02:32:55 -08:00
Tanuj Mittal
93cfd52e36 Update Code of Conduct Overlay and other minor changes (#424)
* Use light theme for coc-overlay

* Updates

* Fix vertical height for COC overlay
2021-02-05 14:56:50 +05:30
Tanuj Mittal
3fd014ddad Disable caching for config.json file (#421)
* Disable caching for config.json file

* Disable cache when fetching config.json
2021-02-04 15:25:13 +05:30
Srinath Narayanan
3b6fda4fa5 Changes to notebook publish pane (#419)
* Changes to publish pane

* fixed format errors

* fixed failing test

Co-authored-by: Tanuj Mittal <tamitta@microsoft.com>
2021-02-03 10:46:51 -08:00
Tanuj Mittal
db7c45c9b8 Enable gallery publishing in MPAC (#416)
* Enable gallery publishing in MPAC

* Address feedback

* Use ENABLE_GALLERY_PUBLISH config in standalone gallery

* Fix test
2021-02-03 23:42:11 +05:30
Tanuj Mittal
4f6b75fe79 More gallery updates (#418)
* More gallery updates

* Add PublishContent icon

* Address feedback
2021-02-03 23:24:27 +05:30
Tanuj Mittal
5038a01079 Add telemetry for Notebooks Gallery and other updates (#413)
* Add telemetry for Notebooks Gallery

* More changes

* Address feedback and fix lint error

* Fix margins for My published work
2021-02-03 14:48:50 +05:30
Tanuj Mittal
e0063c76d9 Add support for gallerypublish flight (#412) 2021-02-03 02:05:19 +05:30
Tanuj Mittal
9278654479 Public gallery improvements (#409)
- [x] Don't show extension in name field for publish
- [x] Open "Your published work" tab after publishing
- [x] Continue showing dialog for Report Abuse status
- [x] For showing COC in Public Gallery tab show backdrop of thumbnails
- [x] Liked -> My Favorites & Your published work -> My published work
2021-01-29 17:04:38 +00:00
Tanuj Mittal
59113d7bbf Update Juno endpoints (#405) 2021-01-29 20:28:20 +05:30
Tim Sander
88d8200c14 Check that customer is using Mongo 3.6 before applying index everything policy (#410) 2021-01-28 15:26:47 -06:00
Srinath Narayanan
6aaddd9c60 Added localization for the Self Serve Model (#406)
* added localization for selfserve model

* added comment

* addressed PR comments

* fixed format errors

* Addressed PR comments
2021-01-28 11:17:02 -08:00
Jordi Bunster
f8ede0cc1e Remove Q from ViewModels (#390)
I got cold feet at the thought of merging #324 in one go, so I'm going to split it into smaller chunks and keep rebasing the large one until there's no more Q.
2021-01-28 18:13:26 +00:00
Laurent Nguyen
bddb288a89 Update package versions and package-lock.json (#404)
The file `package-lock.json` is not in sync with `package.json` anymore. This causes build issues when upgrading a package.
This change sync's `package-lock.json` and fixes the build issues.
2021-01-28 08:50:24 +00:00
Steve Faulkner
a14d20a88e Fix applyExplorerBindings call in Portal (#408) 2021-01-27 20:37:14 -06:00
Steve Faulkner
f1db1ed978 Region Select Button (#407) 2021-01-27 15:32:53 -06:00
Laurent Nguyen
86a483c3a4 Fix notebook cell selection bug (#402)
This fixes a bug that prevents getting focus to a text cell (effectively preventing editing) when the window height is small after double-clicking on a neighboring code cell.
The issue is that selecting a text cell is broken likely because there's a behavior change in MonacoEditor that keeps the focus on the code cell. The selection issue will probably be resolved when migrating the text cell to Monaco (which will acquire and keep focus the same way), but for now, this will disable the faulty code which doesn't appear to work anymore (presumably auto-scrolling to the cell).
2021-01-27 09:09:54 +00:00
Tanuj Mittal
263262a040 Update Juno endpoints to pass subscriptionId (#339)
Corresponding [server side change](https://msdata.visualstudio.com/CosmosDB/_git/CosmosDB-portal/pullrequest/464443?_a=overview) has been deployed to Prod so now we can go ahead with DE side changes.
2021-01-27 08:08:58 +00:00
victor-meng
bd4d8da065 Move notification console to react (#400) 2021-01-26 15:32:37 -08:00
Steve Faulkner
59ec18cd9b Add basic static code metrics (#396) 2021-01-26 13:13:13 -06:00
Srinath Narayanan
49bf8c60db Added more Self Serve functionalities (#401)
* added recursion and inition decorators

* working version

* added todo comment and removed console.log

* Added Recursive add

* removed type requirement

* proper resolution of promises

* added custom element and base class

* Made selfServe standalone page

* Added custom renderer as async type

* Added overall defaults

* added inital open from data explorer

* removed landingpage

* added feature for self serve type

* renamed sqlx->example and added invalid type

* Added comments for Example

* removed unnecessary changes

* Resolved PR comments

Added tests
Moved onSubmt and initialize inside base class
Moved testExplorer to separate folder
made fields of SelfServe Class non static

* fixed lint errors

* fixed compilation errors

* Removed reactbinding changes

* renamed dropdown -> choice

* Added SelfServeComponent

* Addressed PR comments

* added toggle, visibility, text display,commandbar

* added sqlx example

* added onRefrssh

* formatting changes

* rmoved radioswitch display

* updated smartui tests

* Added more tests

* onSubmit -> onSave

* Resolved PR comments
2021-01-26 09:44:14 -08:00
Steve Faulkner
b0b973b21a Refactor explorer config into useKnockoutExplorer hook (#397)
Co-authored-by: Steve Faulkner <stfaul@microsoft.com>
2021-01-25 13:56:15 -06:00
Chris-MS-896
3529e80f0d no message (#398) 2021-01-22 10:02:35 -06:00
199 changed files with 11792 additions and 7094 deletions

View File

@@ -4,6 +4,8 @@ PORTAL_RUNNER_SUBSCRIPTION=
PORTAL_RUNNER_RESOURCE_GROUP= PORTAL_RUNNER_RESOURCE_GROUP=
PORTAL_RUNNER_DATABASE_ACCOUNT= PORTAL_RUNNER_DATABASE_ACCOUNT=
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY= PORTAL_RUNNER_DATABASE_ACCOUNT_KEY=
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT=
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY=
PORTAL_RUNNER_CONNECTION_STRING= PORTAL_RUNNER_CONNECTION_STRING=
NOTEBOOKS_TEST_RUNNER_TENANT_ID= NOTEBOOKS_TEST_RUNNER_TENANT_ID=
NOTEBOOKS_TEST_RUNNER_CLIENT_ID= NOTEBOOKS_TEST_RUNNER_CLIENT_ID=

View File

@@ -87,7 +87,7 @@ src/Explorer/DataSamples/ContainerSampleGenerator.test.ts
src/Explorer/DataSamples/ContainerSampleGenerator.ts src/Explorer/DataSamples/ContainerSampleGenerator.ts
src/Explorer/DataSamples/DataSamplesUtil.test.ts src/Explorer/DataSamples/DataSamplesUtil.test.ts
src/Explorer/DataSamples/DataSamplesUtil.ts src/Explorer/DataSamples/DataSamplesUtil.ts
src/Explorer/Explorer.ts src/Explorer/Explorer.tsx
src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts
src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts
src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts
@@ -162,7 +162,7 @@ src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts
src/Explorer/Panes/UploadFilePane.ts src/Explorer/Panes/UploadFilePane.ts
src/Explorer/Panes/UploadItemsPane.ts src/Explorer/Panes/UploadItemsPane.ts
src/Explorer/SplashScreen/SplashScreenComponentAdapter.test.ts src/Explorer/SplashScreen/SplashScreen.test.ts
src/Explorer/Tables/Constants.ts src/Explorer/Tables/Constants.ts
src/Explorer/Tables/DataTable/CacheBase.ts src/Explorer/Tables/DataTable/CacheBase.ts
src/Explorer/Tables/DataTable/DataTableBindingManager.ts src/Explorer/Tables/DataTable/DataTableBindingManager.ts
@@ -200,7 +200,6 @@ src/Explorer/Tabs/QueryTab.test.ts
src/Explorer/Tabs/QueryTab.ts src/Explorer/Tabs/QueryTab.ts
src/Explorer/Tabs/QueryTablesTab.ts src/Explorer/Tabs/QueryTablesTab.ts
src/Explorer/Tabs/ScriptTabBase.ts src/Explorer/Tabs/ScriptTabBase.ts
src/Explorer/Tabs/SparkMasterTab.ts
src/Explorer/Tabs/StoredProcedureTab.ts src/Explorer/Tabs/StoredProcedureTab.ts
src/Explorer/Tabs/TabComponents.ts src/Explorer/Tabs/TabComponents.ts
src/Explorer/Tabs/TabsBase.ts src/Explorer/Tabs/TabsBase.ts
@@ -377,8 +376,7 @@ src/Explorer/Notebook/temp/inputs/editor.tsx
src/Explorer/Notebook/temp/markdown-cell.tsx src/Explorer/Notebook/temp/markdown-cell.tsx
src/Explorer/Notebook/temp/source.tsx src/Explorer/Notebook/temp/source.tsx
src/Explorer/Notebook/temp/syntax-highlighter/index.tsx src/Explorer/Notebook/temp/syntax-highlighter/index.tsx
src/Explorer/SplashScreen/SplashScreenComponent.tsx src/Explorer/SplashScreen/SplashScreen.tsx
src/Explorer/SplashScreen/SplashScreenComponentApdapter.tsx
src/Explorer/Tabs/GalleryTab.tsx src/Explorer/Tabs/GalleryTab.tsx
src/Explorer/Tabs/NotebookViewerTab.tsx src/Explorer/Tabs/NotebookViewerTab.tsx
src/Explorer/Tabs/TerminalTab.tsx src/Explorer/Tabs/TerminalTab.tsx

35
.eslintrc.allFiles.js Normal file
View File

@@ -0,0 +1,35 @@
module.exports = {
env: {
browser: true,
es6: true,
},
plugins: ["@typescript-eslint"],
globals: {
Atomics: "readonly",
SharedArrayBuffer: "readonly",
},
overrides: [
{
files: ["**/*.tsx"],
plugins: ["react"],
},
{
files: ["**/*.{test,spec}.{ts,tsx}"],
env: {
jest: true,
},
plugins: ["jest"],
},
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2018,
sourceType: "module",
},
rules: {
"@typescript-eslint/no-unused-vars-experimental": "error",
},
};

View File

@@ -9,6 +9,20 @@ on:
branches: branches:
- master - master
jobs: jobs:
codemetrics:
runs-on: ubuntu-latest
name: "Log Code Metrics"
if: github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v2
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- run: npm ci
- run: node utils/codeMetrics.js
env:
CODE_METRICS_APP_ID: ${{ secrets.CODE_METRICS_APP_ID }}
compile: compile:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: "Compile TypeScript" name: "Compile TypeScript"
@@ -142,6 +156,7 @@ jobs:
run: | run: |
npm ci npm ci
npm start & npm start &
node utils/cleanupDBs.js
npm run wait-for-server npm run wait-for-server
npm run test:e2e npm run test:e2e
shell: bash shell: bash
@@ -151,6 +166,8 @@ jobs:
PORTAL_RUNNER_RESOURCE_GROUP: ${{ secrets.PORTAL_RUNNER_RESOURCE_GROUP }} PORTAL_RUNNER_RESOURCE_GROUP: ${{ secrets.PORTAL_RUNNER_RESOURCE_GROUP }}
PORTAL_RUNNER_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT }} PORTAL_RUNNER_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT }}
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY }} PORTAL_RUNNER_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY }}
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT }}
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY }}
NOTEBOOKS_TEST_RUNNER_TENANT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_TENANT_ID }} NOTEBOOKS_TEST_RUNNER_TENANT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_TENANT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }} NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }} NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}

194
CODING_GUIDELINES.md Normal file
View File

@@ -0,0 +1,194 @@
# Coding Guidelines and Recommendations
Cosmos Explorer has been under constant development for over 5 years. As a result, there are many different patterns and practices in the codebase. This document serves as a guide to how we write code and helps avoid propagating practices which are no longer preferred. Each requirement in this document is labeled and color-coded to show the relative importance. In order from highest to lowest importance:
✅ DO this. If you feel you need an exception, engage with the project owners _prior_ to implementation.
⛔️ DO NOT do this. If you feel you need an exception, engage with the project owners _prior_ to implementation.
☑️ YOU SHOULD strongly consider this but it is not a requirement. If not following this advice, please comment code with why and proactively begin a discussion as part of the PR process.
⚠️ YOU SHOULD NOT strongly consider not doing this. If not following this advice, please comment code with why and proactively begin a discussion as part of the PR process.
💭 YOU MAY consider this advice if appropriate to your situation. Other team members may comment on this as part of PR review, but there is no need to be proactive.
## Development Environment
☑️ YOU SHOULD
- Use VSCode and install the following extensions. This setup will catch most linting/formatting/type errors as you develop:
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
💭 YOU MAY
- Use the [GitHub CLI](https://cli.github.com/). It has helpful workflows for submitting PRs as well as for checking out other team member's PRs.
- Use Windows, Linux (including WSL), or OSX. We have team members developing on all three environments.
✅ DO
- Maintain cross-platform compatibility when modifying any engineering or build systems
## Code Formatting
✅ DO
- Use [Prettier](https://prettier.io/) to format your code
- This will occur automatically if using the recommended editor setup
- `npm run format` will also format code
## Linting
✅ DO
- Use [ESLint](https://eslint.org/) to check for code errors.
- This will occur automatically if using the recommended editor setup
- `npm run lint` will also check for linting errors
💭 YOU MAY
- Consider adding new lint rules.
- If you find yourself performing "nits" as part of PR review, consider adding a lint rule that will automatically catch the error in the future
⚠️ YOU SHOULD NOT
- Disable lint rules
- Lint rules exist as guidance and to catch common mistakes
- You will find places we disable specific lint rules however it should be exceptional.
- If a rule does need to be disabled, prefer disabling a specific line instead of the entire file.
⛔️ DO NOT
- Add [TSLint](https://palantir.github.io/tslint/) rules
- TSLint has been deprecated and is on track to be removed
- Always prefer ESLint rules
## UI Components
☑️ YOU SHOULD
- Write new components using [React](https://reactjs.org/). We are actively migrating Cosmos Explorer off of [Knockout](https://knockoutjs.com/).
- Use [Fluent](https://developer.microsoft.com/en-us/fluentui#/) components.
- Fluent components are designed to be highly accessible and composable
- Using Fluent allows us to build upon the work of the Fluent team and leads to a lower total cost of ownership for UI code
### React
☑️ YOU SHOULD
- Use pure functional components when no state is required
💭 YOU MAY
- Use functional (hooks) or class components
- The project contains examples of both
- Neither is strongly preferred at this time
⛔️ DO NOT
- Use inheritance for sharing component behavior.
- React documentation covers this topic in detail https://reactjs.org/docs/composition-vs-inheritance.html
- Suffix your file or component name with "Component"
- Even though the code has examples of it, we are ending the practice.
## Libraries
⚠️ YOU SHOULD NOT
- Add new libraries to package.json.
- Adding libraries may bring in code that explodes the bundled size or attempts to run NodeJS code in the browser
- Consult with project owners for help with library selection if one is needed
⛔️ DO NOT
- Use underscore.js
- Much of this library is now native to JS and will be automatically transpiled
- Use jQuery
- Much of this library is not native to the DOM.
- We are planning to remove it
## Testing
⛔️ DO NOT
- Decrease test coverage
- Unit/Functional test coverage is checked as part of the CI process
### Unit Tests
✅ DO
- Write unit tests for non-UI and utility code.
- Write your tests using [Jest](https://jestjs.io/)
☑️ YOU SHOULD
- Abstract non-UI and utility code so it can run either the NodeJS or Browser environment
### Functional(Component) Tests
✅ DO
- Write tests for UI components
- Write your tests using [Jest](https://jestjs.io/)
- Use either Enzyme or React Testing Library to perform component tests.
### Mocking
✅ DO
- Use Jest's built-in mocking helpers
☑️ YOU SHOULD
- Write code that does not require mocking
- Build components that do not require mocking extremely large or difficult to mock objects (like Explorer.ts). Pass _only_ what you need.
⛔️ DO NOT
- Use sinon.js for mocking
- Sinon has been deprecated and planned for removal
### End to End Tests
✅ DO
- Use [Puppeteer](https://developers.google.com/web/tools/puppeteer) and [Jest](https://jestjs.io/)
- Write or modify an existing E2E test that covers the primary use case of any major feature.
- Use caution. Do not try to cover every case. End to End tests can be slow and brittle.
☑️ YOU SHOULD
- Write tests that use accessible attributes to perform actions. Role, Title, Label, etc
- More information https://testing-library.com/docs/queries/about#priority
⚠️ YOU SHOULD NOT
- Add test specfic `data-*` attributes to dom elements
- This is a common current practice, but one we would like to avoid in the future
- End to end tests need to use semantic HTML and accesible attributes to be truely end to end
- No user or screen reader actually navigates an app using `data-*` attributes
- Add arbitrary time delays to wait for page to render or element to be ready.
- All the time delays add up and slow down testing.
- Prefer using the framework's "wait for..." functionality.
### Migrating Knockout to React
✅ DO
- Consult other team members before beginning migration work. There is a significant amount of flux in patterns we are using and it is important we do not propagate incorrect patterns.
- Start by converting HTML to JSX: https://magic.reactjs.net/htmltojsx.htm. Add functionality as a second step.
☑️ YOU SHOULD
- Write React components that require no dependency on Knockout or observables to trigger rendering.
## Browser Support
✅ DO
- Support all [browsers supported by the Azure Portal](https://docs.microsoft.com/en-us/azure/azure-portal/azure-portal-supported-browsers-devices)
- Support IE11
- In practice, this should not need to be considered as part of a normal development workflow
- Polyfills and transpilation are already provided by our engineering systems.
- This requirement will be removed on March 30th, 2021 when Azure drops IE11 support.

View File

@@ -1,6 +1,6 @@
# Contribution guidelines to Data Explorer # Contribution guidelines to Data Explorer
This project welcomes contributions and suggestions. Most contributions require you to agree to a This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com. the rights to use your contribution. For details, visit https://cla.microsoft.com.
@@ -13,6 +13,7 @@ For more information see the [Code of Conduct FAQ](https://opensource.microsoft.
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
## Microsoft Open Source Code of Conduct ## Microsoft Open Source Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
Resources: Resources:
@@ -20,33 +21,3 @@ Resources:
- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
## Browser support
Please make sure to support all modern browsers as well as Internet Explorer 11.
For IE support, polyfill is preferred over new usage of lodash or underscore. We already polyfill almost everything by importing babel-polyfill at the top of entry points.
## Coding guidelines, conventions and recommendations
### Typescript
* Follow this [typescript style guide](https://github.com/excelmicro/typescript) which is based on [airbnb's style guide](https://github.com/airbnb/javascript).
* Conventions speficic to this project:
- Use double-quotes for string
- Don't use `null`, use `undefined`
- Pascal case for private static readonly fields
- Camel case for classnames in markup
* Don't use class unless necessary
* Code related to notebooks should be dynamically imported so that it is loaded from a separate bundle only if the account is notebook-enabled. There are already top-level notebook components which are dynamically imported and their dependencies can be statically imported from these files.
* Prefer using [Fluent UI controls](https://developer.microsoft.com/en-us/fluentui#/controls/web) over creating your own, in order to maintain consistency and support a11y.
### React
* Prefer using React class components over function components and hooks unless you have a simple component and require no nested functions:
* Nested functions may be harder to test independently
* Switching from function component to class component later mayb be painful
## Testing
Any PR should not decrease testing coverage.
## Recommended Tools and VS Code extensions
* [Bookmarks](https://github.com/alefragnani/vscode-bookmarks)
* [Bracket pair colorizer](https://github.com/CoenraadS/Bracket-Pair-Colorizer-2)
* [GitHub Pull Requests and Issues](https://github.com/Microsoft/vscode-pull-request-github)

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.31449 2.01439L4.00103 5.31963L3.26105 4.57965L7.8407 0L12.4203 4.57965L11.6804 5.31963L8.36691 2.01439V12.8428H7.31449V2.01439ZM13.629 12.8428H14.6814V16H1V12.8428H2.05242V14.9476H13.629V12.8428Z" fill="#0078D4"/>
</svg>

After

Width:  |  Height:  |  Size: 329 B

View File

@@ -57,6 +57,13 @@
@FocusColor: #605e5c; @FocusColor: #605e5c;
@GalleryBackgroundColor: #fdfdfd;
//Icons
@InfoIconColor: #0072c6;
@WarningIconColor: #db7500;
@ErrorIconColor: #b91f26;
/****************************************************************************** /******************************************************************************
METRICS METRICS
/******************************************************************************/ /******************************************************************************/

View File

@@ -1523,6 +1523,21 @@ p {
.tooltipVisible(); .tooltipVisible();
} }
.inputTooltip {
.inputTooltip();
}
.inputTooltip .inputTooltipText {
top: -68px;
.inputTooltipText();
}
.inputTooltip .inputTooltipText::after {
border-width: @MediumSpace @MediumSpace 0 @MediumSpace;
top: 55px;
.inputTooltipTextAfter();
}
.infoTooltip a { .infoTooltip a {
color: @AccentHigh; color: @AccentHigh;
} }
@@ -1694,6 +1709,7 @@ input::-webkit-calendar-picker-indicator {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
max-height: 100vh;
} }
.contextual-pane .paneErrorDetailsContainer { .contextual-pane .paneErrorDetailsContainer {
@@ -2083,7 +2099,7 @@ a:link {
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: auto;
height: 100%; height: 100%;
} }
@@ -3027,3 +3043,45 @@ settings-pane {
.collapsibleSection :hover { .collapsibleSection :hover {
cursor: pointer; cursor: pointer;
} }
.messageBarInfoIcon {
color: @InfoIconColor;
}
.messageBarWarningIcon {
color: @WarningIconColor;
}
.freeTierInfoBanner {
background-color: @BaseLow;
display: inline-flex;
padding: @DefaultSpace;
width: 100%;
.freeTierInfoIcon img {
height: 28px;
width: 28px;
margin-left: 4px;
}
.freeTierInfoMessage {
margin: auto 0;
padding-left: @MediumSpace;
}
}
.freeTierInlineWarning {
display: inline-flex;
padding: 8px 8px 8px 0;
width: 100%;
.freeTierWarningIcon img {
height: 20px;
width: 20px;
}
.freeTierWarningMessage {
margin: auto 0;
padding-left: @SmallSpace;
}
}

4596
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,10 +10,10 @@
"@azure/identity": "1.2.1", "@azure/identity": "1.2.1",
"@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12", "@babel/plugin-proposal-decorators": "7.12.12",
"@jupyterlab/services": "6.0.0-rc.2", "@jupyterlab/services": "6.0.2",
"@jupyterlab/terminal": "3.0.0-rc.2", "@jupyterlab/terminal": "3.0.3",
"@microsoft/applicationinsights-web": "2.5.9", "@microsoft/applicationinsights-web": "2.5.9",
"@nteract/commutable": "7.3.2", "@nteract/commutable": "7.4.2",
"@nteract/connected-components": "6.8.2", "@nteract/connected-components": "6.8.2",
"@nteract/core": "15.1.0", "@nteract/core": "15.1.0",
"@nteract/data-explorer": "8.0.3", "@nteract/data-explorer": "8.0.3",
@@ -49,6 +49,7 @@
"bootstrap": "3.4.1", "bootstrap": "3.4.1",
"canvas": "file:./canvas", "canvas": "file:./canvas",
"clean-webpack-plugin": "0.1.19", "clean-webpack-plugin": "0.1.19",
"clipboard-copy": "4.0.1",
"copy-webpack-plugin": "6.0.2", "copy-webpack-plugin": "6.0.2",
"crossroads": "0.12.2", "crossroads": "0.12.2",
"css-element-queries": "1.1.1", "css-element-queries": "1.1.1",
@@ -60,10 +61,14 @@
"dotenv": "8.2.0", "dotenv": "8.2.0",
"es6-object-assign": "1.1.0", "es6-object-assign": "1.1.0",
"es6-symbol": "3.1.3", "es6-symbol": "3.1.3",
"eslint-nibble": "6.1.0",
"eslint-plugin-jest": "23.13.2", "eslint-plugin-jest": "23.13.2",
"eslint-plugin-react": "7.20.0", "eslint-plugin-react": "7.20.0",
"hasher": "1.2.0", "hasher": "1.2.0",
"html2canvas": "1.0.0-rc.5", "html2canvas": "1.0.0-rc.5",
"i18next": "19.8.4",
"i18next-browser-languagedetector": "6.0.1",
"i18next-http-backend": "1.0.23",
"immutable": "4.0.0-rc.12", "immutable": "4.0.0-rc.12",
"is-ci": "2.0.0", "is-ci": "2.0.0",
"jquery": "3.5.1", "jquery": "3.5.1",
@@ -86,6 +91,7 @@
"react-dnd-html5-backend": "9.4.0", "react-dnd-html5-backend": "9.4.0",
"react-dom": "16.13.1", "react-dom": "16.13.1",
"react-hotkeys": "2.0.0", "react-hotkeys": "2.0.0",
"react-i18next": "11.8.5",
"react-notification-system": "0.2.17", "react-notification-system": "0.2.17",
"react-redux": "7.1.3", "react-redux": "7.1.3",
"redux": "4.0.4", "redux": "4.0.4",
@@ -124,7 +130,7 @@
"@types/prop-types": "15.5.8", "@types/prop-types": "15.5.8",
"@types/puppeteer": "3.0.1", "@types/puppeteer": "3.0.1",
"@types/q": "1.5.1", "@types/q": "1.5.1",
"@types/react": "16.9.56", "@types/react": "17.0.0",
"@types/react-dom": "17.0.0", "@types/react-dom": "17.0.0",
"@types/react-notification-system": "0.2.39", "@types/react-notification-system": "0.2.39",
"@types/react-redux": "7.1.7", "@types/react-redux": "7.1.7",
@@ -151,6 +157,7 @@
"eslint-plugin-prefer-arrow": "1.2.2", "eslint-plugin-prefer-arrow": "1.2.2",
"eslint-plugin-react-hooks": "4.2.0", "eslint-plugin-react-hooks": "4.2.0",
"expose-loader": "0.7.5", "expose-loader": "0.7.5",
"fast-glob": "3.2.5",
"file-loader": "2.0.0", "file-loader": "2.0.0",
"fs-extra": "7.0.0", "fs-extra": "7.0.0",
"html-loader": "0.5.5", "html-loader": "0.5.5",
@@ -206,6 +213,7 @@
"format": "prettier --write \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"", "format": "prettier --write \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
"format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"", "format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"", "lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
"lint:important": "eslint --no-ingore --no-eslintrc -c ./eslintrc.important.js \"**/*.{ts,tsx}\"",
"build:contracts": "npm run compile:contracts", "build:contracts": "npm run compile:contracts",
"strictEligibleFiles": "node ./strict-migration-tools/index.js", "strictEligibleFiles": "node ./strict-migration-tools/index.js",
"autoAddStrictEligibleFiles": "node ./strict-migration-tools/autoAdd.js", "autoAddStrictEligibleFiles": "node ./strict-migration-tools/autoAdd.js",

View File

@@ -3,6 +3,7 @@
"offerThroughput": 400, "offerThroughput": 400,
"databaseLevelThroughput": false, "databaseLevelThroughput": false,
"collectionId": "Persons", "collectionId": "Persons",
"createNewDatabase": true,
"partitionKey": { "kind": "Hash", "paths": ["/name"] }, "partitionKey": { "kind": "Hash", "paths": ["/name"] },
"data": [ "data": [
"g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)", "g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)",

View File

@@ -105,8 +105,6 @@ export class Features {
public static readonly hostedDataExplorer = "hosteddataexplorerenabled"; public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
public static readonly enableTtl = "enablettl"; public static readonly enableTtl = "enablettl";
public static readonly enableNotebooks = "enablenotebooks"; public static readonly enableNotebooks = "enablenotebooks";
public static readonly enableGalleryPublish = "enablegallerypublish";
public static readonly enableLinkInjection = "enablelinkinjection";
public static readonly enableSpark = "enablespark"; public static readonly enableSpark = "enablespark";
public static readonly livyEndpoint = "livyendpoint"; public static readonly livyEndpoint = "livyendpoint";
public static readonly notebookServerUrl = "notebookserverurl"; public static readonly notebookServerUrl = "notebookserverurl";
@@ -119,7 +117,9 @@ export class Features {
public static readonly enableSchema = "enableschema"; public static readonly enableSchema = "enableschema";
public static readonly enableSDKoperations = "enablesdkoperations"; public static readonly enableSDKoperations = "enablesdkoperations";
public static readonly showMinRUSurvey = "showminrusurvey"; public static readonly showMinRUSurvey = "showminrusurvey";
public static readonly enableDatabaseSettingsTabV1 = "enabledbsettingsv1";
public static readonly selfServeType = "selfservetype"; public static readonly selfServeType = "selfservetype";
public static readonly enableKOPanel = "enablekopanel";
} }
// flight names returned from the portal are always lowercase // flight names returned from the portal are always lowercase

View File

@@ -220,7 +220,6 @@ describe("MongoProxyClient", () => {
describe("getEndpoint", () => { describe("getEndpoint", () => {
beforeEach(() => { beforeEach(() => {
resetConfigContext(); resetConfigContext();
delete window.authType;
updateUserContext({ updateUserContext({
databaseAccount, databaseAccount,
}); });
@@ -241,7 +240,9 @@ describe("MongoProxyClient", () => {
}); });
it("returns a guest endpoint", () => { it("returns a guest endpoint", () => {
window.authType = AuthType.EncryptedToken; updateUserContext({
authType: AuthType.EncryptedToken,
});
const endpoint = getEndpoint(); const endpoint = getEndpoint();
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer"); expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
}); });

View File

@@ -20,7 +20,7 @@ const defaultHeaders = {
}; };
function authHeaders() { function authHeaders() {
if (window.authType === AuthType.EncryptedToken) { if (userContext.authType === AuthType.EncryptedToken) {
return { [HttpHeaders.guestAccessToken]: userContext.accessToken }; return { [HttpHeaders.guestAccessToken]: userContext.accessToken };
} else { } else {
return { [HttpHeaders.authorization]: userContext.authorizationToken }; return { [HttpHeaders.authorization]: userContext.authorizationToken };
@@ -337,7 +337,7 @@ export function createMongoCollectionWithProxy(
export function getEndpoint(): string { export function getEndpoint(): string {
let url = (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT) + "/api/mongo/explorer"; let url = (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT) + "/api/mongo/explorer";
if (window.authType === AuthType.EncryptedToken) { if (userContext.authType === AuthType.EncryptedToken) {
url = url.replace("api/mongo", "api/guest/mongo"); url = url.replace("api/mongo", "api/guest/mongo");
} }
return url; return url;

View File

@@ -27,13 +27,17 @@ describe("createCollection", () => {
}); });
it("should call ARM if logged in with AAD", async () => { it("should call ARM if logged in with AAD", async () => {
window.authType = AuthType.AAD; updateUserContext({
authType: AuthType.AAD,
});
await createCollection(createCollectionParams); await createCollection(createCollectionParams);
expect(armRequest).toHaveBeenCalled(); expect(armRequest).toHaveBeenCalled();
}); });
it("should call SDK if not logged in with non-AAD method", async () => { it("should call SDK if not logged in with non-AAD method", async () => {
window.authType = AuthType.MasterKey; updateUserContext({
authType: AuthType.MasterKey,
});
(client as jest.Mock).mockReturnValue({ (client as jest.Mock).mockReturnValue({
databases: { databases: {
createIfNotExists: () => { createIfNotExists: () => {

View File

@@ -35,7 +35,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
); );
try { try {
let collection: DataModels.Collection; let collection: DataModels.Collection;
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) { if (userContext.authType === AuthType.AAD && !userContext.useSDKOperations) {
if (params.createNewDatabase) { if (params.createNewDatabase) {
const createDatabaseParams: DataModels.CreateDatabaseParams = { const createDatabaseParams: DataModels.CreateDatabaseParams = {
autoPilotMaxThroughput: params.autoPilotMaxThroughput, autoPilotMaxThroughput: params.autoPilotMaxThroughput,

View File

@@ -34,7 +34,7 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P
if (userContext.defaultExperience === DefaultAccountExperienceType.Table) { if (userContext.defaultExperience === DefaultAccountExperienceType.Table) {
throw new Error("Creating database resources is not allowed for tables accounts"); throw new Error("Creating database resources is not allowed for tables accounts");
} }
const database: DataModels.Database = await (window.authType === AuthType.AAD && !userContext.useSDKOperations const database: DataModels.Database = await (userContext.authType === AuthType.AAD && !userContext.useSDKOperations
? createDatabaseWithARM(params) ? createDatabaseWithARM(params)
: createDatabaseWithSDK(params)); : createDatabaseWithSDK(params));

View File

@@ -22,7 +22,7 @@ export async function createStoredProcedure(
const clearMessage = logConsoleProgress(`Creating stored procedure ${storedProcedure.id}`); const clearMessage = logConsoleProgress(`Creating stored procedure ${storedProcedure.id}`);
try { try {
if ( if (
window.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB
) { ) {

View File

@@ -19,7 +19,7 @@ export async function createTrigger(
const clearMessage = logConsoleProgress(`Creating trigger ${trigger.id}`); const clearMessage = logConsoleProgress(`Creating trigger ${trigger.id}`);
try { try {
if ( if (
window.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB
) { ) {

View File

@@ -22,7 +22,7 @@ export async function createUserDefinedFunction(
const clearMessage = logConsoleProgress(`Creating user defined function ${userDefinedFunction.id}`); const clearMessage = logConsoleProgress(`Creating user defined function ${userDefinedFunction.id}`);
try { try {
if ( if (
window.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB
) { ) {

View File

@@ -20,13 +20,17 @@ describe("deleteCollection", () => {
}); });
it("should call ARM if logged in with AAD", async () => { it("should call ARM if logged in with AAD", async () => {
window.authType = AuthType.AAD; updateUserContext({
authType: AuthType.AAD,
});
await deleteCollection("database", "collection"); await deleteCollection("database", "collection");
expect(armRequest).toHaveBeenCalled(); expect(armRequest).toHaveBeenCalled();
}); });
it("should call SDK if not logged in with non-AAD method", async () => { it("should call SDK if not logged in with non-AAD method", async () => {
window.authType = AuthType.MasterKey; updateUserContext({
authType: AuthType.MasterKey,
});
(client as jest.Mock).mockReturnValue({ (client as jest.Mock).mockReturnValue({
database: () => { database: () => {
return { return {

View File

@@ -13,7 +13,7 @@ import { client } from "../CosmosClient";
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> { export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`); const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
try { try {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) { if (userContext.authType === AuthType.AAD && !userContext.useSDKOperations) {
await deleteCollectionWithARM(databaseId, collectionId); await deleteCollectionWithARM(databaseId, collectionId);
} else { } else {
await client().database(databaseId).container(collectionId).delete(); await client().database(databaseId).container(collectionId).delete();

View File

@@ -20,13 +20,17 @@ describe("deleteDatabase", () => {
}); });
it("should call ARM if logged in with AAD", async () => { it("should call ARM if logged in with AAD", async () => {
window.authType = AuthType.AAD; updateUserContext({
authType: AuthType.AAD,
});
await deleteDatabase("database"); await deleteDatabase("database");
expect(armRequest).toHaveBeenCalled(); expect(armRequest).toHaveBeenCalled();
}); });
it("should call SDK if not logged in with non-AAD method", async () => { it("should call SDK if not logged in with non-AAD method", async () => {
window.authType = AuthType.MasterKey; updateUserContext({
authType: AuthType.MasterKey,
});
(client as jest.Mock).mockReturnValue({ (client as jest.Mock).mockReturnValue({
database: () => { database: () => {
return { return {

View File

@@ -16,7 +16,7 @@ export async function deleteDatabase(databaseId: string): Promise<void> {
if (userContext.defaultExperience === DefaultAccountExperienceType.Table) { if (userContext.defaultExperience === DefaultAccountExperienceType.Table) {
throw new Error("Deleting database resources is not allowed for tables accounts"); throw new Error("Deleting database resources is not allowed for tables accounts");
} }
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) { if (userContext.authType === AuthType.AAD && !userContext.useSDKOperations) {
await deleteDatabaseWithARM(databaseId); await deleteDatabaseWithARM(databaseId);
} else { } else {
await client().database(databaseId).delete(); await client().database(databaseId).delete();

View File

@@ -14,7 +14,7 @@ export async function deleteStoredProcedure(
const clearMessage = logConsoleProgress(`Deleting stored procedure ${storedProcedureId}`); const clearMessage = logConsoleProgress(`Deleting stored procedure ${storedProcedureId}`);
try { try {
if ( if (
window.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB
) { ) {

View File

@@ -10,7 +10,7 @@ export async function deleteTrigger(databaseId: string, collectionId: string, tr
const clearMessage = logConsoleProgress(`Deleting trigger ${triggerId}`); const clearMessage = logConsoleProgress(`Deleting trigger ${triggerId}`);
try { try {
if ( if (
window.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB
) { ) {

View File

@@ -10,7 +10,7 @@ export async function deleteUserDefinedFunction(databaseId: string, collectionId
const clearMessage = logConsoleProgress(`Deleting user defined function ${id}`); const clearMessage = logConsoleProgress(`Deleting user defined function ${id}`);
try { try {
if ( if (
window.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB
) { ) {

View File

@@ -41,7 +41,7 @@ interface MetricsResponse {
} }
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => { export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
if (window.authType !== AuthType.AAD) { if (userContext.authType !== AuthType.AAD) {
return undefined; return undefined;
} }
@@ -76,7 +76,7 @@ export const getCollectionUsageSizeInKB = async (databaseName: string, container
return dataUsageSizeInKb + indexUsageSizeInKb; return dataUsageSizeInKb + indexUsageSizeInKb;
} catch (error) { } catch (error) {
handleError(error, "getCollectionUsageSize"); handleError(error, "getCollectionUsageSize");
throw error; return undefined;
} }
}; };

View File

@@ -3,9 +3,10 @@ import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import * as Constants from "../Constants"; import * as Constants from "../Constants";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
export async function getIndexTransformationProgress(databaseId: string, collectionId: string): Promise<number> { export async function getIndexTransformationProgress(databaseId: string, collectionId: string): Promise<number> {
if (window.authType !== AuthType.AAD) { if (userContext.authType !== AuthType.AAD) {
return undefined; return undefined;
} }
let indexTransformationPercentage: number; let indexTransformationPercentage: number;

View File

@@ -9,6 +9,7 @@ import { updateUserContext } from "../../UserContext";
describe("readCollection", () => { describe("readCollection", () => {
beforeAll(() => { beforeAll(() => {
updateUserContext({ updateUserContext({
authType: AuthType.ResourceToken,
databaseAccount: { databaseAccount: {
name: "test", name: "test",
} as DatabaseAccount, } as DatabaseAccount,
@@ -17,7 +18,6 @@ describe("readCollection", () => {
}); });
it("should call SDK if logged in with resource token", async () => { it("should call SDK if logged in with resource token", async () => {
window.authType = AuthType.ResourceToken;
(client as jest.Mock).mockReturnValue({ (client as jest.Mock).mockReturnValue({
database: () => { database: () => {
return { return {

View File

@@ -16,7 +16,7 @@ export const readCollectionOffer = async (params: ReadCollectionOfferParams): Pr
try { try {
if ( if (
window.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table userContext.defaultExperience !== DefaultAccountExperienceType.Table
) { ) {

View File

@@ -19,13 +19,17 @@ describe("readCollections", () => {
}); });
it("should call ARM if logged in with AAD", async () => { it("should call ARM if logged in with AAD", async () => {
window.authType = AuthType.AAD; updateUserContext({
authType: AuthType.AAD,
});
await readCollections("database"); await readCollections("database");
expect(armRequest).toHaveBeenCalled(); expect(armRequest).toHaveBeenCalled();
}); });
it("should call SDK if not logged in with non-AAD method", async () => { it("should call SDK if not logged in with non-AAD method", async () => {
window.authType = AuthType.MasterKey; updateUserContext({
authType: AuthType.MasterKey,
});
(client as jest.Mock).mockReturnValue({ (client as jest.Mock).mockReturnValue({
database: () => { database: () => {
return { return {

View File

@@ -15,7 +15,7 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`); const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
try { try {
if ( if (
window.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB && userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table userContext.defaultExperience !== DefaultAccountExperienceType.Table

View File

@@ -15,7 +15,7 @@ export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promis
try { try {
if ( if (
window.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table userContext.defaultExperience !== DefaultAccountExperienceType.Table
) { ) {

View File

@@ -19,13 +19,17 @@ describe("readDatabases", () => {
}); });
it("should call ARM if logged in with AAD", async () => { it("should call ARM if logged in with AAD", async () => {
window.authType = AuthType.AAD; updateUserContext({
authType: AuthType.AAD,
});
await readDatabases(); await readDatabases();
expect(armRequest).toHaveBeenCalled(); expect(armRequest).toHaveBeenCalled();
}); });
it("should call SDK if not logged in with non-AAD method", async () => { it("should call SDK if not logged in with non-AAD method", async () => {
window.authType = AuthType.MasterKey; updateUserContext({
authType: AuthType.MasterKey,
});
(client as jest.Mock).mockReturnValue({ (client as jest.Mock).mockReturnValue({
databases: { databases: {
readAll: () => { readAll: () => {

View File

@@ -15,7 +15,7 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
const clearMessage = logConsoleProgress(`Querying databases`); const clearMessage = logConsoleProgress(`Querying databases`);
try { try {
if ( if (
window.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table userContext.defaultExperience !== DefaultAccountExperienceType.Table
) { ) {

View File

@@ -9,7 +9,7 @@ export async function readMongoDBCollectionThroughRP(
databaseId: string, databaseId: string,
collectionId: string collectionId: string
): Promise<MongoDBCollectionResource> { ): Promise<MongoDBCollectionResource> {
if (window.authType !== AuthType.AAD) { if (userContext.authType !== AuthType.AAD) {
return undefined; return undefined;
} }
let collection: MongoDBCollectionResource; let collection: MongoDBCollectionResource;

View File

@@ -14,7 +14,7 @@ export async function readStoredProcedures(
const clearMessage = logConsoleProgress(`Querying stored procedures for container ${collectionId}`); const clearMessage = logConsoleProgress(`Querying stored procedures for container ${collectionId}`);
try { try {
if ( if (
window.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB
) { ) {

View File

@@ -14,7 +14,7 @@ export async function readTriggers(
const clearMessage = logConsoleProgress(`Querying triggers for container ${collectionId}`); const clearMessage = logConsoleProgress(`Querying triggers for container ${collectionId}`);
try { try {
if ( if (
window.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB
) { ) {

View File

@@ -14,7 +14,7 @@ export async function readUserDefinedFunctions(
const clearMessage = logConsoleProgress(`Querying user defined functions for container ${collectionId}`); const clearMessage = logConsoleProgress(`Querying user defined functions for container ${collectionId}`);
try { try {
if ( if (
window.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB
) { ) {

View File

@@ -41,7 +41,7 @@ export async function updateCollection(
try { try {
if ( if (
window.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB && userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table userContext.defaultExperience !== DefaultAccountExperienceType.Table

View File

@@ -58,7 +58,7 @@ export const updateOffer = async (params: UpdateOfferParams): Promise<Offer> =>
const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`); const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`);
try { try {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) { if (userContext.authType === AuthType.AAD && !userContext.useSDKOperations) {
if (params.collectionId) { if (params.collectionId) {
updatedOffer = await updateCollectionOfferWithARM(params); updatedOffer = await updateCollectionOfferWithARM(params);
} else if (userContext.defaultExperience === DefaultAccountExperienceType.Table) { } else if (userContext.defaultExperience === DefaultAccountExperienceType.Table) {

View File

@@ -22,7 +22,7 @@ export async function updateStoredProcedure(
const clearMessage = logConsoleProgress(`Updating stored procedure ${storedProcedure.id}`); const clearMessage = logConsoleProgress(`Updating stored procedure ${storedProcedure.id}`);
try { try {
if ( if (
window.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB
) { ) {

View File

@@ -19,7 +19,7 @@ export async function updateTrigger(
const clearMessage = logConsoleProgress(`Updating trigger ${trigger.id}`); const clearMessage = logConsoleProgress(`Updating trigger ${trigger.id}`);
try { try {
if ( if (
window.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB
) { ) {

View File

@@ -22,7 +22,7 @@ export async function updateUserDefinedFunction(
const clearMessage = logConsoleProgress(`Updating user defined function ${userDefinedFunction.id}`); const clearMessage = logConsoleProgress(`Updating user defined function ${userDefinedFunction.id}`);
try { try {
if ( if (
window.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB
) { ) {

View File

@@ -4,7 +4,7 @@ export enum Platform {
Emulator = "Emulator", Emulator = "Emulator",
} }
interface ConfigContext { export interface ConfigContext {
platform: Platform; platform: Platform;
allowedParentFrameOrigins: string[]; allowedParentFrameOrigins: string[];
gitSha?: string; gitSha?: string;
@@ -79,7 +79,11 @@ if (process.env.NODE_ENV === "development") {
export async function initializeConfiguration(): Promise<ConfigContext> { export async function initializeConfiguration(): Promise<ConfigContext> {
try { try {
const response = await fetch("./config.json"); const response = await fetch("./config.json", {
headers: {
"If-None-Match": "", // disable client side cache
},
});
if (response.status === 200) { if (response.status === 200) {
try { try {
const { allowedParentFrameOrigins, ...externalConfig } = await response.json(); const { allowedParentFrameOrigins, ...externalConfig } = await response.json();

View File

@@ -33,7 +33,6 @@ export enum MessageTypes {
CreateWorkspace, CreateWorkspace,
CreateSparkPool, CreateSparkPool,
RefreshDatabaseAccount, RefreshDatabaseAccount,
InitTestExplorer,
} }
export { Versions, ActionContracts, Diagnostics }; export { Versions, ActionContracts, Diagnostics };

View File

@@ -5,7 +5,6 @@ import {
TriggerDefinition, TriggerDefinition,
UserDefinedFunctionDefinition, UserDefinedFunctionDefinition,
} from "@azure/cosmos"; } from "@azure/cosmos";
import Q from "q";
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
@@ -92,6 +91,7 @@ export interface Database extends TreeNode {
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void; onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
onSettingsClick: () => void; onSettingsClick: () => void;
loadOffer(): Promise<void>; loadOffer(): Promise<void>;
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
} }
export interface CollectionBase extends TreeNode { export interface CollectionBase extends TreeNode {
@@ -108,8 +108,8 @@ export interface CollectionBase extends TreeNode {
isCollectionExpanded: ko.Observable<boolean>; isCollectionExpanded: ko.Observable<boolean>;
onDocumentDBDocumentsClick(): void; onDocumentDBDocumentsClick(): void;
onNewQueryClick(source: any, event: MouseEvent, queryText?: string): void; onNewQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
expandCollection(): Q.Promise<any>; expandCollection(): void;
collapseCollection(): void; collapseCollection(): void;
getDatabase(): Database; getDatabase(): Database;
} }
@@ -138,14 +138,13 @@ export interface Collection extends CollectionBase {
openTab(): void; openTab(): void;
onSettingsClick: () => Promise<void>; onSettingsClick: () => Promise<void>;
onDeleteCollectionContextMenuClick(source: Collection, event: MouseEvent): void;
onNewGraphClick(): void; onNewGraphClick(): void;
onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string): void; onNewMongoQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
onNewMongoShellClick(): void; onNewMongoShellClick(): void;
onNewStoredProcedureClick(source: Collection, event: MouseEvent): void; onNewStoredProcedureClick(source: Collection, event?: MouseEvent): void;
onNewUserDefinedFunctionClick(source: Collection, event: MouseEvent): void; onNewUserDefinedFunctionClick(source: Collection, event?: MouseEvent): void;
onNewTriggerClick(source: Collection, event: MouseEvent): void; onNewTriggerClick(source: Collection, event?: MouseEvent): void;
storedProcedures: ko.Computed<StoredProcedure[]>; storedProcedures: ko.Computed<StoredProcedure[]>;
userDefinedFunctions: ko.Computed<UserDefinedFunction[]>; userDefinedFunctions: ko.Computed<UserDefinedFunction[]>;
triggers: ko.Computed<Trigger[]>; triggers: ko.Computed<Trigger[]>;
@@ -176,9 +175,10 @@ export interface Collection extends CollectionBase {
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void; onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
onDrop(source: Collection, event: { originalEvent: DragEvent }): void; onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
uploadFiles(fileList: FileList): Q.Promise<UploadDetails>; uploadFiles(fileList: FileList): Promise<UploadDetails>;
getLabel(): string; getLabel(): string;
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
} }
/** /**
@@ -293,10 +293,6 @@ export interface DocumentsTabOptions extends TabOptions {
resourceTokenPartitionKey?: string; resourceTokenPartitionKey?: string;
} }
export interface SettingsTabV2Options extends TabOptions {
getPendingNotification: Q.Promise<DataModels.Notification>;
}
export interface ConflictsTabOptions extends TabOptions { export interface ConflictsTabOptions extends TabOptions {
partitionKey: DataModels.PartitionKey; partitionKey: DataModels.PartitionKey;
conflictIds: ko.ObservableArray<ConflictId>; conflictIds: ko.ObservableArray<ConflictId>;
@@ -359,11 +355,12 @@ export enum CollectionTabKind {
Notebook = 13 /* Deprecated */, Notebook = 13 /* Deprecated */,
Terminal = 14, Terminal = 14,
NotebookV2 = 15, NotebookV2 = 15,
SparkMasterTab = 16, SparkMasterTab = 16 /* Deprecated */,
Gallery = 17, Gallery = 17,
NotebookViewer = 18, NotebookViewer = 18,
Schema = 19, Schema = 19,
SettingsV2 = 20, CollectionSettingsV2 = 20,
DatabaseSettingsV2 = 21,
} }
export enum TerminalKind { export enum TerminalKind {
@@ -374,20 +371,20 @@ export enum TerminalKind {
export interface DataExplorerInputsFrame { export interface DataExplorerInputsFrame {
databaseAccount: any; databaseAccount: any;
subscriptionId: string; subscriptionId?: string;
resourceGroup: string; resourceGroup?: string;
masterKey: string; masterKey?: string;
hasWriteAccess: boolean; hasWriteAccess?: boolean;
authorizationToken: string; authorizationToken?: string;
features: any; features: { [key: string]: string };
csmEndpoint: string; csmEndpoint?: string;
dnsSuffix: string; dnsSuffix?: string;
serverId: string; serverId?: string;
extensionEndpoint: string; extensionEndpoint?: string;
subscriptionType: SubscriptionType; subscriptionType?: SubscriptionType;
quotaId: string; quotaId?: string;
addCollectionDefaultFlight: string; addCollectionDefaultFlight?: string;
isTryCosmosDBSubscription: boolean; isTryCosmosDBSubscription?: boolean;
loadDatabaseAccountTimestamp?: number; loadDatabaseAccountTimestamp?: number;
sharedThroughputMinimum?: number; sharedThroughputMinimum?: number;
sharedThroughputMaximum?: number; sharedThroughputMaximum?: number;

View File

@@ -20,10 +20,6 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("graph-style")).toBe(true); expect(ko.components.isRegistered("graph-style")).toBe(true);
}); });
it("should register collapsible-panel component", () => {
expect(ko.components.isRegistered("collapsible-panel")).toBe(true);
});
it("should register json-editor component", () => { it("should register json-editor component", () => {
expect(ko.components.isRegistered("json-editor")).toBe(true); expect(ko.components.isRegistered("json-editor")).toBe(true);
}); });
@@ -45,7 +41,8 @@ describe("Component Registerer", () => {
}); });
it("should register settings-tab-v2 component", () => { it("should register settings-tab-v2 component", () => {
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true); expect(ko.components.isRegistered("database-settings-tab-v2")).toBe(true);
expect(ko.components.isRegistered("collection-settings-tab-v2")).toBe(true);
}); });
it("should register query-tab component", () => { it("should register query-tab component", () => {
@@ -68,10 +65,6 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("terminal-tab")).toBe(true); expect(ko.components.isRegistered("terminal-tab")).toBe(true);
}); });
it("should register spark-master-tab component", () => {
expect(ko.components.isRegistered("spark-master-tab")).toBe(true);
});
it("should register mongo-shell-tab component", () => { it("should register mongo-shell-tab component", () => {
expect(ko.components.isRegistered("mongo-shell-tab")).toBe(true); expect(ko.components.isRegistered("mongo-shell-tab")).toBe(true);
}); });

View File

@@ -1,7 +1,6 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as PaneComponents from "./Panes/PaneComponents"; import * as PaneComponents from "./Panes/PaneComponents";
import * as TabComponents from "./Tabs/TabComponents"; import * as TabComponents from "./Tabs/TabComponents";
import { CollapsiblePanelComponent } from "./Controls/CollapsiblePanel/CollapsiblePanelComponent";
import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent"; import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent";
import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent"; import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent";
import { EditorComponent } from "./Controls/Editor/EditorComponent"; import { EditorComponent } from "./Controls/Editor/EditorComponent";
@@ -17,7 +16,6 @@ ko.components.register("input-typeahead", new InputTypeaheadComponent());
ko.components.register("new-vertex-form", NewVertexComponent); ko.components.register("new-vertex-form", NewVertexComponent);
ko.components.register("error-display", new ErrorDisplayComponent()); ko.components.register("error-display", new ErrorDisplayComponent());
ko.components.register("graph-style", GraphStyleComponent); ko.components.register("graph-style", GraphStyleComponent);
ko.components.register("collapsible-panel", new CollapsiblePanelComponent());
ko.components.register("editor", new EditorComponent()); ko.components.register("editor", new EditorComponent());
ko.components.register("json-editor", new JsonEditorComponent()); ko.components.register("json-editor", new JsonEditorComponent());
ko.components.register("diff-editor", new DiffEditorComponent()); ko.components.register("diff-editor", new DiffEditorComponent());
@@ -31,7 +29,7 @@ ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTa
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab()); ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
ko.components.register("trigger-tab", new TabComponents.TriggerTab()); ko.components.register("trigger-tab", new TabComponents.TriggerTab());
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab()); ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
ko.components.register("settings-tab-v2", new TabComponents.SettingsTabV2()); ko.components.register("collection-settings-tab-v2", new TabComponents.SettingsTabV2());
ko.components.register("query-tab", new TabComponents.QueryTab()); ko.components.register("query-tab", new TabComponents.QueryTab());
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab()); ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
ko.components.register("graph-tab", new TabComponents.GraphTab()); ko.components.register("graph-tab", new TabComponents.GraphTab());
@@ -39,12 +37,12 @@ ko.components.register("mongo-shell-tab", new TabComponents.MongoShellTab());
ko.components.register("conflicts-tab", new TabComponents.ConflictsTab()); ko.components.register("conflicts-tab", new TabComponents.ConflictsTab());
ko.components.register("notebookv2-tab", new TabComponents.NotebookV2Tab()); ko.components.register("notebookv2-tab", new TabComponents.NotebookV2Tab());
ko.components.register("terminal-tab", new TabComponents.TerminalTab()); ko.components.register("terminal-tab", new TabComponents.TerminalTab());
ko.components.register("spark-master-tab", new TabComponents.SparkMasterTab());
ko.components.register("gallery-tab", new TabComponents.GalleryTab()); ko.components.register("gallery-tab", new TabComponents.GalleryTab());
ko.components.register("notebook-viewer-tab", new TabComponents.NotebookViewerTab()); ko.components.register("notebook-viewer-tab", new TabComponents.NotebookViewerTab());
// Database Tabs // Database Tabs
ko.components.register("database-settings-tab", new TabComponents.DatabaseSettingsTab()); ko.components.register("database-settings-tab", new TabComponents.DatabaseSettingsTab());
ko.components.register("database-settings-tab-v2", new TabComponents.SettingsTabV2());
// Panes // Panes
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent()); ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
@@ -66,7 +64,6 @@ ko.components.register("table-query-select-pane", new PaneComponents.TableQueryS
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent()); ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());
ko.components.register("settings-pane", new PaneComponents.SettingsPaneComponent()); ko.components.register("settings-pane", new PaneComponents.SettingsPaneComponent());
ko.components.register("execute-sproc-params-pane", new PaneComponents.ExecuteSprocParamsComponent()); ko.components.register("execute-sproc-params-pane", new PaneComponents.ExecuteSprocParamsComponent());
ko.components.register("renew-adhoc-access-pane", new PaneComponents.RenewAdHocAccessPane());
ko.components.register("upload-items-pane", new PaneComponents.UploadItemsPaneComponent()); ko.components.register("upload-items-pane", new PaneComponents.UploadItemsPaneComponent());
ko.components.register("load-query-pane", new PaneComponents.LoadQueryPaneComponent()); ko.components.register("load-query-pane", new PaneComponents.LoadQueryPaneComponent());
ko.components.register("save-query-pane", new PaneComponents.SaveQueryPaneComponent()); ko.components.register("save-query-pane", new PaneComponents.SaveQueryPaneComponent());

View File

@@ -112,10 +112,7 @@ export class ResourceTreeContextMenuButtonFactory {
items.push({ items.push({
iconSrc: DeleteCollectionIcon, iconSrc: DeleteCollectionIcon,
onClick: () => { onClick: () => container.openDeleteCollectionConfirmationPane(),
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null);
},
label: container.deleteCollectionText(), label: container.deleteCollectionText(),
styleClass: "deleteCollectionMenuItem", styleClass: "deleteCollectionMenuItem",
}); });

View File

@@ -1,56 +0,0 @@
import * as ko from "knockout";
import template from "./collapsible-panel-component.html";
/**
* Helper class for ko component registration
*/
export class CollapsiblePanelComponent {
constructor() {
return {
viewModel: CollapsiblePanelViewModel,
template,
};
}
}
/**
* Parameters for this component
*/
interface CollapsiblePanelParams {
collapsedTitle: ko.Observable<string>;
expandedTitle: ko.Observable<string>;
isCollapsed?: ko.Observable<boolean>;
collapseToLeft?: boolean;
}
/**
* Collapsible panel:
* Contains a header with [>] button to collapse and an title ("expandedTitle").
* Collapsing the panel:
* - shrinks width to narrow amount
* - hides children
* - shows [<]
* - shows vertical title ("collapsedTitle")
* - the default behavior is to collapse to the right (ie, place this component on the right or use "collapseToLeft" parameter)
*
* How to use in your markup:
* <collapsible-panel params="{ collapsedTitle:'Properties', expandedTitle:'Expanded properties' }">
* <!-- add your markup here: the ko context is the same as outside of collapsible-panel (ie $data) -->
* </collapsible-panel>
*
* Use the optional "isCollapsed" parameter to programmatically collapse/expand the pane from outside the component.
* Use the optional "collapseToLeft" parameter to collapse to the left.
*/
class CollapsiblePanelViewModel {
public params: CollapsiblePanelParams;
private isCollapsed: ko.Observable<boolean>;
public constructor(params: CollapsiblePanelParams) {
this.params = params;
this.isCollapsed = params.isCollapsed || ko.observable(false);
}
public toggleCollapse(): void {
this.isCollapsed(!this.isCollapsed());
}
}

View File

@@ -1,44 +0,0 @@
<div class="collapsiblePanel" data-bind="css: { paneCollapsed:isCollapsed() }">
<div class="panelHeader" data-bind="visible: !isCollapsed()">
<span
class="collapsedIconContainer collapseExpandButton"
data-bind="click:toggleCollapse, css: { 'pull-right':params.collapseToLeft }"
>
<img
class="collapsedIcon imgVerticalAlignment"
src="/imgarrowlefticon.svg"
alt="Collapse"
data-bind="css: { expanded:!isCollapsed(), iconMirror:params.collapseToLeft }"
/>
</span>
<span
class="expandedTitle"
data-bind="text: params.expandedTitle, css:{ iconSpacer:!params.collapseToLeft }"
></span>
</div>
<div class="collapsibleNav nav" data-bind="visible:isCollapsed">
<ul class="nav">
<li class="collapsedBtn collapseExpandButton">
<span class="collapsedIconContainer" data-bind="click: toggleCollapse">
<img
class="collapsedIcon"
src="/imgarrowlefticon.svg"
data-bind="css: { expanded:!isCollapsed(), iconMirror:params.collapseToLeft }"
alt="Expand"
/>
</span>
<span class="rotatedInner" data-bind="click: toggleCollapse">
<span data-bind="text: params.collapsedTitle"></span>
</span>
</li>
</ul>
</div>
<div class="panelContent" data-bind="visible:!isCollapsed()">
<!-- ko with:$parent -->
<!-- ko template: { nodes: $componentTemplateNodes } -->
<!-- /ko -->
<!-- /ko -->
</div>
</div>

View File

@@ -1,9 +1,15 @@
import * as React from "react"; import * as React from "react";
import { Dialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric-react/lib/Dialog"; import { Dialog as FluentDialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric-react/lib/Dialog";
import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button"; import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button";
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField"; import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
import { Link } from "office-ui-fabric-react/lib/Link"; import { Link } from "office-ui-fabric-react/lib/Link";
import { ChoiceGroup, FontIcon, IChoiceGroupProps } from "office-ui-fabric-react"; import {
ChoiceGroup,
FontIcon,
IChoiceGroupProps,
IProgressIndicatorProps,
ProgressIndicator,
} from "office-ui-fabric-react";
export interface TextFieldProps extends ITextFieldProps { export interface TextFieldProps extends ITextFieldProps {
label: string; label: string;
@@ -27,6 +33,7 @@ export interface DialogProps {
choiceGroupProps?: IChoiceGroupProps; choiceGroupProps?: IChoiceGroupProps;
textFieldProps?: TextFieldProps; textFieldProps?: TextFieldProps;
linkProps?: LinkProps; linkProps?: LinkProps;
progressIndicatorProps?: IProgressIndicatorProps;
primaryButtonText: string; primaryButtonText: string;
secondaryButtonText: string; secondaryButtonText: string;
onPrimaryButtonClick: () => void; onPrimaryButtonClick: () => void;
@@ -43,7 +50,7 @@ const DIALOG_TITLE_FONT_SIZE = "17px";
const DIALOG_TITLE_FONT_WEIGHT = 400; const DIALOG_TITLE_FONT_WEIGHT = 400;
const DIALOG_SUBTEXT_FONT_SIZE = "15px"; const DIALOG_SUBTEXT_FONT_SIZE = "15px";
export class DialogComponent extends React.Component<DialogProps, {}> { export class Dialog extends React.Component<DialogProps> {
constructor(props: DialogProps) { constructor(props: DialogProps) {
super(props); super(props);
} }
@@ -62,13 +69,14 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
showCloseButton: this.props.showCloseButton || false, showCloseButton: this.props.showCloseButton || false,
onDismiss: this.props.onDismiss, onDismiss: this.props.onDismiss,
}, },
modalProps: { isBlocking: this.props.isModal }, modalProps: { isBlocking: this.props.isModal, isDarkOverlay: false },
minWidth: DIALOG_MIN_WIDTH, minWidth: DIALOG_MIN_WIDTH,
maxWidth: DIALOG_MAX_WIDTH, maxWidth: DIALOG_MAX_WIDTH,
}; };
const choiceGroupProps: IChoiceGroupProps = this.props.choiceGroupProps; const choiceGroupProps: IChoiceGroupProps = this.props.choiceGroupProps;
const textFieldProps: ITextFieldProps = this.props.textFieldProps; const textFieldProps: ITextFieldProps = this.props.textFieldProps;
const linkProps: LinkProps = this.props.linkProps; const linkProps: LinkProps = this.props.linkProps;
const progressIndicatorProps: IProgressIndicatorProps = this.props.progressIndicatorProps;
const primaryButtonProps: IButtonProps = { const primaryButtonProps: IButtonProps = {
text: this.props.primaryButtonText, text: this.props.primaryButtonText,
disabled: this.props.primaryButtonDisabled || false, disabled: this.props.primaryButtonDisabled || false,
@@ -83,7 +91,7 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
: undefined; : undefined;
return ( return (
<Dialog {...dialogProps}> <FluentDialog {...dialogProps}>
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />} {choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}
{textFieldProps && <TextField {...textFieldProps} />} {textFieldProps && <TextField {...textFieldProps} />}
{linkProps && ( {linkProps && (
@@ -91,11 +99,12 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" /> {linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
</Link> </Link>
)} )}
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
<DialogFooter> <DialogFooter>
<PrimaryButton {...primaryButtonProps} /> <PrimaryButton {...primaryButtonProps} />
{secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />} {secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />}
</DialogFooter> </DialogFooter>
</Dialog> </FluentDialog>
); );
} }
} }

View File

@@ -1,16 +0,0 @@
/**
* This adapter is responsible to render the Dialog React component
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
* and update any knockout observables passed from the parent.
*/
import * as React from "react";
import { DialogComponent, DialogProps } from "./DialogComponent";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
export class DialogComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<DialogProps>;
public renderComponent(): JSX.Element {
return <DialogComponent {...this.parameters()} />;
}
}

View File

@@ -47,13 +47,7 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
{ key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" }, { key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" },
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" }, { key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
{ key: "feature.enablettl", label: "Enable TTL", value: "true" }, { key: "feature.enablettl", label: "Enable TTL", value: "true" },
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
{ key: "feature.selfServeType", label: "Self serve feature", value: "sample" }, { key: "feature.selfServeType", label: "Self serve feature", value: "sample" },
{
key: "feature.enableLinkInjection",
label: "Enable Injecting Notebook Viewer Link into the first cell",
value: "true",
},
{ key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" }, { key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" },
{ {
key: "feature.enablefixedcollectionwithsharedthroughput", key: "feature.enablefixedcollectionwithsharedthroughput",

View File

@@ -149,12 +149,6 @@ exports[`Feature panel renders all flags 1`] = `
label="Enable TTL" label="Enable TTL"
onChange={[Function]} onChange={[Function]}
/> />
<StyledCheckboxBase
checked={false}
key="feature.enablegallerypublish"
label="Enable Notebook Gallery Publishing"
onChange={[Function]}
/>
<StyledCheckboxBase <StyledCheckboxBase
checked={false} checked={false}
key="feature.selfServeType" key="feature.selfServeType"
@@ -163,8 +157,8 @@ exports[`Feature panel renders all flags 1`] = `
/> />
<StyledCheckboxBase <StyledCheckboxBase
checked={false} checked={false}
key="feature.enableLinkInjection" key="feature.canexceedmaximumvalue"
label="Enable Injecting Notebook Viewer Link into the first cell" label="Can exceed max value"
onChange={[Function]} onChange={[Function]}
/> />
</Stack> </Stack>
@@ -172,12 +166,6 @@ exports[`Feature panel renders all flags 1`] = `
className="checkboxRow" className="checkboxRow"
horizontalAlign="space-between" horizontalAlign="space-between"
> >
<StyledCheckboxBase
checked={false}
key="feature.canexceedmaximumvalue"
label="Can exceed max value"
onChange={[Function]}
/>
<StyledCheckboxBase <StyledCheckboxBase
checked={false} checked={false}
key="feature.enablefixedcollectionwithsharedthroughput" key="feature.enablefixedcollectionwithsharedthroughput"

View File

@@ -18,7 +18,6 @@ import * as React from "react";
import { IGalleryItem } from "../../../../Juno/JunoClient"; import { IGalleryItem } from "../../../../Juno/JunoClient";
import { FileSystemUtil } from "../../../Notebook/FileSystemUtil"; import { FileSystemUtil } from "../../../Notebook/FileSystemUtil";
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg"; import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
import { StyleConstants } from "../../../../Common/Constants";
export interface GalleryCardComponentProps { export interface GalleryCardComponentProps {
data: IGalleryItem; data: IGalleryItem;
@@ -38,7 +37,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
private static readonly cardImageHeight = 144; private static readonly cardImageHeight = 144;
public static readonly cardHeightToWidthRatio = public static readonly cardHeightToWidthRatio =
GalleryCardComponent.cardImageHeight / GalleryCardComponent.CARD_WIDTH; GalleryCardComponent.cardImageHeight / GalleryCardComponent.CARD_WIDTH;
private static readonly cardDescriptionMaxChars = 88; private static readonly cardDescriptionMaxChars = 80;
private static readonly cardItemGapBig = 10; private static readonly cardItemGapBig = 10;
private static readonly cardItemGapSmall = 8; private static readonly cardItemGapSmall = 8;
@@ -54,6 +53,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
return ( return (
<Card <Card
style={{ background: "white" }}
aria-label={cardTitle} aria-label={cardTitle}
data-is-focusable="true" data-is-focusable="true"
tokens={{ width: GalleryCardComponent.CARD_WIDTH, childrenGap: 0 }} tokens={{ width: GalleryCardComponent.CARD_WIDTH, childrenGap: 0 }}
@@ -79,12 +79,16 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
<Card.Section styles={{ root: { padding: GalleryCardComponent.cardItemGapBig } }}> <Card.Section styles={{ root: { padding: GalleryCardComponent.cardItemGapBig } }}>
<Text variant="small" nowrap> <Text variant="small" nowrap>
{this.props.data.tags?.map((tag, index, array) => ( {this.props.data.tags ? (
<span key={tag}> this.props.data.tags.map((tag, index, array) => (
<Link onClick={(event) => this.onClick(event, () => this.props.onTagClick(tag))}>{tag}</Link> <span key={tag}>
{index === array.length - 1 ? <></> : ", "} <Link onClick={(event) => this.onClick(event, () => this.props.onTagClick(tag))}>{tag}</Link>
</span> {index === array.length - 1 ? <></> : ", "}
))} </span>
))
) : (
<br />
)}
</Text> </Text>
<Text <Text
@@ -101,13 +105,14 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
</Text> </Text>
<Text variant="small" styles={{ root: { height: 36 } }}> <Text variant="small" styles={{ root: { height: 36 } }}>
{this.props.data.description.substr(0, GalleryCardComponent.cardDescriptionMaxChars)} {this.renderTruncatedDescription()}
</Text> </Text>
<span> <span>
{this.generateIconText("RedEye", this.props.data.views.toString())} {this.props.data.views !== undefined && this.generateIconText("RedEye", this.props.data.views.toString())}
{this.generateIconText("Download", this.props.data.downloads.toString())} {this.props.data.downloads !== undefined &&
{this.props.isFavorite !== undefined && this.generateIconText("Download", this.props.data.downloads.toString())}
{this.props.data.favorites !== undefined &&
this.generateIconText("Heart", this.props.data.favorites.toString())} this.generateIconText("Heart", this.props.data.favorites.toString())}
</span> </span>
</Card.Section> </Card.Section>
@@ -127,7 +132,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
{this.props.isFavorite !== undefined && {this.props.isFavorite !== undefined &&
this.generateIconButtonWithTooltip( this.generateIconButtonWithTooltip(
this.props.isFavorite ? "HeartFill" : "Heart", this.props.isFavorite ? "HeartFill" : "Heart",
this.props.isFavorite ? "Unlike" : "Like", this.props.isFavorite ? "Unfavorite" : "Favorite",
"left", "left",
this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick
)} )}
@@ -144,12 +149,17 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
); );
} }
private renderTruncatedDescription = (): string => {
let truncatedDescription = this.props.data.description.substr(0, GalleryCardComponent.cardDescriptionMaxChars);
if (this.props.data.description.length > GalleryCardComponent.cardDescriptionMaxChars) {
truncatedDescription = `${truncatedDescription} ...`;
}
return truncatedDescription;
};
private generateIconText = (iconName: string, text: string): JSX.Element => { private generateIconText = (iconName: string, text: string): JSX.Element => {
return ( return (
<Text <Text variant="tiny" styles={{ root: { color: "#605E5C", paddingRight: GalleryCardComponent.cardItemGapSmall } }}>
variant="tiny"
styles={{ root: { color: StyleConstants.BaseMedium, paddingRight: GalleryCardComponent.cardItemGapSmall } }}
>
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} /> {text} <Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} /> {text}
</Text> </Text>
); );

View File

@@ -5,6 +5,11 @@ exports[`GalleryCardComponent renders 1`] = `
aria-label="name" aria-label="name"
data-is-focusable="true" data-is-focusable="true"
onClick={[Function]} onClick={[Function]}
style={
Object {
"background": "white",
}
}
tokens={ tokens={
Object { Object {
"childrenGap": 0, "childrenGap": 0,
@@ -88,7 +93,7 @@ exports[`GalleryCardComponent renders 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": undefined, "color": "#605E5C",
"paddingRight": 8, "paddingRight": 8,
}, },
} }
@@ -112,7 +117,7 @@ exports[`GalleryCardComponent renders 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": undefined, "color": "#605E5C",
"paddingRight": 8, "paddingRight": 8,
}, },
} }
@@ -136,7 +141,7 @@ exports[`GalleryCardComponent renders 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": undefined, "color": "#605E5C",
"paddingRight": 8, "paddingRight": 8,
}, },
} }
@@ -185,7 +190,7 @@ exports[`GalleryCardComponent renders 1`] = `
"gapSpace": 0, "gapSpace": 0,
} }
} }
content="Like" content="Favorite"
id="TooltipHost-IconButton-Heart" id="TooltipHost-IconButton-Heart"
styles={ styles={
Object { Object {
@@ -197,14 +202,14 @@ exports[`GalleryCardComponent renders 1`] = `
} }
> >
<CustomizedIconButton <CustomizedIconButton
ariaLabel="Like" ariaLabel="Favorite"
iconProps={ iconProps={
Object { Object {
"iconName": "Heart", "iconName": "Heart",
} }
} }
onClick={[Function]} onClick={[Function]}
title="Like" title="Favorite"
/> />
</StyledTooltipHostBase> </StyledTooltipHostBase>
<StyledTooltipHostBase <StyledTooltipHostBase

View File

@@ -2,7 +2,9 @@ import * as React from "react";
import { JunoClient } from "../../../Juno/JunoClient"; import { JunoClient } from "../../../Juno/JunoClient";
import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants"; import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants";
import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react"; import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react";
import { handleError } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
export interface CodeOfConductComponentProps { export interface CodeOfConductComponentProps {
junoClient: JunoClient; junoClient: JunoClient;
@@ -14,11 +16,11 @@ interface CodeOfConductComponentState {
} }
export class CodeOfConductComponent extends React.Component<CodeOfConductComponentProps, CodeOfConductComponentState> { export class CodeOfConductComponent extends React.Component<CodeOfConductComponentProps, CodeOfConductComponentState> {
private viewCodeOfConductTraced: boolean;
private descriptionPara1: string; private descriptionPara1: string;
private descriptionPara2: string; private descriptionPara2: string;
private descriptionPara3: string; private descriptionPara3: string;
private link1: { label: string; url: string }; private link1: { label: string; url: string };
private link2: { label: string; url: string };
constructor(props: CodeOfConductComponentProps) { constructor(props: CodeOfConductComponentProps) {
super(props); super(props);
@@ -27,23 +29,34 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
readCodeOfConduct: false, readCodeOfConduct: false,
}; };
this.descriptionPara1 = "Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement"; this.descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct";
this.descriptionPara2 = this.descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB.";
"Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB."; this.descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the ";
this.descriptionPara3 = "In order to access Azure Cosmos DB Notebook Gallery resources, you must accept the "; this.link1 = { label: "code of conduct.", url: CodeOfConductEndpoints.codeOfConduct };
this.link1 = { label: "code of conduct", url: CodeOfConductEndpoints.codeOfConduct };
this.link2 = { label: "privacy statement", url: CodeOfConductEndpoints.privacyStatement };
} }
private async acceptCodeOfConduct(): Promise<void> { private async acceptCodeOfConduct(): Promise<void> {
const startKey = traceStart(Action.NotebooksGalleryAcceptCodeOfConduct);
try { try {
const response = await this.props.junoClient.acceptCodeOfConduct(); const response = await this.props.junoClient.acceptCodeOfConduct();
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`); throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
} }
traceSuccess(Action.NotebooksGalleryAcceptCodeOfConduct, startKey);
this.props.onAcceptCodeOfConduct(response.data); this.props.onAcceptCodeOfConduct(response.data);
} catch (error) { } catch (error) {
traceFailure(
Action.NotebooksGalleryAcceptCodeOfConduct,
{
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct"); handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct");
} }
} }
@@ -53,6 +66,11 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
}; };
public render(): JSX.Element { public render(): JSX.Element {
if (!this.viewCodeOfConductTraced) {
this.viewCodeOfConductTraced = true;
trace(Action.NotebooksGalleryViewCodeOfConduct);
}
return ( return (
<Stack tokens={{ childrenGap: 20 }}> <Stack tokens={{ childrenGap: 20 }}>
<Stack.Item> <Stack.Item>
@@ -69,10 +87,6 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
<Link href={this.link1.url} target="_blank"> <Link href={this.link1.url} target="_blank">
{this.link1.label} {this.link1.label}
</Link> </Link>
{" and "}
<Link href={this.link2.url} target="_blank">
{this.link2.label}
</Link>
</Text> </Text>
</Stack.Item> </Stack.Item>
@@ -87,7 +101,7 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
fontSize: 12, fontSize: 12,
}, },
}} }}
label="I have read and accepted the code of conduct and privacy statement" label="I have read and accept the code of conduct."
onChange={this.onChangeCheckbox} onChange={this.onChangeCheckbox}
/> />
</Stack.Item> </Stack.Item>

View File

@@ -7,14 +7,20 @@ import {
} from "./GalleryAndNotebookViewerComponent"; } from "./GalleryAndNotebookViewerComponent";
export class GalleryAndNotebookViewerComponentAdapter implements ReactAdapter { export class GalleryAndNotebookViewerComponentAdapter implements ReactAdapter {
private key: string;
public parameters: ko.Observable<number>; public parameters: ko.Observable<number>;
constructor(private props: GalleryAndNotebookViewerComponentProps) { constructor(private props: GalleryAndNotebookViewerComponentProps) {
this.reset();
this.parameters = ko.observable<number>(Date.now()); this.parameters = ko.observable<number>(Date.now());
} }
public renderComponent(): JSX.Element { public renderComponent(): JSX.Element {
return <GalleryAndNotebookViewerComponent {...this.props} />; return <GalleryAndNotebookViewerComponent key={this.key} {...this.props} />;
}
public reset(): void {
this.key = `GalleryAndNotebookViewerComponent-${Date.now()}`;
} }
public triggerRender(): void { public triggerRender(): void {

View File

@@ -6,4 +6,16 @@
overflow-y: auto; overflow-y: auto;
width: 100%; width: 100%;
font-family: @DataExplorerFont; font-family: @DataExplorerFont;
background: @GalleryBackgroundColor;
}
.publicGalleryTabContainer {
position: relative;
height: 100vh;
}
.publicGalleryTabOverlayContent {
background: white;
padding: 20px;
margin: 10%;
} }

View File

@@ -9,17 +9,21 @@ import {
IPivotProps, IPivotProps,
IRectangle, IRectangle,
Label, Label,
Link,
List, List,
Overlay,
Pivot, Pivot,
PivotItem, PivotItem,
SearchBox, SearchBox,
Spinner,
SpinnerSize,
Stack, Stack,
Text, Text,
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient"; import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils"; import * as GalleryUtils from "../../../Utils/GalleryUtils";
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent"; import { Dialog, DialogProps } from "../Dialog";
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent"; import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
import "./GalleryViewerComponent.less"; import "./GalleryViewerComponent.less";
import { HttpStatusCodes } from "../../../Common/Constants"; import { HttpStatusCodes } from "../../../Common/Constants";
@@ -27,6 +31,8 @@ import Explorer from "../../Explorer";
import { CodeOfConductComponent } from "./CodeOfConductComponent"; import { CodeOfConductComponent } from "./CodeOfConductComponent";
import { InfoComponent } from "./InfoComponent/InfoComponent"; import { InfoComponent } from "./InfoComponent/InfoComponent";
import { handleError } from "../../../Common/ErrorHandlingUtils"; import { handleError } from "../../../Common/ErrorHandlingUtils";
import { trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
export interface GalleryViewerComponentProps { export interface GalleryViewerComponentProps {
container?: Explorer; container?: Explorer;
@@ -64,6 +70,8 @@ interface GalleryViewerComponentState {
searchText: string; searchText: string;
dialogProps: DialogProps; dialogProps: DialogProps;
isCodeOfConductAccepted: boolean; isCodeOfConductAccepted: boolean;
isFetchingPublishedNotebooks: boolean;
isFetchingFavouriteNotebooks: boolean;
} }
interface GalleryTabInfo { interface GalleryTabInfo {
@@ -74,18 +82,24 @@ interface GalleryTabInfo {
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps, GalleryViewerComponentState> { export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps, GalleryViewerComponentState> {
public static readonly OfficialSamplesTitle = "Official samples"; public static readonly OfficialSamplesTitle = "Official samples";
public static readonly PublicGalleryTitle = "Public gallery"; public static readonly PublicGalleryTitle = "Public gallery";
public static readonly FavoritesTitle = "Liked"; public static readonly FavoritesTitle = "My favorites";
public static readonly PublishedTitle = "Your published work"; public static readonly PublishedTitle = "My published work";
private static readonly rowsPerPage = 5; private static readonly rowsPerPage = 5;
private static readonly mostViewedText = "Most viewed"; private static readonly mostViewedText = "Most viewed";
private static readonly mostDownloadedText = "Most downloaded"; private static readonly mostDownloadedText = "Most downloaded";
private static readonly mostFavoritedText = "Most liked"; private static readonly mostFavoritedText = "Most favorited";
private static readonly mostRecentText = "Most recent"; private static readonly mostRecentText = "Most recent";
private readonly sortingOptions: IDropdownOption[]; private readonly sortingOptions: IDropdownOption[];
private viewGalleryTraced: boolean;
private viewOfficialSamplesTraced: boolean;
private viewPublicGalleryTraced: boolean;
private viewFavoritesTraced: boolean;
private viewPublishedNotebooksTraced: boolean;
private sampleNotebooks: IGalleryItem[]; private sampleNotebooks: IGalleryItem[];
private publicNotebooks: IGalleryItem[]; private publicNotebooks: IGalleryItem[];
private favoriteNotebooks: IGalleryItem[]; private favoriteNotebooks: IGalleryItem[];
@@ -107,6 +121,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
searchText: props.searchText, searchText: props.searchText,
dialogProps: undefined, dialogProps: undefined,
isCodeOfConductAccepted: undefined, isCodeOfConductAccepted: undefined,
isFetchingFavouriteNotebooks: true,
isFetchingPublishedNotebooks: true,
}; };
this.sortingOptions = [ this.sortingOptions = [
@@ -123,38 +139,30 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
text: GalleryViewerComponent.mostRecentText, text: GalleryViewerComponent.mostRecentText,
}, },
]; ];
if (this.props.container?.isGalleryPublishEnabled()) { this.sortingOptions.push({
this.sortingOptions.push({ key: SortBy.MostFavorited,
key: SortBy.MostFavorited, text: GalleryViewerComponent.mostFavoritedText,
text: GalleryViewerComponent.mostFavoritedText, });
});
}
this.loadTabContent(this.state.selectedTab, this.state.searchText, this.state.sortBy, false); this.loadTabContent(this.state.selectedTab, this.state.searchText, this.state.sortBy, false);
if (this.props.container?.isGalleryPublishEnabled()) { this.loadFavoriteNotebooks(this.state.searchText, this.state.sortBy, false); // Need this to show correct favorite button state
this.loadFavoriteNotebooks(this.state.searchText, this.state.sortBy, false); // Need this to show correct favorite button state
}
} }
public render(): JSX.Element { public render(): JSX.Element {
this.traceViewGallery();
const tabs: GalleryTabInfo[] = [this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)]; const tabs: GalleryTabInfo[] = [this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
if (this.props.container?.isGalleryPublishEnabled()) { tabs.push(
tabs.push( this.createPublicGalleryTab(
this.createPublicGalleryTab( GalleryTab.PublicGallery,
GalleryTab.PublicGallery, this.state.publicNotebooks,
this.state.publicNotebooks, this.state.isCodeOfConductAccepted
this.state.isCodeOfConductAccepted )
) );
);
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined. tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
// Displaying code of conduct component on gallery load should not be the default behavior. tabs.push(this.createPublishedNotebooksTab(GalleryTab.Published, this.state.publishedNotebooks));
if (this.state.isCodeOfConductAccepted !== false) {
tabs.push(this.createPublishedNotebooksTab(GalleryTab.Published, this.state.publishedNotebooks));
}
}
const pivotProps: IPivotProps = { const pivotProps: IPivotProps = {
onLinkClick: this.onPivotChange, onLinkClick: this.onPivotChange,
@@ -179,16 +187,63 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
<div className="galleryContainer"> <div className="galleryContainer">
<Pivot {...pivotProps}>{pivotItems}</Pivot> <Pivot {...pivotProps}>{pivotItems}</Pivot>
{this.state.dialogProps && <DialogComponent {...this.state.dialogProps} />} {this.state.dialogProps && <Dialog {...this.state.dialogProps} />}
</div> </div>
); );
} }
private traceViewGallery = (): void => {
if (!this.viewGalleryTraced) {
this.viewGalleryTraced = true;
trace(Action.NotebooksGalleryViewGallery);
}
switch (this.state.selectedTab) {
case GalleryTab.OfficialSamples:
if (!this.viewOfficialSamplesTraced) {
this.resetViewGalleryTabTracedFlags();
this.viewOfficialSamplesTraced = true;
trace(Action.NotebooksGalleryViewOfficialSamples);
}
break;
case GalleryTab.PublicGallery:
if (!this.viewPublicGalleryTraced) {
this.resetViewGalleryTabTracedFlags();
this.viewPublicGalleryTraced = true;
trace(Action.NotebooksGalleryViewPublicGallery);
}
break;
case GalleryTab.Favorites:
if (!this.viewFavoritesTraced) {
this.resetViewGalleryTabTracedFlags();
this.viewFavoritesTraced = true;
trace(Action.NotebooksGalleryViewFavorites);
}
break;
case GalleryTab.Published:
if (!this.viewPublishedNotebooksTraced) {
this.resetViewGalleryTabTracedFlags();
this.viewPublishedNotebooksTraced = true;
trace(Action.NotebooksGalleryViewPublishedNotebooks);
}
break;
default:
throw new Error(`Unknown selected tab ${this.state.selectedTab}`);
}
};
private resetViewGalleryTabTracedFlags = (): void => {
this.viewOfficialSamplesTraced = false;
this.viewPublicGalleryTraced = false;
this.viewFavoritesTraced = false;
this.viewPublishedNotebooksTraced = false;
};
private isEmptyData = (data: IGalleryItem[]): boolean => { private isEmptyData = (data: IGalleryItem[]): boolean => {
return !data || data.length === 0; return !data || data.length === 0;
}; };
private createEmptyTabContent = (iconName: string, line1: string, line2: string): JSX.Element => { private createEmptyTabContent = (iconName: string, line1: JSX.Element, line2: JSX.Element): JSX.Element => {
return ( return (
<Stack horizontalAlign="center" tokens={{ childrenGap: 10 }}> <Stack horizontalAlign="center" tokens={{ childrenGap: 10 }}>
<FontIcon iconName={iconName} style={{ fontSize: 100, color: "lightgray", marginTop: 20 }} /> <FontIcon iconName={iconName} style={{ fontSize: 100, color: "lightgray", marginTop: 20 }} />
@@ -216,40 +271,63 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}; };
} }
private getFavouriteNotebooksTabContent = (data: IGalleryItem[]) => {
if (this.isEmptyData(data)) {
if (this.state.isFetchingFavouriteNotebooks) {
return <Spinner size={SpinnerSize.large} />;
}
return this.createEmptyTabContent(
"ContactHeart",
<>You don&apos;t have any favorites yet</>,
<>
Favorite any notebook from the{" "}
<Link onClick={() => this.setState({ selectedTab: GalleryTab.OfficialSamples })}>official samples</Link> or{" "}
<Link onClick={() => this.setState({ selectedTab: GalleryTab.PublicGallery })}>public gallery</Link>
</>
);
}
return this.createSearchBarHeader(this.createCardsTabContent(data));
};
private createFavoritesTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo { private createFavoritesTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
return { return {
tab, tab,
content: this.isEmptyData(data) content: this.getFavouriteNotebooksTabContent(data),
? this.createEmptyTabContent(
"ContactHeart",
"You have not liked anything",
"Like any notebook from Official Samples or Public gallery"
)
: this.createSearchBarHeader(this.createCardsTabContent(data)),
}; };
} }
private getPublishedNotebooksTabContent = (data: IGalleryItem[]) => {
if (this.isEmptyData(data)) {
if (this.state.isFetchingPublishedNotebooks) {
return <Spinner size={SpinnerSize.large} />;
}
return this.createEmptyTabContent(
"Contact",
<>
You have not published anything to the{" "}
<Link onClick={() => this.setState({ selectedTab: GalleryTab.PublicGallery })}>public gallery</Link> yet
</>,
<>Publish your notebooks to share your work with other users</>
);
}
return this.createPublishedNotebooksTabContent(data);
};
private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => { private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
return { return {
tab, tab,
content: this.isEmptyData(data) content: this.getPublishedNotebooksTabContent(data),
? this.createEmptyTabContent(
"Contact",
"You have not published anything",
"Publish your sample notebooks to share your published work with others"
)
: this.createPublishedNotebooksTabContent(data),
}; };
}; };
private createPublishedNotebooksTabContent = (data: IGalleryItem[]): JSX.Element => { private createPublishedNotebooksTabContent = (data: IGalleryItem[]): JSX.Element => {
const { published, underReview, removed } = GalleryUtils.filterPublishedNotebooks(data); const { published, underReview, removed } = GalleryUtils.filterPublishedNotebooks(data);
const content = ( const content = (
<Stack tokens={{ childrenGap: 10 }}> <Stack tokens={{ childrenGap: 20 }}>
{published?.length > 0 && {published?.length > 0 &&
this.createPublishedNotebooksSectionContent( this.createPublishedNotebooksSectionContent(
undefined, undefined,
"You have successfully published the following notebook(s) to public gallery and shared with other Azure Cosmos DB users.", "You have successfully published and shared the following notebook(s) to the public gallery.",
this.createCardsTabContent(published) this.createCardsTabContent(published)
)} )}
{underReview?.length > 0 && {underReview?.length > 0 &&
@@ -276,24 +354,33 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
content: JSX.Element content: JSX.Element
): JSX.Element => { ): JSX.Element => {
return ( return (
<Stack tokens={{ childrenGap: 5 }}> <Stack tokens={{ childrenGap: 10 }}>
{title && <Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{title}</Text>} {title && (
{description && <Text>{description}</Text>} <Text styles={{ root: { fontWeight: FontWeights.semibold, marginLeft: 10, marginRight: 10 } }}>{title}</Text>
)}
{description && <Text styles={{ root: { marginLeft: 10, marginRight: 10 } }}>{description}</Text>}
{content} {content}
</Stack> </Stack>
); );
}; };
private createPublicGalleryTabContent(data: IGalleryItem[], acceptedCodeOfConduct: boolean): JSX.Element { private createPublicGalleryTabContent(data: IGalleryItem[], acceptedCodeOfConduct: boolean): JSX.Element {
return acceptedCodeOfConduct === false ? ( return (
<CodeOfConductComponent <div className="publicGalleryTabContainer">
junoClient={this.props.junoClient} {this.createSearchBarHeader(this.createCardsTabContent(data))}
onAcceptCodeOfConduct={(result: boolean) => { {acceptedCodeOfConduct === false && (
this.setState({ isCodeOfConductAccepted: result }); <Overlay isDarkThemed>
}} <div className="publicGalleryTabOverlayContent">
/> <CodeOfConductComponent
) : ( junoClient={this.props.junoClient}
this.createSearchBarHeader(this.createCardsTabContent(data)) onAcceptCodeOfConduct={(result: boolean) => {
this.setState({ isCodeOfConductAccepted: result });
}}
/>
</div>
</Overlay>
)}
</div>
); );
} }
@@ -310,11 +397,9 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
<Stack.Item styles={{ root: { minWidth: 200 } }}> <Stack.Item styles={{ root: { minWidth: 200 } }}>
<Dropdown options={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} /> <Dropdown options={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} />
</Stack.Item> </Stack.Item>
{(!this.props.container || this.props.container.isGalleryPublishEnabled()) && ( <Stack.Item>
<Stack.Item> <InfoComponent />
<InfoComponent /> </Stack.Item>
</Stack.Item>
)}
</Stack> </Stack>
<Stack.Item>{content}</Stack.Item> <Stack.Item>{content}</Stack.Item>
</Stack> </Stack>
@@ -322,7 +407,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
} }
private createCardsTabContent(data: IGalleryItem[]): JSX.Element { private createCardsTabContent(data: IGalleryItem[]): JSX.Element {
return ( return data ? (
<FocusZone> <FocusZone>
<List <List
items={data} items={data}
@@ -331,12 +416,14 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
onRenderCell={this.onRenderCell} onRenderCell={this.onRenderCell}
/> />
</FocusZone> </FocusZone>
) : (
<Spinner size={SpinnerSize.large} />
); );
} }
private createPolicyViolationsListContent(data: IGalleryItem[]): JSX.Element { private createPolicyViolationsListContent(data: IGalleryItem[]): JSX.Element {
return ( return (
<table> <table style={{ margin: 10 }}>
<tbody> <tbody>
<tr> <tr>
<th>Name</th> <th>Name</th>
@@ -385,6 +472,10 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
} }
this.sampleNotebooks = response.data; this.sampleNotebooks = response.data;
trace(Action.NotebooksGalleryOfficialSamplesCount, ActionModifiers.Mark, {
count: this.sampleNotebooks?.length,
});
} catch (error) { } catch (error) {
handleError(error, "GalleryViewerComponent/loadSampleNotebooks", "Failed to load sample notebooks"); handleError(error, "GalleryViewerComponent/loadSampleNotebooks", "Failed to load sample notebooks");
} }
@@ -411,6 +502,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when loading public notebooks`); throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
} }
trace(Action.NotebooksGalleryPublicGalleryCount, ActionModifiers.Mark, { count: this.publicNotebooks?.length });
} catch (error) { } catch (error) {
handleError(error, "GalleryViewerComponent/loadPublicNotebooks", "Failed to load public notebooks"); handleError(error, "GalleryViewerComponent/loadPublicNotebooks", "Failed to load public notebooks");
} }
@@ -425,14 +518,19 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private async loadFavoriteNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> { private async loadFavoriteNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
if (!offline) { if (!offline) {
try { try {
this.setState({ isFetchingFavouriteNotebooks: true });
const response = await this.props.junoClient.getFavoriteNotebooks(); const response = await this.props.junoClient.getFavoriteNotebooks();
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when loading favorite notebooks`); throw new Error(`Received HTTP ${response.status} when loading favorite notebooks`);
} }
this.favoriteNotebooks = response.data; this.favoriteNotebooks = response.data;
trace(Action.NotebooksGalleryFavoritesCount, ActionModifiers.Mark, { count: this.favoriteNotebooks?.length });
} catch (error) { } catch (error) {
handleError(error, "GalleryViewerComponent/loadFavoriteNotebooks", "Failed to load favorite notebooks"); handleError(error, "GalleryViewerComponent/loadFavoriteNotebooks", "Failed to load favorite notebooks");
} finally {
this.setState({ isFetchingFavouriteNotebooks: false });
} }
} }
@@ -451,14 +549,25 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private async loadPublishedNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> { private async loadPublishedNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
if (!offline) { if (!offline) {
try { try {
this.setState({ isFetchingPublishedNotebooks: true });
const response = await this.props.junoClient.getPublishedNotebooks(); const response = await this.props.junoClient.getPublishedNotebooks();
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when loading published notebooks`); throw new Error(`Received HTTP ${response.status} when loading published notebooks`);
} }
this.publishedNotebooks = response.data; this.publishedNotebooks = response.data;
const { published, underReview, removed } = GalleryUtils.filterPublishedNotebooks(this.publishedNotebooks);
trace(Action.NotebooksGalleryPublishedCount, ActionModifiers.Mark, {
count: this.publishedNotebooks?.length,
publishedCount: published.length,
underReviewCount: underReview.length,
removedCount: removed.length,
});
} catch (error) { } catch (error) {
handleError(error, "GalleryViewerComponent/loadPublishedNotebooks", "Failed to load published notebooks"); handleError(error, "GalleryViewerComponent/loadPublishedNotebooks", "Failed to load published notebooks");
} finally {
this.setState({ isFetchingPublishedNotebooks: false });
} }
} }
@@ -544,10 +653,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}; };
private onRenderCell = (data?: IGalleryItem): JSX.Element => { private onRenderCell = (data?: IGalleryItem): JSX.Element => {
let isFavorite: boolean; const isFavorite = this.favoriteNotebooks?.find((item) => item.id === data.id) !== undefined;
if (this.props.container?.isGalleryPublishEnabled()) {
isFavorite = this.favoriteNotebooks?.find((item) => item.id === data.id) !== undefined;
}
const props: GalleryCardComponentProps = { const props: GalleryCardComponentProps = {
data, data,
isFavorite, isFavorite,

View File

@@ -17,35 +17,28 @@ exports[`CodeOfConductComponent renders 1`] = `
} }
} }
> >
Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement Azure Cosmos DB Notebook Gallery - Code of Conduct
</Text> </Text>
</StackItem> </StackItem>
<StackItem> <StackItem>
<Text> <Text>
Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB. The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB.
</Text> </Text>
</StackItem> </StackItem>
<StackItem> <StackItem>
<Text> <Text>
In order to access Azure Cosmos DB Notebook Gallery resources, you must accept the In order to view and publish your samples to the gallery, you must accept the
<StyledLinkBase <StyledLinkBase
href="https://aka.ms/cosmos-code-of-conduct" href="https://aka.ms/cosmos-code-of-conduct"
target="_blank" target="_blank"
> >
code of conduct code of conduct.
</StyledLinkBase>
and
<StyledLinkBase
href="https://aka.ms/ms-privacy-policy"
target="_blank"
>
privacy statement
</StyledLinkBase> </StyledLinkBase>
</Text> </Text>
</StackItem> </StackItem>
<StackItem> <StackItem>
<StyledCheckboxBase <StyledCheckboxBase
label="I have read and accepted the code of conduct and privacy statement" label="I have read and accept the code of conduct."
onChange={[Function]} onChange={[Function]}
styles={ styles={
Object { Object {

View File

@@ -72,6 +72,10 @@ exports[`GalleryViewerComponent renders 1`] = `
"key": 3, "key": 3,
"text": "Most recent", "text": "Most recent",
}, },
Object {
"key": 2,
"text": "Most favorited",
},
] ]
} }
selectedKey={0} selectedKey={0}
@@ -82,22 +86,128 @@ exports[`GalleryViewerComponent renders 1`] = `
</StackItem> </StackItem>
</Stack> </Stack>
<StackItem> <StackItem>
<FocusZone <StyledSpinnerBase
direction={2} size={3}
isCircularNavigation={false} />
shouldRaiseClicks={true}
>
<List
getPageSpecification={[Function]}
onRenderCell={[Function]}
renderedWindowsAhead={3}
renderedWindowsBehind={2}
startIndex={0}
/>
</FocusZone>
</StackItem> </StackItem>
</Stack> </Stack>
</PivotItem> </PivotItem>
<PivotItem
headerText="Public gallery"
itemKey="PublicGallery"
key="PublicGallery"
style={
Object {
"marginTop": 20,
}
}
>
<div
className="publicGalleryTabContainer"
>
<Stack
tokens={
Object {
"childrenGap": 10,
}
}
>
<Stack
horizontal={true}
tokens={
Object {
"childrenGap": 20,
"padding": 10,
}
}
>
<StackItem
grow={true}
>
<StyledSearchBoxBase
onChange={[Function]}
placeholder="Search"
/>
</StackItem>
<StackItem>
<StyledLabelBase>
Sort by
</StyledLabelBase>
</StackItem>
<StackItem
styles={
Object {
"root": Object {
"minWidth": 200,
},
}
}
>
<StyledWithResponsiveMode
onChange={[Function]}
options={
Array [
Object {
"key": 0,
"text": "Most viewed",
},
Object {
"key": 1,
"text": "Most downloaded",
},
Object {
"key": 3,
"text": "Most recent",
},
Object {
"key": 2,
"text": "Most favorited",
},
]
}
selectedKey={0}
/>
</StackItem>
<StackItem>
<InfoComponent />
</StackItem>
</Stack>
<StackItem>
<StyledSpinnerBase
size={3}
/>
</StackItem>
</Stack>
</div>
</PivotItem>
<PivotItem
headerText="My favorites"
itemKey="Favorites"
key="Favorites"
style={
Object {
"marginTop": 20,
}
}
>
<StyledSpinnerBase
size={3}
/>
</PivotItem>
<PivotItem
headerText="My published work"
itemKey="Published"
key="Published"
style={
Object {
"marginTop": 20,
}
}
>
<StyledSpinnerBase
size={3}
/>
</PivotItem>
</StyledPivotBase> </StyledPivotBase>
</div> </div>
`; `;

View File

@@ -31,6 +31,26 @@ export interface NotebookMetadataComponentProps {
} }
export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> { export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> {
private renderFavouriteButton = (): JSX.Element => {
return (
<Text>
{this.props.isFavorite !== undefined ? (
<>
<IconButton
iconProps={{ iconName: this.props.isFavorite ? "HeartFill" : "Heart" }}
onClick={this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick}
/>
{this.props.data.favorites} likes
</>
) : (
<>
<Icon iconName="Heart" /> {this.props.data.favorites} likes
</>
)}
</Text>
);
};
public render(): JSX.Element { public render(): JSX.Element {
const options: Intl.DateTimeFormatOptions = { const options: Intl.DateTimeFormatOptions = {
year: "numeric", year: "numeric",
@@ -49,19 +69,7 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
</Text> </Text>
</Stack.Item> </Stack.Item>
<Stack.Item> <Stack.Item>{this.renderFavouriteButton()}</Stack.Item>
<Text>
{this.props.isFavorite !== undefined && (
<>
<IconButton
iconProps={{ iconName: this.props.isFavorite ? "HeartFill" : "Heart" }}
onClick={this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick}
/>
{this.props.data.favorites} likes
</>
)}
</Text>
</Stack.Item>
{this.props.downloadButtonText && ( {this.props.downloadButtonText && (
<Stack.Item> <Stack.Item>

View File

@@ -3,25 +3,24 @@
*/ */
import { Notebook } from "@nteract/commutable"; import { Notebook } from "@nteract/commutable";
import { createContentRef } from "@nteract/core"; import { createContentRef } from "@nteract/core";
import { IChoiceGroupProps, Icon, Link, ProgressIndicator } from "office-ui-fabric-react"; import { IChoiceGroupProps, Icon, IProgressIndicatorProps, Link, ProgressIndicator } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { contents } from "rx-jupyter"; import { contents } from "rx-jupyter";
import * as Logger from "../../../Common/Logger";
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient"; import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils"; import * as GalleryUtils from "../../../Utils/GalleryUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2"; import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper"; import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer"; import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
import { DialogComponent, DialogProps, TextFieldProps } from "../DialogReactComponent/DialogComponent"; import { Dialog, DialogProps, TextFieldProps } from "../Dialog";
import { NotebookMetadataComponent } from "./NotebookMetadataComponent"; import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
import "./NotebookViewerComponent.less"; import "./NotebookViewerComponent.less";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { NotebookV4 } from "@nteract/commutable/lib/v4"; import { NotebookV4 } from "@nteract/commutable/lib/v4";
import { SessionStorageUtility } from "../../../Shared/StorageUtility"; import { SessionStorageUtility } from "../../../Shared/StorageUtility";
import { DialogHost } from "../../../Utils/GalleryUtils"; import { DialogHost } from "../../../Utils/GalleryUtils";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
export interface NotebookViewerComponentProps { export interface NotebookViewerComponentProps {
container?: Explorer; container?: Explorer;
@@ -80,6 +79,12 @@ export class NotebookViewerComponent
} }
private async loadNotebookContent(): Promise<void> { private async loadNotebookContent(): Promise<void> {
const startKey = traceStart(Action.NotebooksGalleryViewNotebook, {
notebookUrl: this.props.notebookUrl,
notebookId: this.props.galleryItem?.id,
isSample: this.props.galleryItem?.isSample,
});
try { try {
const response = await fetch(this.props.notebookUrl); const response = await fetch(this.props.notebookUrl);
if (!response.ok) { if (!response.ok) {
@@ -87,6 +92,16 @@ export class NotebookViewerComponent
throw new Error(`Received HTTP ${response.status} while fetching ${this.props.notebookUrl}`); throw new Error(`Received HTTP ${response.status} while fetching ${this.props.notebookUrl}`);
} }
traceSuccess(
Action.NotebooksGalleryViewNotebook,
{
notebookUrl: this.props.notebookUrl,
notebookId: this.props.galleryItem?.id,
isSample: this.props.galleryItem?.isSample,
},
startKey
);
const notebook: Notebook = await response.json(); const notebook: Notebook = await response.json();
this.removeNotebookViewerLink(notebook, this.props.galleryItem?.newCellId); this.removeNotebookViewerLink(notebook, this.props.galleryItem?.newCellId);
this.notebookComponentBootstrapper.setContent("json", notebook); this.notebookComponentBootstrapper.setContent("json", notebook);
@@ -101,6 +116,18 @@ export class NotebookViewerComponent
SessionStorageUtility.setEntry(this.props.galleryItem?.id, "true"); SessionStorageUtility.setEntry(this.props.galleryItem?.id, "true");
} }
} catch (error) { } catch (error) {
traceFailure(
Action.NotebooksGalleryViewNotebook,
{
notebookUrl: this.props.notebookUrl,
notebookId: this.props.galleryItem?.id,
isSample: this.props.galleryItem?.isSample,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
this.setState({ showProgressBar: false }); this.setState({ showProgressBar: false });
handleError(error, "NotebookViewerComponent/loadNotebookContent", "Failed to load notebook content"); handleError(error, "NotebookViewerComponent/loadNotebookContent", "Failed to load notebook content");
} }
@@ -152,7 +179,7 @@ export class NotebookViewerComponent
hidePrompts: this.props.hidePrompts, hidePrompts: this.props.hidePrompts,
})} })}
{this.state.dialogProps && <DialogComponent {...this.state.dialogProps} />} {this.state.dialogProps && <Dialog {...this.state.dialogProps} />}
</div> </div>
); );
} }
@@ -178,6 +205,32 @@ export class NotebookViewerComponent
}; };
} }
// DialogHost
showOkModalDialog(
title: string,
msg: string,
okLabel: string,
onOk: () => void,
progressIndicatorProps?: IProgressIndicatorProps
): void {
this.setState({
dialogProps: {
isModal: true,
visible: true,
title,
subText: msg,
primaryButtonText: okLabel,
onPrimaryButtonClick: () => {
this.setState({ dialogProps: undefined });
onOk && onOk();
},
secondaryButtonText: undefined,
onSecondaryButtonClick: undefined,
progressIndicatorProps,
},
});
}
// DialogHost // DialogHost
showOkCancelModalDialog( showOkCancelModalDialog(
title: string, title: string,
@@ -186,8 +239,10 @@ export class NotebookViewerComponent
onOk: () => void, onOk: () => void,
cancelLabel: string, cancelLabel: string,
onCancel: () => void, onCancel: () => void,
progressIndicatorProps?: IProgressIndicatorProps,
choiceGroupProps?: IChoiceGroupProps, choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean
): void { ): void {
this.setState({ this.setState({
dialogProps: { dialogProps: {
@@ -205,8 +260,10 @@ export class NotebookViewerComponent
this.setState({ dialogProps: undefined }); this.setState({ dialogProps: undefined });
onCancel && onCancel(); onCancel && onCancel();
}, },
progressIndicatorProps,
choiceGroupProps, choiceGroupProps,
textFieldProps, textFieldProps,
primaryButtonDisabled,
}, },
}); });
} }

View File

@@ -2,7 +2,7 @@ import { shallow } from "enzyme";
import React from "react"; import React from "react";
import { SettingsComponentProps, SettingsComponent, SettingsComponentState } from "./SettingsComponent"; import { SettingsComponentProps, SettingsComponent, SettingsComponentState } from "./SettingsComponent";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import SettingsTabV2 from "../../Tabs/SettingsTabV2"; import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2";
import { collection } from "./TestUtils"; import { collection } from "./TestUtils";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import ko from "knockout"; import ko from "knockout";
@@ -31,25 +31,21 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
})); }));
import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types"; import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import Q from "q";
jest.mock("../../../Common/dataAccess/updateOffer", () => ({ jest.mock("../../../Common/dataAccess/updateOffer", () => ({
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer), updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer),
})); }));
describe("SettingsComponent", () => { describe("SettingsComponent", () => {
const baseProps: SettingsComponentProps = { const baseProps: SettingsComponentProps = {
settingsTab: new SettingsTabV2({ settingsTab: new CollectionSettingsTabV2({
collection: collection, collection: collection,
tabKind: ViewModels.CollectionTabKind.SettingsV2, tabKind: ViewModels.CollectionTabKind.CollectionSettingsV2,
title: "Scale & Settings", title: "Scale & Settings",
tabPath: "", tabPath: "",
node: undefined, node: undefined,
hashLocation: "settings", hashLocation: "settings",
isActive: ko.observable(false), isActive: ko.observable(false),
onUpdateTabsButtons: undefined, onUpdateTabsButtons: undefined,
getPendingNotification: Q.Promise<DataModels.Notification>(() => {
return;
}),
}), }),
}; };
@@ -142,6 +138,7 @@ describe("SettingsComponent", () => {
readSettings: undefined, readSettings: undefined,
onSettingsClick: undefined, onSettingsClick: undefined,
loadOffer: undefined, loadOffer: undefined,
getPendingThroughputSplitNotification: undefined,
} as ViewModels.Database; } as ViewModels.Database;
newCollection.getDatabase = () => newDatabase; newCollection.getDatabase = () => newDatabase;
newCollection.offer = ko.observable(undefined); newCollection.offer = ko.observable(undefined);

View File

@@ -11,7 +11,7 @@ import Explorer from "../../Explorer";
import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection"; import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import SettingsTab from "../../Tabs/SettingsTabV2"; import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils"; import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent"; import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
import { import {
@@ -44,7 +44,6 @@ import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/genera
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection"; import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress"; import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { isEmpty } from "underscore";
interface SettingsV2TabInfo { interface SettingsV2TabInfo {
tab: SettingsV2TabTypes; tab: SettingsV2TabTypes;
@@ -58,7 +57,7 @@ interface ButtonV2 {
} }
export interface SettingsComponentProps { export interface SettingsComponentProps {
settingsTab: SettingsTab; settingsTab: SettingsTabV2;
} }
export interface SettingsComponentState { export interface SettingsComponentState {
@@ -116,7 +115,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private discardSettingsChangesButton: ButtonV2; private discardSettingsChangesButton: ButtonV2;
private isAnalyticalStorageEnabled: boolean; private isAnalyticalStorageEnabled: boolean;
private isCollectionSettingsTab: boolean;
private collection: ViewModels.Collection; private collection: ViewModels.Collection;
private database: ViewModels.Database;
private offer: DataModels.Offer;
private container: Explorer; private container: Explorer;
private changeFeedPolicyVisible: boolean; private changeFeedPolicyVisible: boolean;
private isFixedContainer: boolean; private isFixedContainer: boolean;
@@ -126,20 +128,28 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
constructor(props: SettingsComponentProps) { constructor(props: SettingsComponentProps) {
super(props); super(props);
this.collection = this.props.settingsTab.collection as ViewModels.Collection; this.isCollectionSettingsTab = this.props.settingsTab.tabKind === ViewModels.CollectionTabKind.CollectionSettingsV2;
this.container = this.collection?.container; if (this.isCollectionSettingsTab) {
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl(); this.collection = this.props.settingsTab.collection as ViewModels.Collection;
this.shouldShowIndexingPolicyEditor = this.container = this.collection?.container;
this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB(); this.offer = this.collection?.offer();
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
this.shouldShowIndexingPolicyEditor =
this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB();
this.changeFeedPolicyVisible = this.collection?.container.isFeatureEnabled( this.changeFeedPolicyVisible = this.collection?.container.isFeatureEnabled(
Constants.Features.enableChangeFeedPolicy Constants.Features.enableChangeFeedPolicy
); );
// Mongo container with system partition key still treat as "Fixed" // Mongo container with system partition key still treat as "Fixed"
this.isFixedContainer = this.isFixedContainer =
this.container.isPreferredApiMongoDB() && this.container.isPreferredApiMongoDB() &&
(!this.collection.partitionKey || this.collection.partitionKey.systemKey); (!this.collection?.partitionKey || this.collection?.partitionKey.systemKey);
} else {
this.database = this.props.settingsTab.database;
this.container = this.database?.container;
this.offer = this.database?.offer();
}
this.state = { this.state = {
throughput: undefined, throughput: undefined,
@@ -206,18 +216,21 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
} }
componentDidMount(): void { componentDidMount(): void {
this.refreshIndexTransformationProgress(); if (this.isCollectionSettingsTab) {
this.loadMongoIndexes(); this.refreshIndexTransformationProgress();
this.loadMongoIndexes();
}
this.setAutoPilotStates(); this.setAutoPilotStates();
this.setBaseline(); this.setBaseline();
if (this.props.settingsTab.isActive()) { if (this.props.settingsTab.isActive()) {
this.props.settingsTab.getSettingsTabContainer().onUpdateTabsButtons(this.getTabsButtons()); this.props.settingsTab.getContainer().onUpdateTabsButtons(this.getTabsButtons());
} }
} }
componentDidUpdate(): void { componentDidUpdate(): void {
if (this.props.settingsTab.isActive()) { if (this.props.settingsTab.isActive()) {
this.props.settingsTab.getSettingsTabContainer().onUpdateTabsButtons(this.getTabsButtons()); this.props.settingsTab.getContainer().onUpdateTabsButtons(this.getTabsButtons());
} }
} }
@@ -270,7 +283,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}; };
private setAutoPilotStates = (): void => { private setAutoPilotStates = (): void => {
const autoscaleMaxThroughput = this.collection?.offer()?.autoscaleMaxThroughput; const autoscaleMaxThroughput = this.offer?.autoscaleMaxThroughput;
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) { if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
this.setState({ this.setState({
@@ -295,7 +308,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
!!this.collection.conflictResolutionPolicy(); !!this.collection.conflictResolutionPolicy();
public isOfferReplacePending = (): boolean => { public isOfferReplacePending = (): boolean => {
return this.collection?.offer()?.offerReplacePending; return this.offer?.offerReplacePending;
}; };
public onSaveClick = async (): Promise<void> => { public onSaveClick = async (): Promise<void> => {
@@ -309,174 +322,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
tabTitle: this.props.settingsTab.tabTitle(), tabTitle: this.props.settingsTab.tabTitle(),
}); });
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
try { try {
if ( await (this.isCollectionSettingsTab
this.state.isSubSettingsSaveable || ? this.saveCollectionSettings(startKey)
this.state.isIndexingPolicyDirty || : this.saveDatabaseSettings(startKey));
this.state.isConflictResolutionDirty
) {
let defaultTtl: number;
switch (this.state.timeToLive) {
case TtlType.On:
defaultTtl = Number(this.state.timeToLiveSeconds);
break;
case TtlType.OnNoDefault:
defaultTtl = -1;
break;
case TtlType.Off:
default:
defaultTtl = undefined;
break;
}
const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty;
newCollection.defaultTtl = defaultTtl;
newCollection.indexingPolicy = this.state.indexingPolicyContent;
newCollection.changeFeedPolicy =
this.changeFeedPolicyVisible && this.state.changeFeedPolicy === ChangeFeedPolicyState.On
? {
retentionDuration: Constants.BackendDefaults.maxChangeFeedRetentionDuration,
}
: undefined;
newCollection.analyticalStorageTtl = this.getAnalyticalStorageTtl();
newCollection.geospatialConfig = {
type: this.state.geospatialConfigType,
};
const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy();
if (conflictResolutionChanges) {
newCollection.conflictResolutionPolicy = conflictResolutionChanges;
}
const updatedCollection: DataModels.Collection = await updateCollection(
this.collection.databaseId,
this.collection.id(),
newCollection
);
this.collection.rawDataModel = updatedCollection;
this.collection.defaultTtl(updatedCollection.defaultTtl);
this.collection.analyticalStorageTtl(updatedCollection.analyticalStorageTtl);
this.collection.id(updatedCollection.id);
this.collection.indexingPolicy(updatedCollection.indexingPolicy);
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
if (wasIndexingPolicyModified) {
await this.refreshIndexTransformationProgress();
}
this.setState({
isSubSettingsSaveable: false,
isSubSettingsDiscardable: false,
isIndexingPolicyDirty: false,
isConflictResolutionDirty: false,
});
}
if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) {
try {
const newMongoIndexes = this.getMongoIndexesToSave();
const newMongoCollection: MongoDBCollectionResource = {
...this.mongoDBCollectionResource,
indexes: newMongoIndexes,
};
this.mongoDBCollectionResource = await updateMongoDBCollectionThroughRP(
this.collection.databaseId,
this.collection.id(),
newMongoCollection
);
await this.refreshIndexTransformationProgress();
this.setState({
isMongoIndexingPolicySaveable: false,
indexesToDrop: [],
indexesToAdd: [],
currentMongoIndexes: [...this.mongoDBCollectionResource.indexes],
});
traceSuccess(
Action.MongoIndexUpdated,
{
databaseAccountName: this.container.databaseAccount()?.name,
databaseName: this.collection?.databaseId,
collectionName: this.collection?.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle(),
},
startKey
);
} catch (error) {
traceFailure(
Action.MongoIndexUpdated,
{
databaseAccountName: this.container.databaseAccount()?.name,
databaseName: this.collection?.databaseId,
collectionName: this.collection?.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle(),
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
throw error;
}
}
if (this.state.isScaleSaveable) {
const updateOfferParams: DataModels.UpdateOfferParams = {
databaseId: this.collection.databaseId,
collectionId: this.collection.id(),
currentOffer: this.collection.offer(),
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput,
};
if (this.hasProvisioningTypeChanged()) {
if (this.state.isAutoPilotSelected) {
updateOfferParams.migrateToAutoPilot = true;
} else {
updateOfferParams.migrateToManual = true;
}
}
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
this.collection.offer(updatedOffer);
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
if (this.state.isAutoPilotSelected) {
this.setState({
autoPilotThroughput: updatedOffer.autoscaleMaxThroughput,
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput,
});
} else {
this.setState({
throughput: updatedOffer.manualThroughput,
throughputBaseline: updatedOffer.manualThroughput,
});
}
}
this.container.isRefreshingExplorer(false);
this.setBaseline();
this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected });
traceSuccess(
Action.SettingsV2Updated,
{
databaseAccountName: this.container.databaseAccount()?.name,
databaseName: this.collection?.databaseId,
collectionName: this.collection?.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle(),
},
startKey
);
} catch (error) { } catch (error) {
this.container.isRefreshingExplorer(false); this.container.isRefreshingExplorer(false);
this.props.settingsTab.isExecutionError(true); this.props.settingsTab.isExecutionError(true);
@@ -495,8 +344,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}, },
startKey startKey
); );
} finally {
this.props.settingsTab.isExecuting(false);
} }
this.props.settingsTab.isExecuting(false);
}; };
public onRevertClick = (): void => { public onRevertClick = (): void => {
@@ -693,6 +543,17 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}; };
public setBaseline = (): void => { public setBaseline = (): void => {
const offerThroughput = this.offer?.manualThroughput;
if (!this.isCollectionSettingsTab) {
this.setState({
throughput: offerThroughput,
throughputBaseline: offerThroughput,
});
return;
}
const defaultTtl = this.collection.defaultTtl(); const defaultTtl = this.collection.defaultTtl();
let timeToLive: TtlType = this.state.timeToLive; let timeToLive: TtlType = this.state.timeToLive;
@@ -725,7 +586,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
} }
} }
const offerThroughput = this.collection.offer()?.manualThroughput;
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
? ChangeFeedPolicyState.On ? ChangeFeedPolicyState.On
: ChangeFeedPolicyState.Off; : ChangeFeedPolicyState.Off;
@@ -811,9 +671,225 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.setState({ selectedTab: selectedTab }); this.setState({ selectedTab: selectedTab });
}; };
private saveDatabaseSettings = async (startKey: number): Promise<void> => {
if (this.state.isScaleSaveable) {
const updateOfferParams: DataModels.UpdateOfferParams = {
databaseId: this.database.id(),
currentOffer: this.database.offer(),
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput,
};
if (this.hasProvisioningTypeChanged()) {
if (this.state.isAutoPilotSelected) {
updateOfferParams.migrateToAutoPilot = true;
} else {
updateOfferParams.migrateToManual = true;
}
}
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
this.database.offer(updatedOffer);
this.offer = updatedOffer;
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
if (this.state.isAutoPilotSelected) {
this.setState({
autoPilotThroughput: updatedOffer.autoscaleMaxThroughput,
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput,
});
} else {
this.setState({
throughput: updatedOffer.manualThroughput,
throughputBaseline: updatedOffer.manualThroughput,
});
}
}
this.container.isRefreshingExplorer(false);
this.setBaseline();
this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected });
traceSuccess(
Action.SettingsV2Updated,
{
databaseAccountName: this.container.databaseAccount()?.name,
databaseName: this.database.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle(),
},
startKey
);
};
private saveCollectionSettings = async (startKey: number): Promise<void> => {
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
if (this.state.isSubSettingsSaveable || this.state.isIndexingPolicyDirty || this.state.isConflictResolutionDirty) {
let defaultTtl: number;
switch (this.state.timeToLive) {
case TtlType.On:
defaultTtl = Number(this.state.timeToLiveSeconds);
break;
case TtlType.OnNoDefault:
defaultTtl = -1;
break;
case TtlType.Off:
default:
defaultTtl = undefined;
break;
}
const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty;
newCollection.defaultTtl = defaultTtl;
newCollection.indexingPolicy = this.state.indexingPolicyContent;
newCollection.changeFeedPolicy =
this.changeFeedPolicyVisible && this.state.changeFeedPolicy === ChangeFeedPolicyState.On
? {
retentionDuration: Constants.BackendDefaults.maxChangeFeedRetentionDuration,
}
: undefined;
newCollection.analyticalStorageTtl = this.getAnalyticalStorageTtl();
newCollection.geospatialConfig = {
type: this.state.geospatialConfigType,
};
const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy();
if (conflictResolutionChanges) {
newCollection.conflictResolutionPolicy = conflictResolutionChanges;
}
const updatedCollection: DataModels.Collection = await updateCollection(
this.collection.databaseId,
this.collection.id(),
newCollection
);
this.collection.rawDataModel = updatedCollection;
this.collection.defaultTtl(updatedCollection.defaultTtl);
this.collection.analyticalStorageTtl(updatedCollection.analyticalStorageTtl);
this.collection.id(updatedCollection.id);
this.collection.indexingPolicy(updatedCollection.indexingPolicy);
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
if (wasIndexingPolicyModified) {
await this.refreshIndexTransformationProgress();
}
this.setState({
isSubSettingsSaveable: false,
isSubSettingsDiscardable: false,
isIndexingPolicyDirty: false,
isConflictResolutionDirty: false,
});
}
if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) {
try {
const newMongoIndexes = this.getMongoIndexesToSave();
const newMongoCollection: MongoDBCollectionResource = {
...this.mongoDBCollectionResource,
indexes: newMongoIndexes,
};
this.mongoDBCollectionResource = await updateMongoDBCollectionThroughRP(
this.collection.databaseId,
this.collection.id(),
newMongoCollection
);
await this.refreshIndexTransformationProgress();
this.setState({
isMongoIndexingPolicySaveable: false,
indexesToDrop: [],
indexesToAdd: [],
currentMongoIndexes: [...this.mongoDBCollectionResource.indexes],
});
traceSuccess(
Action.MongoIndexUpdated,
{
databaseAccountName: this.container.databaseAccount()?.name,
databaseName: this.collection?.databaseId,
collectionName: this.collection?.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle(),
},
startKey
);
} catch (error) {
traceFailure(
Action.MongoIndexUpdated,
{
databaseAccountName: this.container.databaseAccount()?.name,
databaseName: this.collection?.databaseId,
collectionName: this.collection?.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle(),
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
throw error;
}
}
if (this.state.isScaleSaveable) {
const updateOfferParams: DataModels.UpdateOfferParams = {
databaseId: this.collection.databaseId,
collectionId: this.collection.id(),
currentOffer: this.collection.offer(),
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput,
};
if (this.hasProvisioningTypeChanged()) {
if (this.state.isAutoPilotSelected) {
updateOfferParams.migrateToAutoPilot = true;
} else {
updateOfferParams.migrateToManual = true;
}
}
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
this.collection.offer(updatedOffer);
this.offer = updatedOffer;
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
if (this.state.isAutoPilotSelected) {
this.setState({
autoPilotThroughput: updatedOffer.autoscaleMaxThroughput,
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput,
});
} else {
this.setState({
throughput: updatedOffer.manualThroughput,
throughputBaseline: updatedOffer.manualThroughput,
});
}
}
this.container.isRefreshingExplorer(false);
this.setBaseline();
this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected });
traceSuccess(
Action.SettingsV2Updated,
{
databaseAccountName: this.container.databaseAccount()?.name,
databaseName: this.collection?.databaseId,
collectionName: this.collection?.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle(),
},
startKey
);
};
public render(): JSX.Element { public render(): JSX.Element {
const scaleComponentProps: ScaleComponentProps = { const scaleComponentProps: ScaleComponentProps = {
collection: this.collection, collection: this.collection,
database: this.database,
container: this.container, container: this.container,
isFixedContainer: this.isFixedContainer, isFixedContainer: this.isFixedContainer,
onThroughputChange: this.onThroughputChange, onThroughputChange: this.onThroughputChange,
@@ -830,6 +906,16 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
initialNotification: this.props.settingsTab.pendingNotification(), initialNotification: this.props.settingsTab.pendingNotification(),
}; };
if (!this.isCollectionSettingsTab) {
return (
<div className="settingsV2MainContainer">
<div className="settingsV2TabsContainer">
<ScaleComponent {...scaleComponentProps} />
</div>
</div>
);
}
const subSettingsComponentProps: SubSettingsComponentProps = { const subSettingsComponentProps: SubSettingsComponentProps = {
collection: this.collection, collection: this.collection,
container: this.container, container: this.container,
@@ -899,7 +985,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}; };
const tabs: SettingsV2TabInfo[] = []; const tabs: SettingsV2TabInfo[] = [];
if (!hasDatabaseSharedThroughput(this.collection) && this.collection.offer()) { if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
tabs.push({ tabs.push({
tab: SettingsV2TabTypes.ScaleTab, tab: SettingsV2TabTypes.ScaleTab,
content: <ScaleComponent {...scaleComponentProps} />, content: <ScaleComponent {...scaleComponentProps} />,
@@ -917,16 +1003,16 @@ 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 (isEmpty(this.container.features())) { if (this.container.isEnableMongoCapabilityPresent()) {
tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab,
content: mongoIndexingPolicyAADError,
});
} else if (this.container.isEnableMongoCapabilityPresent()) {
tabs.push({ tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab, tab: SettingsV2TabTypes.IndexingPolicyTab,
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />, content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />,
}); });
} else {
tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab,
content: mongoIndexingPolicyAADError,
});
} }
} }

View File

@@ -375,7 +375,7 @@ export const getThroughputApplyShortDelayMessage = (
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage"> <Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
A request to increase the throughput is currently in progress. This operation will take some time to complete. A request to increase the throughput is currently in progress. This operation will take some time to complete.
<br /> <br />
Database: {databaseName}, Container: {collectionName}{" "} {collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)} {getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
</Text> </Text>
); );
@@ -392,7 +392,7 @@ export const getThroughputApplyLongDelayMessage = (
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to A request to increase the throughput is currently in progress. This operation will take 1-3 business days to
complete. View the latest status in Notifications. complete. View the latest status in Notifications.
<br /> <br />
Database: {databaseName}, Container: {collectionName}{" "} {collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)} {getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
</Text> </Text>
); );

View File

@@ -18,6 +18,7 @@ describe("ScaleComponent", () => {
const baseProps: ScaleComponentProps = { const baseProps: ScaleComponentProps = {
collection: collection, collection: collection,
database: undefined,
container: container, container: container,
isFixedContainer: false, isFixedContainer: false,
onThroughputChange: () => { onThroughputChange: () => {

View File

@@ -21,6 +21,7 @@ import { configContext, Platform } from "../../../../ConfigContext";
export interface ScaleComponentProps { export interface ScaleComponentProps {
collection: ViewModels.Collection; collection: ViewModels.Collection;
database: ViewModels.Database;
container: Explorer; container: Explorer;
isFixedContainer: boolean; isFixedContainer: boolean;
onThroughputChange: (newThroughput: number) => void; onThroughputChange: (newThroughput: number) => void;
@@ -39,9 +40,16 @@ export interface ScaleComponentProps {
export class ScaleComponent extends React.Component<ScaleComponentProps> { export class ScaleComponent extends React.Component<ScaleComponentProps> {
private isEmulator: boolean; private isEmulator: boolean;
private offer: DataModels.Offer;
private databaseId: string;
private collectionId: string;
constructor(props: ScaleComponentProps) { constructor(props: ScaleComponentProps) {
super(props); super(props);
this.isEmulator = configContext.platform === Platform.Emulator; this.isEmulator = configContext.platform === Platform.Emulator;
this.offer = this.props.database?.offer() || this.props.collection?.offer();
this.databaseId = this.props.database?.id() || this.props.collection.databaseId;
this.collectionId = this.props.collection?.id();
} }
public isAutoScaleEnabled = (): boolean => { public isAutoScaleEnabled = (): boolean => {
@@ -87,9 +95,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
return SharedConstants.CollectionCreation.DefaultCollectionRUs400; return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
} }
return ( return this.offer?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400;
this.props.collection.offer()?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400
);
}; };
public getThroughputTitle = (): string => { public getThroughputTitle = (): string => {
@@ -115,15 +121,14 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
return this.getLongDelayMessage(); return this.getLongDelayMessage();
} }
const offer = this.props.collection?.offer(); if (this.offer?.offerReplacePending) {
if (offer?.offerReplacePending) { const throughput = this.offer.manualThroughput || this.offer.autoscaleMaxThroughput;
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
return getThroughputApplyShortDelayMessage( return getThroughputApplyShortDelayMessage(
this.props.isAutoPilotSelected, this.props.isAutoPilotSelected,
throughput, throughput,
throughputUnit, throughputUnit,
this.props.collection.databaseId, this.databaseId,
this.props.collection.id() this.collectionId
); );
} }
@@ -135,7 +140,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
this.canThroughputExceedMaximumValue() && this.canThroughputExceedMaximumValue() &&
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million; this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) { if (throughputExceedsBackendLimits && !this.props.isFixedContainer) {
return updateThroughputBeyondLimitWarningMessage; return updateThroughputBeyondLimitWarningMessage;
} }
@@ -154,8 +159,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
this.props.wasAutopilotOriginallySet, this.props.wasAutopilotOriginallySet,
throughput, throughput,
throughputUnit, throughputUnit,
this.props.collection.databaseId, this.databaseId,
this.props.collection.id(), this.collectionId,
targetThroughput targetThroughput
); );
} }
@@ -165,15 +170,15 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
private getThroughputInputComponent = (): JSX.Element => ( private getThroughputInputComponent = (): JSX.Element => (
<ThroughputInputAutoPilotV3Component <ThroughputInputAutoPilotV3Component
databaseAccount={this.props.container.databaseAccount()} databaseAccount={this.props.container.databaseAccount()}
databaseName={this.props.collection.databaseId} databaseName={this.databaseId}
collectionName={this.props.collection.id()} collectionName={this.collectionId}
serverId={this.props.container.serverId()} 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}
minimum={this.getMinRUs()} minimum={this.getMinRUs()}
maximum={this.getMaxRUs()} maximum={this.getMaxRUs()}
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)} isEnabled={!!this.props.database || !hasDatabaseSharedThroughput(this.props.collection)}
canExceedMaximumValue={this.canThroughputExceedMaximumValue()} canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
label={this.getThroughputTitle()} label={this.getThroughputTitle()}
isEmulator={this.isEmulator} isEmulator={this.isEmulator}
@@ -189,7 +194,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
onScaleSaveableChange={this.props.onScaleSaveableChange} onScaleSaveableChange={this.props.onScaleSaveableChange}
onScaleDiscardableChange={this.props.onScaleDiscardableChange} onScaleDiscardableChange={this.props.onScaleDiscardableChange}
getThroughputWarningMessage={this.getThroughputWarningMessage} getThroughputWarningMessage={this.getThroughputWarningMessage}
usageSizeInKB={this.props.collection.usageSizeInKB()} usageSizeInKB={this.props.collection?.usageSizeInKB()}
/> />
); );
@@ -230,7 +235,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
{!this.isAutoScaleEnabled() && ( {!this.isAutoScaleEnabled() && (
<Stack {...subComponentStackProps}> <Stack {...subComponentStackProps}>
{this.getThroughputInputComponent()} {this.getThroughputInputComponent()}
{this.getStorageCapacityTitle()} {!this.props.database && this.getStorageCapacityTitle()}
</Stack> </Stack>
)} )}

View File

@@ -40,6 +40,7 @@ import { userContext } from "../../../../../UserContext";
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType"; import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
import { usageInGB, calculateEstimateNumber } from "../../../../../Utils/PricingUtils"; import { usageInGB, calculateEstimateNumber } from "../../../../../Utils/PricingUtils";
import { Features } from "../../../../../Common/Constants"; import { Features } from "../../../../../Common/Constants";
import { minAutoPilotThroughput } from "../../../../../Utils/AutoPilotUtils";
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
@@ -541,6 +542,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
step={AutoPilotUtils.autoPilotIncrementStep} step={AutoPilotUtils.autoPilotIncrementStep}
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()} value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
onChange={this.onAutoPilotThroughputChange} onChange={this.onAutoPilotThroughputChange}
min={minAutoPilotThroughput}
/> />
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()} {!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
{this.minRUperGBSurvey()} {this.minRUperGBSurvey()}
@@ -579,6 +581,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
: this.props.throughput?.toString() : this.props.throughput?.toString()
} }
onChange={this.onThroughputChange} onChange={this.onThroughputChange}
min={this.props.minimum}
/> />
{this.state.exceedFreeTierThroughput && ( {this.state.exceedFreeTierThroughput && (
<MessageBar <MessageBar

View File

@@ -142,6 +142,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
id="autopilotInput" id="autopilotInput"
key="auto pilot throughput input" key="auto pilot throughput input"
label="Max RU/s" label="Max RU/s"
min={4000}
onChange={[Function]} onChange={[Function]}
required={true} required={true}
step={1000} step={1000}
@@ -260,6 +261,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
disabled={false} disabled={false}
id="throughputInput" id="throughputInput"
key="provisioned throughput input" key="provisioned throughput input"
min={10000}
onChange={[Function]} onChange={[Function]}
required={true} required={true}
step={100} step={100}
@@ -533,6 +535,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
disabled={false} disabled={false}
id="throughputInput" id="throughputInput"
key="provisioned throughput input" key="provisioned throughput input"
min={10000}
onChange={[Function]} onChange={[Function]}
required={true} required={true}
step={100} step={100}

View File

@@ -23,11 +23,7 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
> >
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications. A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
<br /> <br />
Database: Database: test, Container: test
test
, Container:
test
, Current autoscale throughput: 100 - 1000 RU/s, Target autoscale throughput: 600 - 6000 RU/s , Current autoscale throughput: 100 - 1000 RU/s, Target autoscale throughput: 600 - 6000 RU/s
</Text> </Text>
</StyledMessageBarBase> </StyledMessageBarBase>

View File

@@ -46,6 +46,7 @@ describe("SettingsUtils", () => {
readSettings: undefined, readSettings: undefined,
onSettingsClick: undefined, onSettingsClick: undefined,
loadOffer: undefined, loadOffer: undefined,
getPendingThroughputSplitNotification: undefined,
} as ViewModels.Database; } as ViewModels.Database;
}; };
newCollection.offer(undefined); newCollection.offer(undefined);

View File

@@ -28,16 +28,11 @@ exports[`SettingsComponent renders 1`] = `
"changeFeedPolicy": [Function], "changeFeedPolicy": [Function],
"conflictResolutionPolicy": [Function], "conflictResolutionPolicy": [Function],
"container": Explorer { "container": Explorer {
"_addSynapseLinkDialogProps": [Function],
"_closeModalDialog": [Function], "_closeModalDialog": [Function],
"_closeSynapseLinkModalDialog": [Function], "_closeSynapseLinkModalDialog": [Function],
"_dialogProps": [Function],
"_importExplorerConfigComplete": false,
"_isAfecFeatureRegistered": [Function], "_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_isInitializingSparkConnectionInfo": false,
"_isSystemDatabasePredicate": [Function], "_isSystemDatabasePredicate": [Function],
"_openShareDialog": [Function],
"_panes": Array [ "_panes": Array [
AddDatabasePane { AddDatabasePane {
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
@@ -440,22 +435,6 @@ exports[`SettingsComponent renders 1`] = `
"validPartitionKeyValue": [Function], "validPartitionKeyValue": [Function],
"visible": [Function], "visible": [Function],
}, },
RenewAdHocAccessPane {
"_renewShareAccess": [Function],
"accessKey": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "renewadhocaccesspane",
"isExecuting": [Function],
"isHelperImageVisible": [Function],
"isTemplateReady": [Function],
"onShowHelperImageClick": [Function],
"onShowHelperImageKeyPress": [Function],
"title": [Function],
"visible": [Function],
},
UploadItemsPane { UploadItemsPane {
"container": [Circular], "container": [Circular],
"fileUploadSummaryText": [Function], "fileUploadSummaryText": [Function],
@@ -695,9 +674,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"addDatabaseText": [Function], "addDatabaseText": [Function],
"addSynapseLinkDialog": DialogComponentAdapter {
"parameters": [Function],
},
"addTableEntityPane": AddTableEntityPane { "addTableEntityPane": AddTableEntityPane {
"addButtonLabel": "Add Property", "addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name", "attributeNameLabel": "Property Name",
@@ -804,6 +780,8 @@ exports[`SettingsComponent renders 1`] = `
}, },
"clickHostedAccountSwitch": [Function], "clickHostedAccountSwitch": [Function],
"clickHostedDirectorySwitch": [Function], "clickHostedDirectorySwitch": [Function],
"closeDialog": undefined,
"closeSidePanel": undefined,
"collapsedResourceTreeWidth": 36, "collapsedResourceTreeWidth": 36,
"collectionCreationDefaults": Object { "collectionCreationDefaults": Object {
"storage": "100", "storage": "100",
@@ -858,9 +836,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"deleteDatabaseText": [Function], "deleteDatabaseText": [Function],
"dialogComponentAdapter": DialogComponentAdapter {
"parameters": [Function],
},
"editTableEntityPane": EditTableEntityPane { "editTableEntityPane": EditTableEntityPane {
"addButtonLabel": "Add Property", "addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name", "attributeNameLabel": "Property Name",
@@ -950,23 +925,18 @@ exports[`SettingsComponent renders 1`] = `
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isGalleryPublishEnabled": [Function],
"isGitHubPaneEnabled": [Function], "isGitHubPaneEnabled": [Function],
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function], "isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function],
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function],
"isPreferredApiCassandra": [Function], "isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function], "isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function], "isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function], "isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
"isReadToggled": [Function],
"isReadWriteToggled": [Function],
"isRefreshingExplorer": [Function], "isRefreshingExplorer": [Function],
"isResourceTokenCollectionNodeSelected": [Function], "isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function], "isRightPanelV2Enabled": [Function],
@@ -1018,16 +988,11 @@ exports[`SettingsComponent renders 1`] = `
"nonSystemDatabases": [Function], "nonSystemDatabases": [Function],
"notebookBasePath": [Function], "notebookBasePath": [Function],
"notebookServerInfo": [Function], "notebookServerInfo": [Function],
"notificationConsoleComponentAdapter": NotificationConsoleComponentAdapter {
"consoleData": [Function],
"container": [Circular],
"parameters": [Function],
},
"notificationConsoleData": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function], "openDialog": undefined,
"openSidePanel": undefined,
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -1056,24 +1021,6 @@ exports[`SettingsComponent renders 1`] = `
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
"renewAdHocAccessPane": RenewAdHocAccessPane {
"_renewShareAccess": [Function],
"accessKey": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "renewadhocaccesspane",
"isExecuting": [Function],
"isHelperImageVisible": [Function],
"isTemplateReady": [Function],
"onShowHelperImageClick": [Function],
"onShowHelperImageKeyPress": [Function],
"title": [Function],
"visible": [Function],
},
"renewToken": [Function],
"renewTokenError": [Function],
"resourceTokenCollection": [Function], "resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function], "resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function], "resourceTokenDatabaseId": [Function],
@@ -1129,6 +1076,9 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selfServeType": [Function], "selfServeType": [Function],
"serverId": [Function], "serverId": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined,
"setNotificationConsoleData": undefined,
"settingsPane": SettingsPane { "settingsPane": SettingsPane {
"container": [Circular], "container": [Circular],
"crossPartitionQueryEnabled": [Function], "crossPartitionQueryEnabled": [Function],
@@ -1166,22 +1116,8 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"shareAccessData": [Function],
"shareAccessToggleState": [Function],
"shareAccessUrl": [Function],
"shareTokenCopyHelperText": [Function],
"shareUrlCopyHelperText": [Function],
"shouldShowContextSwitchPrompt": [Function],
"shouldShowDataAccessExpiryDialog": [Function],
"shouldShowShareDialogContents": [Function],
"signInAad": [Function], "signInAad": [Function],
"sparkClusterConnectionInfo": [Function], "sparkClusterConnectionInfo": [Function],
"splashScreenAdapter": SplashScreenComponentAdapter {
"clearMostRecent": [Function],
"container": [Circular],
"forceRender": [Function],
"parameters": [Function],
},
"splitter": Splitter { "splitter": Splitter {
"bounds": Object { "bounds": Object {
"max": 400, "max": 400,
@@ -1239,9 +1175,6 @@ exports[`SettingsComponent renders 1`] = `
"openedTabs": [Function], "openedTabs": [Function],
}, },
"toggleLeftPaneExpandedKeyPress": [Function], "toggleLeftPaneExpandedKeyPress": [Function],
"toggleRead": [Function],
"toggleReadWrite": [Function],
"tokenForRenewal": [Function],
"uploadFilePane": UploadFilePane { "uploadFilePane": UploadFilePane {
"container": [Circular], "container": [Circular],
"extensions": [Function], "extensions": [Function],
@@ -1311,16 +1244,11 @@ exports[`SettingsComponent renders 1`] = `
} }
container={ container={
Explorer { Explorer {
"_addSynapseLinkDialogProps": [Function],
"_closeModalDialog": [Function], "_closeModalDialog": [Function],
"_closeSynapseLinkModalDialog": [Function], "_closeSynapseLinkModalDialog": [Function],
"_dialogProps": [Function],
"_importExplorerConfigComplete": false,
"_isAfecFeatureRegistered": [Function], "_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_isInitializingSparkConnectionInfo": false,
"_isSystemDatabasePredicate": [Function], "_isSystemDatabasePredicate": [Function],
"_openShareDialog": [Function],
"_panes": Array [ "_panes": Array [
AddDatabasePane { AddDatabasePane {
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
@@ -1723,22 +1651,6 @@ exports[`SettingsComponent renders 1`] = `
"validPartitionKeyValue": [Function], "validPartitionKeyValue": [Function],
"visible": [Function], "visible": [Function],
}, },
RenewAdHocAccessPane {
"_renewShareAccess": [Function],
"accessKey": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "renewadhocaccesspane",
"isExecuting": [Function],
"isHelperImageVisible": [Function],
"isTemplateReady": [Function],
"onShowHelperImageClick": [Function],
"onShowHelperImageKeyPress": [Function],
"title": [Function],
"visible": [Function],
},
UploadItemsPane { UploadItemsPane {
"container": [Circular], "container": [Circular],
"fileUploadSummaryText": [Function], "fileUploadSummaryText": [Function],
@@ -1978,9 +1890,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"addDatabaseText": [Function], "addDatabaseText": [Function],
"addSynapseLinkDialog": DialogComponentAdapter {
"parameters": [Function],
},
"addTableEntityPane": AddTableEntityPane { "addTableEntityPane": AddTableEntityPane {
"addButtonLabel": "Add Property", "addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name", "attributeNameLabel": "Property Name",
@@ -2087,6 +1996,8 @@ exports[`SettingsComponent renders 1`] = `
}, },
"clickHostedAccountSwitch": [Function], "clickHostedAccountSwitch": [Function],
"clickHostedDirectorySwitch": [Function], "clickHostedDirectorySwitch": [Function],
"closeDialog": undefined,
"closeSidePanel": undefined,
"collapsedResourceTreeWidth": 36, "collapsedResourceTreeWidth": 36,
"collectionCreationDefaults": Object { "collectionCreationDefaults": Object {
"storage": "100", "storage": "100",
@@ -2141,9 +2052,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"deleteDatabaseText": [Function], "deleteDatabaseText": [Function],
"dialogComponentAdapter": DialogComponentAdapter {
"parameters": [Function],
},
"editTableEntityPane": EditTableEntityPane { "editTableEntityPane": EditTableEntityPane {
"addButtonLabel": "Add Property", "addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name", "attributeNameLabel": "Property Name",
@@ -2233,23 +2141,18 @@ exports[`SettingsComponent renders 1`] = `
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isGalleryPublishEnabled": [Function],
"isGitHubPaneEnabled": [Function], "isGitHubPaneEnabled": [Function],
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function], "isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function],
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function],
"isPreferredApiCassandra": [Function], "isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function], "isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function], "isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function], "isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
"isReadToggled": [Function],
"isReadWriteToggled": [Function],
"isRefreshingExplorer": [Function], "isRefreshingExplorer": [Function],
"isResourceTokenCollectionNodeSelected": [Function], "isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function], "isRightPanelV2Enabled": [Function],
@@ -2301,16 +2204,11 @@ exports[`SettingsComponent renders 1`] = `
"nonSystemDatabases": [Function], "nonSystemDatabases": [Function],
"notebookBasePath": [Function], "notebookBasePath": [Function],
"notebookServerInfo": [Function], "notebookServerInfo": [Function],
"notificationConsoleComponentAdapter": NotificationConsoleComponentAdapter {
"consoleData": [Function],
"container": [Circular],
"parameters": [Function],
},
"notificationConsoleData": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function], "openDialog": undefined,
"openSidePanel": undefined,
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -2339,24 +2237,6 @@ exports[`SettingsComponent renders 1`] = `
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
"renewAdHocAccessPane": RenewAdHocAccessPane {
"_renewShareAccess": [Function],
"accessKey": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "renewadhocaccesspane",
"isExecuting": [Function],
"isHelperImageVisible": [Function],
"isTemplateReady": [Function],
"onShowHelperImageClick": [Function],
"onShowHelperImageKeyPress": [Function],
"title": [Function],
"visible": [Function],
},
"renewToken": [Function],
"renewTokenError": [Function],
"resourceTokenCollection": [Function], "resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function], "resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function], "resourceTokenDatabaseId": [Function],
@@ -2412,6 +2292,9 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selfServeType": [Function], "selfServeType": [Function],
"serverId": [Function], "serverId": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined,
"setNotificationConsoleData": undefined,
"settingsPane": SettingsPane { "settingsPane": SettingsPane {
"container": [Circular], "container": [Circular],
"crossPartitionQueryEnabled": [Function], "crossPartitionQueryEnabled": [Function],
@@ -2449,22 +2332,8 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"shareAccessData": [Function],
"shareAccessToggleState": [Function],
"shareAccessUrl": [Function],
"shareTokenCopyHelperText": [Function],
"shareUrlCopyHelperText": [Function],
"shouldShowContextSwitchPrompt": [Function],
"shouldShowDataAccessExpiryDialog": [Function],
"shouldShowShareDialogContents": [Function],
"signInAad": [Function], "signInAad": [Function],
"sparkClusterConnectionInfo": [Function], "sparkClusterConnectionInfo": [Function],
"splashScreenAdapter": SplashScreenComponentAdapter {
"clearMostRecent": [Function],
"container": [Circular],
"forceRender": [Function],
"parameters": [Function],
},
"splitter": Splitter { "splitter": Splitter {
"bounds": Object { "bounds": Object {
"max": 400, "max": 400,
@@ -2522,9 +2391,6 @@ exports[`SettingsComponent renders 1`] = `
"openedTabs": [Function], "openedTabs": [Function],
}, },
"toggleLeftPaneExpandedKeyPress": [Function], "toggleLeftPaneExpandedKeyPress": [Function],
"toggleRead": [Function],
"toggleReadWrite": [Function],
"tokenForRenewal": [Function],
"uploadFilePane": UploadFilePane { "uploadFilePane": UploadFilePane {
"container": [Circular], "container": [Circular],
"extensions": [Function], "extensions": [Function],
@@ -2607,16 +2473,11 @@ exports[`SettingsComponent renders 1`] = `
"changeFeedPolicy": [Function], "changeFeedPolicy": [Function],
"conflictResolutionPolicy": [Function], "conflictResolutionPolicy": [Function],
"container": Explorer { "container": Explorer {
"_addSynapseLinkDialogProps": [Function],
"_closeModalDialog": [Function], "_closeModalDialog": [Function],
"_closeSynapseLinkModalDialog": [Function], "_closeSynapseLinkModalDialog": [Function],
"_dialogProps": [Function],
"_importExplorerConfigComplete": false,
"_isAfecFeatureRegistered": [Function], "_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_isInitializingSparkConnectionInfo": false,
"_isSystemDatabasePredicate": [Function], "_isSystemDatabasePredicate": [Function],
"_openShareDialog": [Function],
"_panes": Array [ "_panes": Array [
AddDatabasePane { AddDatabasePane {
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
@@ -3019,22 +2880,6 @@ exports[`SettingsComponent renders 1`] = `
"validPartitionKeyValue": [Function], "validPartitionKeyValue": [Function],
"visible": [Function], "visible": [Function],
}, },
RenewAdHocAccessPane {
"_renewShareAccess": [Function],
"accessKey": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "renewadhocaccesspane",
"isExecuting": [Function],
"isHelperImageVisible": [Function],
"isTemplateReady": [Function],
"onShowHelperImageClick": [Function],
"onShowHelperImageKeyPress": [Function],
"title": [Function],
"visible": [Function],
},
UploadItemsPane { UploadItemsPane {
"container": [Circular], "container": [Circular],
"fileUploadSummaryText": [Function], "fileUploadSummaryText": [Function],
@@ -3274,9 +3119,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"addDatabaseText": [Function], "addDatabaseText": [Function],
"addSynapseLinkDialog": DialogComponentAdapter {
"parameters": [Function],
},
"addTableEntityPane": AddTableEntityPane { "addTableEntityPane": AddTableEntityPane {
"addButtonLabel": "Add Property", "addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name", "attributeNameLabel": "Property Name",
@@ -3383,6 +3225,8 @@ exports[`SettingsComponent renders 1`] = `
}, },
"clickHostedAccountSwitch": [Function], "clickHostedAccountSwitch": [Function],
"clickHostedDirectorySwitch": [Function], "clickHostedDirectorySwitch": [Function],
"closeDialog": undefined,
"closeSidePanel": undefined,
"collapsedResourceTreeWidth": 36, "collapsedResourceTreeWidth": 36,
"collectionCreationDefaults": Object { "collectionCreationDefaults": Object {
"storage": "100", "storage": "100",
@@ -3437,9 +3281,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"deleteDatabaseText": [Function], "deleteDatabaseText": [Function],
"dialogComponentAdapter": DialogComponentAdapter {
"parameters": [Function],
},
"editTableEntityPane": EditTableEntityPane { "editTableEntityPane": EditTableEntityPane {
"addButtonLabel": "Add Property", "addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name", "attributeNameLabel": "Property Name",
@@ -3529,23 +3370,18 @@ exports[`SettingsComponent renders 1`] = `
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isGalleryPublishEnabled": [Function],
"isGitHubPaneEnabled": [Function], "isGitHubPaneEnabled": [Function],
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function], "isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function],
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function],
"isPreferredApiCassandra": [Function], "isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function], "isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function], "isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function], "isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
"isReadToggled": [Function],
"isReadWriteToggled": [Function],
"isRefreshingExplorer": [Function], "isRefreshingExplorer": [Function],
"isResourceTokenCollectionNodeSelected": [Function], "isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function], "isRightPanelV2Enabled": [Function],
@@ -3597,16 +3433,11 @@ exports[`SettingsComponent renders 1`] = `
"nonSystemDatabases": [Function], "nonSystemDatabases": [Function],
"notebookBasePath": [Function], "notebookBasePath": [Function],
"notebookServerInfo": [Function], "notebookServerInfo": [Function],
"notificationConsoleComponentAdapter": NotificationConsoleComponentAdapter {
"consoleData": [Function],
"container": [Circular],
"parameters": [Function],
},
"notificationConsoleData": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function], "openDialog": undefined,
"openSidePanel": undefined,
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -3635,24 +3466,6 @@ exports[`SettingsComponent renders 1`] = `
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
"renewAdHocAccessPane": RenewAdHocAccessPane {
"_renewShareAccess": [Function],
"accessKey": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "renewadhocaccesspane",
"isExecuting": [Function],
"isHelperImageVisible": [Function],
"isTemplateReady": [Function],
"onShowHelperImageClick": [Function],
"onShowHelperImageKeyPress": [Function],
"title": [Function],
"visible": [Function],
},
"renewToken": [Function],
"renewTokenError": [Function],
"resourceTokenCollection": [Function], "resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function], "resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function], "resourceTokenDatabaseId": [Function],
@@ -3708,6 +3521,9 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selfServeType": [Function], "selfServeType": [Function],
"serverId": [Function], "serverId": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined,
"setNotificationConsoleData": undefined,
"settingsPane": SettingsPane { "settingsPane": SettingsPane {
"container": [Circular], "container": [Circular],
"crossPartitionQueryEnabled": [Function], "crossPartitionQueryEnabled": [Function],
@@ -3745,22 +3561,8 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"shareAccessData": [Function],
"shareAccessToggleState": [Function],
"shareAccessUrl": [Function],
"shareTokenCopyHelperText": [Function],
"shareUrlCopyHelperText": [Function],
"shouldShowContextSwitchPrompt": [Function],
"shouldShowDataAccessExpiryDialog": [Function],
"shouldShowShareDialogContents": [Function],
"signInAad": [Function], "signInAad": [Function],
"sparkClusterConnectionInfo": [Function], "sparkClusterConnectionInfo": [Function],
"splashScreenAdapter": SplashScreenComponentAdapter {
"clearMostRecent": [Function],
"container": [Circular],
"forceRender": [Function],
"parameters": [Function],
},
"splitter": Splitter { "splitter": Splitter {
"bounds": Object { "bounds": Object {
"max": 400, "max": 400,
@@ -3818,9 +3620,6 @@ exports[`SettingsComponent renders 1`] = `
"openedTabs": [Function], "openedTabs": [Function],
}, },
"toggleLeftPaneExpandedKeyPress": [Function], "toggleLeftPaneExpandedKeyPress": [Function],
"toggleRead": [Function],
"toggleReadWrite": [Function],
"tokenForRenewal": [Function],
"uploadFilePane": UploadFilePane { "uploadFilePane": UploadFilePane {
"container": [Circular], "container": [Circular],
"extensions": [Function], "extensions": [Function],
@@ -3890,16 +3689,11 @@ exports[`SettingsComponent renders 1`] = `
} }
container={ container={
Explorer { Explorer {
"_addSynapseLinkDialogProps": [Function],
"_closeModalDialog": [Function], "_closeModalDialog": [Function],
"_closeSynapseLinkModalDialog": [Function], "_closeSynapseLinkModalDialog": [Function],
"_dialogProps": [Function],
"_importExplorerConfigComplete": false,
"_isAfecFeatureRegistered": [Function], "_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_isInitializingSparkConnectionInfo": false,
"_isSystemDatabasePredicate": [Function], "_isSystemDatabasePredicate": [Function],
"_openShareDialog": [Function],
"_panes": Array [ "_panes": Array [
AddDatabasePane { AddDatabasePane {
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
@@ -4302,22 +4096,6 @@ exports[`SettingsComponent renders 1`] = `
"validPartitionKeyValue": [Function], "validPartitionKeyValue": [Function],
"visible": [Function], "visible": [Function],
}, },
RenewAdHocAccessPane {
"_renewShareAccess": [Function],
"accessKey": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "renewadhocaccesspane",
"isExecuting": [Function],
"isHelperImageVisible": [Function],
"isTemplateReady": [Function],
"onShowHelperImageClick": [Function],
"onShowHelperImageKeyPress": [Function],
"title": [Function],
"visible": [Function],
},
UploadItemsPane { UploadItemsPane {
"container": [Circular], "container": [Circular],
"fileUploadSummaryText": [Function], "fileUploadSummaryText": [Function],
@@ -4557,9 +4335,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"addDatabaseText": [Function], "addDatabaseText": [Function],
"addSynapseLinkDialog": DialogComponentAdapter {
"parameters": [Function],
},
"addTableEntityPane": AddTableEntityPane { "addTableEntityPane": AddTableEntityPane {
"addButtonLabel": "Add Property", "addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name", "attributeNameLabel": "Property Name",
@@ -4666,6 +4441,8 @@ exports[`SettingsComponent renders 1`] = `
}, },
"clickHostedAccountSwitch": [Function], "clickHostedAccountSwitch": [Function],
"clickHostedDirectorySwitch": [Function], "clickHostedDirectorySwitch": [Function],
"closeDialog": undefined,
"closeSidePanel": undefined,
"collapsedResourceTreeWidth": 36, "collapsedResourceTreeWidth": 36,
"collectionCreationDefaults": Object { "collectionCreationDefaults": Object {
"storage": "100", "storage": "100",
@@ -4720,9 +4497,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"deleteDatabaseText": [Function], "deleteDatabaseText": [Function],
"dialogComponentAdapter": DialogComponentAdapter {
"parameters": [Function],
},
"editTableEntityPane": EditTableEntityPane { "editTableEntityPane": EditTableEntityPane {
"addButtonLabel": "Add Property", "addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name", "attributeNameLabel": "Property Name",
@@ -4812,23 +4586,18 @@ exports[`SettingsComponent renders 1`] = `
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isGalleryPublishEnabled": [Function],
"isGitHubPaneEnabled": [Function], "isGitHubPaneEnabled": [Function],
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function], "isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function],
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function],
"isPreferredApiCassandra": [Function], "isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function], "isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function], "isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function], "isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
"isReadToggled": [Function],
"isReadWriteToggled": [Function],
"isRefreshingExplorer": [Function], "isRefreshingExplorer": [Function],
"isResourceTokenCollectionNodeSelected": [Function], "isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function], "isRightPanelV2Enabled": [Function],
@@ -4880,16 +4649,11 @@ exports[`SettingsComponent renders 1`] = `
"nonSystemDatabases": [Function], "nonSystemDatabases": [Function],
"notebookBasePath": [Function], "notebookBasePath": [Function],
"notebookServerInfo": [Function], "notebookServerInfo": [Function],
"notificationConsoleComponentAdapter": NotificationConsoleComponentAdapter {
"consoleData": [Function],
"container": [Circular],
"parameters": [Function],
},
"notificationConsoleData": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function], "openDialog": undefined,
"openSidePanel": undefined,
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -4918,24 +4682,6 @@ exports[`SettingsComponent renders 1`] = `
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
"renewAdHocAccessPane": RenewAdHocAccessPane {
"_renewShareAccess": [Function],
"accessKey": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "renewadhocaccesspane",
"isExecuting": [Function],
"isHelperImageVisible": [Function],
"isTemplateReady": [Function],
"onShowHelperImageClick": [Function],
"onShowHelperImageKeyPress": [Function],
"title": [Function],
"visible": [Function],
},
"renewToken": [Function],
"renewTokenError": [Function],
"resourceTokenCollection": [Function], "resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function], "resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function], "resourceTokenDatabaseId": [Function],
@@ -4991,6 +4737,9 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selfServeType": [Function], "selfServeType": [Function],
"serverId": [Function], "serverId": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined,
"setNotificationConsoleData": undefined,
"settingsPane": SettingsPane { "settingsPane": SettingsPane {
"container": [Circular], "container": [Circular],
"crossPartitionQueryEnabled": [Function], "crossPartitionQueryEnabled": [Function],
@@ -5028,22 +4777,8 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"shareAccessData": [Function],
"shareAccessToggleState": [Function],
"shareAccessUrl": [Function],
"shareTokenCopyHelperText": [Function],
"shareUrlCopyHelperText": [Function],
"shouldShowContextSwitchPrompt": [Function],
"shouldShowDataAccessExpiryDialog": [Function],
"shouldShowShareDialogContents": [Function],
"signInAad": [Function], "signInAad": [Function],
"sparkClusterConnectionInfo": [Function], "sparkClusterConnectionInfo": [Function],
"splashScreenAdapter": SplashScreenComponentAdapter {
"clearMostRecent": [Function],
"container": [Circular],
"forceRender": [Function],
"parameters": [Function],
},
"splitter": Splitter { "splitter": Splitter {
"bounds": Object { "bounds": Object {
"max": 400, "max": 400,
@@ -5101,9 +4836,6 @@ exports[`SettingsComponent renders 1`] = `
"openedTabs": [Function], "openedTabs": [Function],
}, },
"toggleLeftPaneExpandedKeyPress": [Function], "toggleLeftPaneExpandedKeyPress": [Function],
"toggleRead": [Function],
"toggleReadWrite": [Function],
"tokenForRenewal": [Function],
"uploadFilePane": UploadFilePane { "uploadFilePane": UploadFilePane {
"container": [Circular], "container": [Circular],
"extensions": [Function], "extensions": [Function],

View File

@@ -256,11 +256,7 @@ exports[`SettingsUtils functions render 1`] = `
> >
A request to increase the throughput is currently in progress. This operation will take some time to complete. A request to increase the throughput is currently in progress. This operation will take some time to complete.
<br /> <br />
Database: Database: sampleDb, Container: sampleCollection
sampleDb
, Container:
sampleCollection
, Current manual throughput: 1000 RU/s , Current manual throughput: 1000 RU/s
</Text> </Text>
<Text <Text
@@ -275,11 +271,7 @@ exports[`SettingsUtils functions render 1`] = `
> >
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications. A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
<br /> <br />
Database: Database: sampleDb, Container: sampleCollection
sampleDb
, Container:
sampleCollection
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000 , Current manual throughput: 1000 RU/s, Target manual throughput: 2000
</Text> </Text>
<Text <Text

View File

@@ -1,63 +1,78 @@
import React from "react"; import React from "react";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import { SmartUiComponent, SmartUiDescriptor, UiType } from "./SmartUiComponent"; import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent";
import { NumberUiType, SmartUiInput } from "../../../SelfServe/SelfServeTypes";
describe("SmartUiComponent", () => { describe("SmartUiComponent", () => {
const exampleData: SmartUiDescriptor = { const exampleData: SmartUiDescriptor = {
root: { root: {
id: "root", id: "root",
info: { info: {
message: "Start at $24/mo per database", messageTKey: "Start at $24/mo per database",
link: { link: {
href: "https://aka.ms/azure-cosmos-db-pricing", href: "https://aka.ms/azure-cosmos-db-pricing",
text: "More Details", textTKey: "More Details",
}, },
}, },
children: [ children: [
{
id: "description",
input: {
dataFieldName: "description",
type: "string",
description: {
textTKey: "this is an example description text.",
link: {
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
textTKey: "Click here for more information.",
},
},
},
},
{ {
id: "throughput", id: "throughput",
input: { input: {
label: "Throughput (input)", labelTKey: "Throughput (input)",
dataFieldName: "throughput", dataFieldName: "throughput",
type: "number", type: "number",
min: 400, min: 400,
max: 500, max: 500,
step: 10, step: 10,
defaultValue: 400, defaultValue: 400,
uiType: UiType.Spinner, uiType: NumberUiType.Spinner,
}, },
}, },
{ {
id: "throughput2", id: "throughput2",
input: { input: {
label: "Throughput (Slider)", labelTKey: "Throughput (Slider)",
dataFieldName: "throughput2", dataFieldName: "throughput2",
type: "number", type: "number",
min: 400, min: 400,
max: 500, max: 500,
step: 10, step: 10,
defaultValue: 400, defaultValue: 400,
uiType: UiType.Slider, uiType: NumberUiType.Slider,
}, },
}, },
{ {
id: "throughput3", id: "throughput3",
input: { input: {
label: "Throughput (invalid)", labelTKey: "Throughput (invalid)",
dataFieldName: "throughput3", dataFieldName: "throughput3",
type: "boolean", type: "boolean",
min: 400, min: 400,
max: 500, max: 500,
step: 10, step: 10,
defaultValue: 400, defaultValue: 400,
uiType: UiType.Spinner, uiType: NumberUiType.Spinner,
errorMessage: "label, truelabel and falselabel are required for boolean input 'throughput3'", errorMessage: "label, truelabel and falselabel are required for boolean input 'throughput3'",
}, },
}, },
{ {
id: "containerId", id: "containerId",
input: { input: {
label: "Container id", labelTKey: "Container id",
dataFieldName: "containerId", dataFieldName: "containerId",
type: "string", type: "string",
}, },
@@ -65,9 +80,9 @@ describe("SmartUiComponent", () => {
{ {
id: "analyticalStore", id: "analyticalStore",
input: { input: {
label: "Analytical Store", labelTKey: "Analytical Store",
trueLabel: "Enabled", trueLabelTKey: "Enabled",
falseLabel: "Disabled", falseLabelTKey: "Disabled",
defaultValue: true, defaultValue: true,
dataFieldName: "analyticalStore", dataFieldName: "analyticalStore",
type: "boolean", type: "boolean",
@@ -76,7 +91,7 @@ describe("SmartUiComponent", () => {
{ {
id: "database", id: "database",
input: { input: {
label: "Database", labelTKey: "Database",
dataFieldName: "database", dataFieldName: "database",
type: "object", type: "object",
choices: [ choices: [
@@ -91,11 +106,64 @@ describe("SmartUiComponent", () => {
}, },
}; };
it("should render", async () => { it("should render and honor input's hidden, disabled state", async () => {
const currentValues = new Map<string, SmartUiInput>();
const wrapper = shallow( const wrapper = shallow(
<SmartUiComponent descriptor={exampleData} currentValues={new Map()} onInputChange={undefined} /> <SmartUiComponent
disabled={false}
descriptor={exampleData}
currentValues={currentValues}
onInputChange={jest.fn()}
onError={() => {
return;
}}
getTranslation={(key: string) => {
return key;
}}
/>
); );
await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0));
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#containerId-textField-input")).toBeTruthy();
currentValues.set("containerId", { value: "container1", hidden: true });
wrapper.setProps({ currentValues });
wrapper.update();
expect(wrapper.exists("#containerId-textField-input")).toBeFalsy();
currentValues.set("containerId", { value: "container1", hidden: false, disabled: true });
wrapper.setProps({ currentValues });
wrapper.update();
const containerIdTextField = wrapper.find("#containerId-textField-input");
expect(containerIdTextField.props().disabled).toBeTruthy();
});
it("disable all inputs", async () => {
const wrapper = shallow(
<SmartUiComponent
disabled={true}
descriptor={exampleData}
currentValues={new Map()}
onInputChange={jest.fn()}
onError={() => {
return;
}}
getTranslation={(key: string) => {
return key;
}}
/>
);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(wrapper).toMatchSnapshot();
const throughputSpinner = wrapper.find("#throughput-spinner-input");
expect(throughputSpinner.props().disabled).toBeTruthy();
const throughput2Slider = wrapper.find("#throughput2-slider-input").childAt(0);
expect(throughput2Slider.props().disabled).toBeTruthy();
const containerIdTextField = wrapper.find("#containerId-textField-input");
expect(containerIdTextField.props().disabled).toBeTruthy();
const analyticalStoreToggle = wrapper.find("#analyticalStore-toggle-input");
expect(analyticalStoreToggle.props().disabled).toBeTruthy();
const databaseDropdown = wrapper.find("#database-dropdown-input");
expect(databaseDropdown.props().disabled).toBeTruthy();
}); });
}); });

View File

@@ -5,11 +5,20 @@ import { SpinButton } from "office-ui-fabric-react/lib/SpinButton";
import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown"; import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
import { TextField } from "office-ui-fabric-react/lib/TextField"; 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 { RadioSwitchComponent } from "../RadioSwitchComponent/RadioSwitchComponent";
import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack"; import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack";
import { Link, MessageBar, MessageBarType } from "office-ui-fabric-react"; import { Link, MessageBar, MessageBarType, Toggle } from "office-ui-fabric-react";
import * as InputUtils from "./InputUtils"; import * as InputUtils from "./InputUtils";
import "./SmartUiComponent.less"; import "./SmartUiComponent.less";
import {
ChoiceItem,
Description,
Info,
InputType,
InputTypeValue,
NumberUiType,
SmartUiInput,
} from "../../../SelfServe/SelfServeTypes";
import { TFunction } from "i18next";
/** /**
* Generic UX renderer * Generic UX renderer
@@ -19,30 +28,15 @@ import "./SmartUiComponent.less";
* - a descriptor of the UX. * - a descriptor of the UX.
*/ */
export type InputTypeValue = "number" | "string" | "boolean" | "object"; interface BaseDisplay {
export enum UiType {
Spinner = "Spinner",
Slider = "Slider",
}
export type ChoiceItem = { label: string; key: string };
export type InputType = number | string | boolean | ChoiceItem;
export interface Info {
message: string;
link?: {
href: string;
text: string;
};
}
interface BaseInput {
label: string;
dataFieldName: string; dataFieldName: string;
errorMessage?: string;
type: InputTypeValue; type: InputTypeValue;
placeholder?: string; }
interface BaseInput extends BaseDisplay {
labelTKey: string;
placeholderTKey?: string;
errorMessage?: string; errorMessage?: string;
} }
@@ -54,12 +48,12 @@ interface NumberInput extends BaseInput {
max: number; max: number;
step: number; step: number;
defaultValue?: number; defaultValue?: number;
uiType: UiType; uiType: NumberUiType;
} }
interface BooleanInput extends BaseInput { interface BooleanInput extends BaseInput {
trueLabel: string; trueLabelTKey: string;
falseLabel: string; falseLabelTKey: string;
defaultValue?: boolean; defaultValue?: boolean;
} }
@@ -72,12 +66,16 @@ interface ChoiceInput extends BaseInput {
defaultKey?: string; defaultKey?: string;
} }
type AnyInput = NumberInput | BooleanInput | StringInput | ChoiceInput; interface DescriptionDisplay extends BaseDisplay {
description: Description;
}
type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay;
interface Node { interface Node {
id: string; id: string;
info?: Info; info?: Info;
input?: AnyInput; input?: AnyDisplay;
children?: Node[]; children?: Node[];
} }
@@ -86,11 +84,13 @@ export interface SmartUiDescriptor {
} }
/************************** Component implementation starts here ************************************* */ /************************** Component implementation starts here ************************************* */
export interface SmartUiComponentProps { export interface SmartUiComponentProps {
descriptor: SmartUiDescriptor; descriptor: SmartUiDescriptor;
currentValues: Map<string, InputType>; currentValues: Map<string, SmartUiInput>;
onInputChange: (input: AnyInput, newValue: InputType) => void; onInputChange: (input: AnyDisplay, newValue: InputType) => void;
onError: (hasError: boolean) => void;
disabled: boolean;
getTranslation: TFunction;
} }
interface SmartUiComponentState { interface SmartUiComponentState {
@@ -98,12 +98,22 @@ interface SmartUiComponentState {
} }
export class SmartUiComponent extends React.Component<SmartUiComponentProps, SmartUiComponentState> { export class SmartUiComponent extends React.Component<SmartUiComponentProps, SmartUiComponentState> {
private shouldCheckErrors = true;
private static readonly labelStyle = { private static readonly labelStyle = {
color: "#393939", color: "#393939",
fontFamily: "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", fontFamily: "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
fontSize: 12, fontSize: 12,
}; };
componentDidUpdate(): void {
if (!this.shouldCheckErrors) {
this.shouldCheckErrors = true;
return;
}
this.props.onError(this.state.errors.size > 0);
this.shouldCheckErrors = false;
}
constructor(props: SmartUiComponentProps) { constructor(props: SmartUiComponentProps) {
super(props); super(props);
this.state = { this.state = {
@@ -113,11 +123,11 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
private renderInfo(info: Info): JSX.Element { private renderInfo(info: Info): JSX.Element {
return ( return (
<MessageBar> <MessageBar styles={{ root: { width: 400 } }}>
{info.message} {this.props.getTranslation(info.messageTKey)}
{info.link && ( {info.link && (
<Link href={info.link.href} target="_blank"> <Link href={info.link.href} target="_blank">
{info.link.text} {this.props.getTranslation(info.link.textTKey)}
</Link> </Link>
)} )}
</MessageBar> </MessageBar>
@@ -125,17 +135,20 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
} }
private renderTextInput(input: StringInput): JSX.Element { private renderTextInput(input: StringInput): JSX.Element {
const value = this.props.currentValues.get(input.dataFieldName) 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;
return ( return (
<div className="stringInputContainer"> <div className="stringInputContainer">
<TextField <TextField
id={`${input.dataFieldName}-textBox-input`} id={`${input.dataFieldName}-textField-input`}
label={input.label} label={this.props.getTranslation(input.labelTKey)}
type="text" type="text"
value={value} value={value || ""}
placeholder={input.placeholder} placeholder={this.props.getTranslation(input.placeholderTKey)}
disabled={disabled}
onChange={(_, newValue) => this.props.onInputChange(input, newValue)} onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
styles={{ styles={{
root: { width: 400 },
subComponentStyles: { subComponentStyles: {
label: { label: {
root: { root: {
@@ -150,13 +163,27 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
); );
} }
private renderDescription(input: DescriptionDisplay): JSX.Element {
const description = input.description;
return (
<Text id={`${input.dataFieldName}-text-display`}>
{this.props.getTranslation(input.description.textTKey)}{" "}
{description.link && (
<Link target="_blank" href={input.description.link.href}>
{this.props.getTranslation(input.description.link.textTKey)}
</Link>
)}
</Text>
);
}
private clearError(dataFieldName: string): void { private clearError(dataFieldName: string): void {
const { errors } = this.state; const { errors } = this.state;
errors.delete(dataFieldName); errors.delete(dataFieldName);
this.setState({ errors }); this.setState({ errors });
} }
private onValidate = (input: AnyInput, value: string, min: number, max: number): string => { private onValidate = (input: NumberInput, value: string, min: number, max: number): string => {
const newValue = InputUtils.onValidateValueChange(value, min, max); const newValue = InputUtils.onValidateValueChange(value, min, max);
const dataFieldName = input.dataFieldName; const dataFieldName = input.dataFieldName;
if (newValue) { if (newValue) {
@@ -165,13 +192,13 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return newValue.toString(); return newValue.toString();
} else { } else {
const { errors } = this.state; const { errors } = this.state;
errors.set(dataFieldName, `Invalid value ${value}: must be between ${min} and ${max}`); errors.set(dataFieldName, `Invalid value '${value}'. It must be between ${min} and ${max}`);
this.setState({ errors }); this.setState({ errors });
} }
return undefined; return undefined;
}; };
private onIncrement = (input: AnyInput, value: string, step: number, max: number): string => { private onIncrement = (input: NumberInput, value: string, step: number, max: number): string => {
const newValue = InputUtils.onIncrementValue(value, step, max); const newValue = InputUtils.onIncrementValue(value, step, max);
const dataFieldName = input.dataFieldName; const dataFieldName = input.dataFieldName;
if (newValue) { if (newValue) {
@@ -182,7 +209,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return undefined; return undefined;
}; };
private onDecrement = (input: AnyInput, value: string, step: number, min: number): string => { private onDecrement = (input: NumberInput, value: string, step: number, min: number): string => {
const newValue = InputUtils.onDecrementValue(value, step, min); const newValue = InputUtils.onDecrementValue(value, step, min);
const dataFieldName = input.dataFieldName; const dataFieldName = input.dataFieldName;
if (newValue) { if (newValue) {
@@ -194,19 +221,20 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
}; };
private renderNumberInput(input: NumberInput): JSX.Element { private renderNumberInput(input: NumberInput): JSX.Element {
const { label, min, max, dataFieldName, step } = input; const { labelTKey, min, max, dataFieldName, step } = input;
const props = { const props = {
label: label, label: this.props.getTranslation(labelTKey),
min: min, min: min,
max: max, max: max,
ariaLabel: label, ariaLabel: labelTKey,
step: step, step: step,
}; };
const value = this.props.currentValues.get(dataFieldName) as number; const value = this.props.currentValues.get(dataFieldName)?.value as number;
if (input.uiType === UiType.Spinner) { const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
if (input.uiType === NumberUiType.Spinner) {
return ( return (
<> <Stack styles={{ root: { width: 400 } }} tokens={{ childrenGap: 2 }}>
<SpinButton <SpinButton
{...props} {...props}
id={`${input.dataFieldName}-spinner-input`} id={`${input.dataFieldName}-spinner-input`}
@@ -215,6 +243,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
onIncrement={(newValue) => this.onIncrement(input, newValue, props.step, props.max)} onIncrement={(newValue) => this.onIncrement(input, newValue, props.step, props.max)}
onDecrement={(newValue) => this.onDecrement(input, newValue, props.step, props.min)} onDecrement={(newValue) => this.onDecrement(input, newValue, props.step, props.min)}
labelPosition={Position.top} labelPosition={Position.top}
disabled={disabled}
styles={{ styles={{
label: { label: {
...SmartUiComponent.labelStyle, ...SmartUiComponent.labelStyle,
@@ -225,16 +254,18 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
{this.state.errors.has(dataFieldName) && ( {this.state.errors.has(dataFieldName) && (
<MessageBar messageBarType={MessageBarType.error}>Error: {this.state.errors.get(dataFieldName)}</MessageBar> <MessageBar messageBarType={MessageBarType.error}>Error: {this.state.errors.get(dataFieldName)}</MessageBar>
)} )}
</> </Stack>
); );
} else if (input.uiType === UiType.Slider) { } else if (input.uiType === NumberUiType.Slider) {
return ( return (
<div id={`${input.dataFieldName}-slider-input`}> <div id={`${input.dataFieldName}-slider-input`}>
<Slider <Slider
{...props} {...props}
value={value} value={value}
disabled={disabled}
onChange={(newValue) => this.props.onInputChange(input, newValue)} onChange={(newValue) => this.props.onInputChange(input, newValue)}
styles={{ styles={{
root: { width: 400 },
titleLabel: { titleLabel: {
...SmartUiComponent.labelStyle, ...SmartUiComponent.labelStyle,
fontWeight: 600, fontWeight: 600,
@@ -250,49 +281,44 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
} }
private renderBooleanInput(input: BooleanInput): JSX.Element { private renderBooleanInput(input: BooleanInput): JSX.Element {
const value = this.props.currentValues.get(input.dataFieldName) as boolean; const value = this.props.currentValues.get(input.dataFieldName)?.value as boolean;
const selectedKey = value || input.defaultValue ? "true" : "false"; const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled;
return ( return (
<div id={`${input.dataFieldName}-radioSwitch-input`}> <Toggle
<div className="inputLabelContainer"> id={`${input.dataFieldName}-toggle-input`}
<Text variant="small" nowrap className="inputLabel"> label={this.props.getTranslation(input.labelTKey)}
{input.label} checked={value || false}
</Text> onText={this.props.getTranslation(input.trueLabelTKey)}
</div> offText={this.props.getTranslation(input.falseLabelTKey)}
<RadioSwitchComponent disabled={disabled}
choices={[ onChange={(event, checked: boolean) => this.props.onInputChange(input, checked)}
{ styles={{ root: { width: 400 } }}
label: input.falseLabel, />
key: "false",
onSelect: () => this.props.onInputChange(input, false),
},
{
label: input.trueLabel,
key: "true",
onSelect: () => this.props.onInputChange(input, true),
},
]}
selectedKey={selectedKey}
/>
</div>
); );
} }
private renderChoiceInput(input: ChoiceInput): JSX.Element { private renderChoiceInput(input: ChoiceInput): JSX.Element {
const { label, defaultKey: defaultKey, dataFieldName, choices, placeholder } = input; const { labelTKey, defaultKey, dataFieldName, choices, placeholderTKey } = input;
const value = this.props.currentValues.get(dataFieldName) as string; const value = this.props.currentValues.get(dataFieldName)?.value as string;
const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
let selectedKey = value ? value : defaultKey;
if (!selectedKey) {
selectedKey = "";
}
return ( return (
<Dropdown <Dropdown
id={`${input.dataFieldName}-dropown-input`} id={`${input.dataFieldName}-dropdown-input`}
label={label} label={this.props.getTranslation(labelTKey)}
selectedKey={value ? value : defaultKey} selectedKey={selectedKey}
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())} onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
placeholder={placeholder} placeholder={this.props.getTranslation(placeholderTKey)}
disabled={disabled}
options={choices.map((c) => ({ options={choices.map((c) => ({
key: c.key, key: c.key,
text: c.label, text: this.props.getTranslation(c.label),
}))} }))}
styles={{ styles={{
root: { width: 400 },
label: { label: {
...SmartUiComponent.labelStyle, ...SmartUiComponent.labelStyle,
fontWeight: 600, fontWeight: 600,
@@ -303,16 +329,23 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
); );
} }
private renderError(input: AnyInput): JSX.Element { private renderError(input: AnyDisplay): JSX.Element {
return <MessageBar messageBarType={MessageBarType.error}>Error: {input.errorMessage}</MessageBar>; return <MessageBar messageBarType={MessageBarType.error}>Error: {input.errorMessage}</MessageBar>;
} }
private renderInput(input: AnyInput): JSX.Element { private renderDisplay(input: AnyDisplay): JSX.Element {
if (input.errorMessage) { if (input.errorMessage) {
return this.renderError(input); return this.renderError(input);
} }
const inputHidden = this.props.currentValues.get(input.dataFieldName)?.hidden;
if (inputHidden) {
return <></>;
}
switch (input.type) { switch (input.type) {
case "string": case "string":
if ("description" in input) {
return this.renderDescription(input as DescriptionDisplay);
}
return this.renderTextInput(input as StringInput); return this.renderTextInput(input as StringInput);
case "number": case "number":
return this.renderNumberInput(input as NumberInput); return this.renderNumberInput(input as NumberInput);
@@ -326,13 +359,13 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
} }
private renderNode(node: Node): JSX.Element { private renderNode(node: Node): JSX.Element {
const containerStackTokens: IStackTokens = { childrenGap: 15 }; const containerStackTokens: IStackTokens = { childrenGap: 10 };
return ( return (
<Stack tokens={containerStackTokens} className="widgetRendererContainer"> <Stack tokens={containerStackTokens} className="widgetRendererContainer">
<Stack.Item> <Stack.Item>
{node.info && this.renderInfo(node.info as Info)} {node.info && this.renderInfo(node.info as Info)}
{node.input && this.renderInput(node.input)} {node.input && this.renderDisplay(node.input)}
</Stack.Item> </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>
@@ -340,11 +373,6 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
} }
render(): JSX.Element { render(): JSX.Element {
const containerStackTokens: IStackTokens = { childrenGap: 20 }; return this.renderNode(this.props.descriptor.root);
return (
<Stack tokens={containerStackTokens} styles={{ root: { width: 400, padding: 10 } }}>
{this.renderNode(this.props.descriptor.root)}
</Stack>
);
} }
} }

View File

@@ -1,52 +1,405 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SmartUiComponent should render 1`] = ` exports[`SmartUiComponent disable all inputs 1`] = `
<Stack <Stack
styles={ className="widgetRendererContainer"
Object {
"root": Object {
"padding": 10,
"width": 400,
},
}
}
tokens={ tokens={
Object { Object {
"childrenGap": 20, "childrenGap": 10,
} }
} }
> >
<Stack <StackItem>
className="widgetRendererContainer" <StyledMessageBarBase
tokens={ styles={
Object { Object {
"childrenGap": 15, "root": Object {
} "width": 400,
} },
>
<StackItem>
<StyledMessageBarBase>
Start at $24/mo per database
<StyledLinkBase
href="https://aka.ms/azure-cosmos-db-pricing"
target="_blank"
>
More Details
</StyledLinkBase>
</StyledMessageBarBase>
</StackItem>
<div
key="throughput"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 15,
}
} }
}
>
Start at $24/mo per database
<StyledLinkBase
href="https://aka.ms/azure-cosmos-db-pricing"
target="_blank"
> >
<StackItem> More Details
</StyledLinkBase>
</StyledMessageBarBase>
</StackItem>
<div
key="description"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
>
<StackItem>
<Text
id="description-text-display"
>
this is an example description text.
<StyledLinkBase
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
target="_blank"
>
Click here for more information.
</StyledLinkBase>
</Text>
</StackItem>
</Stack>
</div>
<div
key="throughput"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
>
<StackItem>
<Stack
styles={
Object {
"root": Object {
"width": 400,
},
}
}
tokens={
Object {
"childrenGap": 2,
}
}
>
<CustomizedSpinButton
ariaLabel="Throughput (input)"
decrementButtonIcon={
Object {
"iconName": "ChevronDownSmall",
}
}
disabled={true}
id="throughput-spinner-input"
incrementButtonIcon={
Object {
"iconName": "ChevronUpSmall",
}
}
label="Throughput (input)"
labelPosition={0}
max={500}
min={400}
onDecrement={[Function]}
onIncrement={[Function]}
onValidate={[Function]}
step={10}
styles={
Object {
"label": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
}
}
/>
</Stack>
</StackItem>
</Stack>
</div>
<div
key="throughput2"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
>
<StackItem>
<div
id="throughput2-slider-input"
>
<StyledSliderBase
ariaLabel="Throughput (Slider)"
disabled={true}
label="Throughput (Slider)"
max={500}
min={400}
onChange={[Function]}
step={10}
styles={
Object {
"root": Object {
"width": 400,
},
"titleLabel": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
"valueLabel": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
}
}
/>
</div>
</StackItem>
</Stack>
</div>
<div
key="throughput3"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
>
<StackItem>
<StyledMessageBarBase
messageBarType={1}
>
Error:
label, truelabel and falselabel are required for boolean input 'throughput3'
</StyledMessageBarBase>
</StackItem>
</Stack>
</div>
<div
key="containerId"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
>
<StackItem>
<div
className="stringInputContainer"
>
<StyledTextFieldBase
disabled={true}
id="containerId-textField-input"
label="Container id"
onChange={[Function]}
styles={
Object {
"root": Object {
"width": 400,
},
"subComponentStyles": Object {
"label": Object {
"root": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
},
},
}
}
type="text"
value=""
/>
</div>
</StackItem>
</Stack>
</div>
<div
key="analyticalStore"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
>
<StackItem>
<StyledToggleBase
checked={false}
disabled={true}
id="analyticalStore-toggle-input"
label="Analytical Store"
offText="Disabled"
onChange={[Function]}
onText="Enabled"
styles={
Object {
"root": Object {
"width": 400,
},
}
}
/>
</StackItem>
</Stack>
</div>
<div
key="database"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
>
<StackItem>
<StyledWithResponsiveMode
disabled={true}
id="database-dropdown-input"
label="Database"
onChange={[Function]}
options={
Array [
Object {
"key": "db1",
"text": "Database 1",
},
Object {
"key": "db2",
"text": "Database 2",
},
Object {
"key": "db3",
"text": "Database 3",
},
]
}
selectedKey="db2"
styles={
Object {
"dropdown": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
"label": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
"root": Object {
"width": 400,
},
}
}
/>
</StackItem>
</Stack>
</div>
</Stack>
`;
exports[`SmartUiComponent should render and honor input's hidden, disabled state 1`] = `
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
>
<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
key="description"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
>
<StackItem>
<Text
id="description-text-display"
>
this is an example description text.
<StyledLinkBase
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
target="_blank"
>
Click here for more information.
</StyledLinkBase>
</Text>
</StackItem>
</Stack>
</div>
<div
key="throughput"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
>
<StackItem>
<Stack
styles={
Object {
"root": Object {
"width": 400,
},
}
}
tokens={
Object {
"childrenGap": 2,
}
}
>
<CustomizedSpinButton <CustomizedSpinButton
ariaLabel="Throughput (input)" ariaLabel="Throughput (input)"
decrementButtonIcon={ decrementButtonIcon={
@@ -80,210 +433,203 @@ exports[`SmartUiComponent should render 1`] = `
} }
} }
/> />
</StackItem> </Stack>
</Stack> </StackItem>
</div> </Stack>
<div </div>
key="throughput2" <div
> key="throughput2"
<Stack >
className="widgetRendererContainer" <Stack
tokens={ className="widgetRendererContainer"
Object { tokens={
"childrenGap": 15, Object {
} "childrenGap": 10,
} }
> }
<StackItem>
<div
id="throughput2-slider-input"
>
<StyledSliderBase
ariaLabel="Throughput (Slider)"
label="Throughput (Slider)"
max={500}
min={400}
onChange={[Function]}
step={10}
styles={
Object {
"titleLabel": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
"valueLabel": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
}
}
/>
</div>
</StackItem>
</Stack>
</div>
<div
key="throughput3"
> >
<Stack <StackItem>
className="widgetRendererContainer" <div
tokens={ id="throughput2-slider-input"
Object { >
"childrenGap": 15, <StyledSliderBase
} ariaLabel="Throughput (Slider)"
} label="Throughput (Slider)"
> max={500}
<StackItem> min={400}
<StyledMessageBarBase
messageBarType={1}
>
Error:
label, truelabel and falselabel are required for boolean input 'throughput3'
</StyledMessageBarBase>
</StackItem>
</Stack>
</div>
<div
key="containerId"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 15,
}
}
>
<StackItem>
<div
className="stringInputContainer"
>
<StyledTextFieldBase
id="containerId-textBox-input"
label="Container id"
onChange={[Function]}
styles={
Object {
"subComponentStyles": Object {
"label": Object {
"root": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
},
},
}
}
type="text"
/>
</div>
</StackItem>
</Stack>
</div>
<div
key="analyticalStore"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 15,
}
}
>
<StackItem>
<div
id="analyticalStore-radioSwitch-input"
>
<div
className="inputLabelContainer"
>
<Text
className="inputLabel"
nowrap={true}
variant="small"
>
Analytical Store
</Text>
</div>
<RadioSwitchComponent
choices={
Array [
Object {
"key": "false",
"label": "Disabled",
"onSelect": [Function],
},
Object {
"key": "true",
"label": "Enabled",
"onSelect": [Function],
},
]
}
selectedKey="true"
/>
</div>
</StackItem>
</Stack>
</div>
<div
key="database"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 15,
}
}
>
<StackItem>
<StyledWithResponsiveMode
id="database-dropown-input"
label="Database"
onChange={[Function]} onChange={[Function]}
options={ step={10}
Array [
Object {
"key": "db1",
"text": "Database 1",
},
Object {
"key": "db2",
"text": "Database 2",
},
Object {
"key": "db3",
"text": "Database 3",
},
]
}
selectedKey="db2"
styles={ styles={
Object { Object {
"dropdown": Object { "root": Object {
"color": "#393939", "width": 400,
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
}, },
"label": Object { "titleLabel": Object {
"color": "#393939", "color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12, "fontSize": 12,
"fontWeight": 600, "fontWeight": 600,
}, },
"valueLabel": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
} }
} }
/> />
</StackItem> </div>
</Stack> </StackItem>
</div> </Stack>
</Stack> </div>
<div
key="throughput3"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
>
<StackItem>
<StyledMessageBarBase
messageBarType={1}
>
Error:
label, truelabel and falselabel are required for boolean input 'throughput3'
</StyledMessageBarBase>
</StackItem>
</Stack>
</div>
<div
key="containerId"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
>
<StackItem>
<div
className="stringInputContainer"
>
<StyledTextFieldBase
id="containerId-textField-input"
label="Container id"
onChange={[Function]}
styles={
Object {
"root": Object {
"width": 400,
},
"subComponentStyles": Object {
"label": Object {
"root": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
},
},
}
}
type="text"
value=""
/>
</div>
</StackItem>
</Stack>
</div>
<div
key="analyticalStore"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
>
<StackItem>
<StyledToggleBase
checked={false}
id="analyticalStore-toggle-input"
label="Analytical Store"
offText="Disabled"
onChange={[Function]}
onText="Enabled"
styles={
Object {
"root": Object {
"width": 400,
},
}
}
/>
</StackItem>
</Stack>
</div>
<div
key="database"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
>
<StackItem>
<StyledWithResponsiveMode
id="database-dropdown-input"
label="Database"
onChange={[Function]}
options={
Array [
Object {
"key": "db1",
"text": "Database 1",
},
Object {
"key": "db2",
"text": "Database 2",
},
Object {
"key": "db3",
"text": "Database 3",
},
]
}
selectedKey="db2"
styles={
Object {
"dropdown": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
"label": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
"root": Object {
"width": 400,
},
}
}
/>
</StackItem>
</Stack>
</div>
</Stack> </Stack>
`; `;

View File

@@ -1,3 +1,4 @@
import React from "react";
import * as ComponentRegisterer from "./ComponentRegisterer"; import * as ComponentRegisterer from "./ComponentRegisterer";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
@@ -20,7 +21,6 @@ import { readDatabases } from "../Common/dataAccess/readDatabases";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane"; import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
import GraphStylingPane from "./Panes/GraphStylingPane"; import GraphStylingPane from "./Panes/GraphStylingPane";
import hasher from "hasher";
import NewVertexPane from "./Panes/NewVertexPane"; import NewVertexPane from "./Panes/NewVertexPane";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import Q from "q"; import Q from "q";
@@ -28,7 +28,7 @@ import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import TerminalTab from "./Tabs/TerminalTab"; import TerminalTab from "./Tabs/TerminalTab";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import { ActionContracts, MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager"; import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager";
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker"; import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
@@ -40,26 +40,21 @@ import { configContext, Platform, updateConfigContext } from "../ConfigContext";
import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils"; import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { DialogComponentAdapter } from "./Controls/DialogReactComponent/DialogComponentAdapter"; import { DialogProps, TextFieldProps } from "./Controls/Dialog";
import { DialogProps, TextFieldProps } from "./Controls/DialogReactComponent/DialogComponent";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane";
import { ExplorerMetrics } from "../Common/Constants"; import { ExplorerMetrics } from "../Common/Constants";
import { ExplorerSettings } from "../Shared/ExplorerSettings"; import { ExplorerSettings } from "../Shared/ExplorerSettings";
import { FileSystemUtil } from "./Notebook/FileSystemUtil"; import { FileSystemUtil } from "./Notebook/FileSystemUtil";
import { handleOpenAction } from "./OpenActions";
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
import { IGalleryItem } from "../Juno/JunoClient"; import { IGalleryItem } from "../Juno/JunoClient";
import { LoadQueryPane } from "./Panes/LoadQueryPane"; import { LoadQueryPane } from "./Panes/LoadQueryPane";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { sendMessage, sendCachedDataMessage, handleCachedDataMessage } from "../Common/MessageHandler"; import { sendMessage, sendCachedDataMessage } from "../Common/MessageHandler";
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 { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
import { NotificationConsoleComponentAdapter } from "./Menus/NotificationConsole/NotificationConsoleComponentAdapter";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { QueriesClient } from "../Common/QueriesClient"; import { QueriesClient } from "../Common/QueriesClient";
import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane"; import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane";
import { RenewAdHocAccessPane } from "./Panes/RenewAdHocAccessPane";
import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory";
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
@@ -67,7 +62,7 @@ import { RouteHandler } from "../RouteHandlers/RouteHandler";
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 { SplashScreenComponentAdapter } from "./SplashScreen/SplashScreenComponentApdapter"; import { SplashScreen } from "./SplashScreen/SplashScreen";
import { Splitter, SplitterBounds, SplitterDirection } from "../Common/Splitter"; import { Splitter, SplitterBounds, SplitterDirection } from "../Common/Splitter";
import { StringInputPane } from "./Panes/StringInputPane"; import { StringInputPane } from "./Panes/StringInputPane";
import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane"; import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane";
@@ -92,19 +87,21 @@ import { appInsights } from "../Shared/appInsights";
import { SelfServeLoadingComponentAdapter } from "../SelfServe/SelfServeLoadingComponentAdapter"; import { SelfServeLoadingComponentAdapter } from "../SelfServe/SelfServeLoadingComponentAdapter";
import { SelfServeType } from "../SelfServe/SelfServeUtils"; import { SelfServeType } from "../SelfServe/SelfServeUtils";
import { SelfServeComponentAdapter } from "../SelfServe/SelfServeComponentAdapter"; 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
var tmp = ComponentRegisterer; var tmp = ComponentRegisterer;
enum ShareAccessToggleState { export interface ExplorerParams {
ReadWrite, setIsNotificationConsoleExpanded: (isExpanded: boolean) => void;
Read, setNotificationConsoleData: (consoleData: ConsoleData) => void;
} setInProgressConsoleDataIdToBeDeleted: (id: string) => void;
openSidePanel: (headerText: string, panelContent: JSX.Element) => void;
interface AdHocAccessData { closeSidePanel: () => void;
readWriteUrl: string; closeDialog: () => void;
readUrl: string; openDialog: (props: DialogProps) => void;
} }
export default class Explorer { export default class Explorer {
@@ -146,11 +143,14 @@ export default class Explorer {
public mostRecentActivity: MostRecentActivity.MostRecentActivity; public mostRecentActivity: MostRecentActivity.MostRecentActivity;
// Notification Console // Notification Console
public notificationConsoleData: ko.ObservableArray<ConsoleData>; private setIsNotificationConsoleExpanded: (isExpanded: boolean) => void;
public isNotificationConsoleExpanded: ko.Observable<boolean>; private setNotificationConsoleData: (consoleData: ConsoleData) => void;
private setInProgressConsoleDataIdToBeDeleted: (id: string) => void;
// Panes // Panes
public contextPanes: ContextualPaneBase[]; public contextPanes: ContextualPaneBase[];
public openSidePanel: (headerText: string, panelContent: JSX.Element) => void;
public closeSidePanel: () => void;
// Resource Tree // Resource Tree
public databases: ko.ObservableArray<ViewModels.Database>; public databases: ko.ObservableArray<ViewModels.Database>;
@@ -192,7 +192,6 @@ export default class Explorer {
public cassandraAddCollectionPane: CassandraAddCollectionPane; public cassandraAddCollectionPane: CassandraAddCollectionPane;
public settingsPane: SettingsPane; public settingsPane: SettingsPane;
public executeSprocParamsPane: ExecuteSprocParamsPane; public executeSprocParamsPane: ExecuteSprocParamsPane;
public renewAdHocAccessPane: RenewAdHocAccessPane;
public uploadItemsPane: UploadItemsPane; public uploadItemsPane: UploadItemsPane;
public uploadItemsPaneAdapter: UploadItemsPaneAdapter; public uploadItemsPaneAdapter: UploadItemsPaneAdapter;
public loadQueryPane: LoadQueryPane; public loadQueryPane: LoadQueryPane;
@@ -206,8 +205,6 @@ export default class Explorer {
public copyNotebookPaneAdapter: ReactAdapter; public copyNotebookPaneAdapter: ReactAdapter;
// features // features
public isGalleryPublishEnabled: ko.Computed<boolean>;
public isLinkInjectionEnabled: ko.Computed<boolean>;
public isGitHubPaneEnabled: ko.Observable<boolean>; public isGitHubPaneEnabled: ko.Observable<boolean>;
public isPublishNotebookPaneEnabled: ko.Observable<boolean>; public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
public isCopyNotebookPaneEnabled: ko.Observable<boolean>; public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
@@ -217,17 +214,6 @@ export default class Explorer {
public canExceedMaximumValue: ko.Computed<boolean>; public canExceedMaximumValue: ko.Computed<boolean>;
public isAutoscaleDefaultEnabled: ko.Observable<boolean>; public isAutoscaleDefaultEnabled: ko.Observable<boolean>;
public shouldShowShareDialogContents: ko.Observable<boolean>;
public shareAccessData: ko.Observable<AdHocAccessData>;
public renewExplorerShareAccess: (explorer: Explorer, token: string) => Q.Promise<void>;
public renewTokenError: ko.Observable<string>;
public tokenForRenewal: ko.Observable<string>;
public shareAccessToggleState: ko.Observable<ShareAccessToggleState>;
public shareAccessUrl: ko.Observable<string>;
public shareUrlCopyHelperText: ko.Observable<string>;
public shareTokenCopyHelperText: ko.Observable<string>;
public shouldShowDataAccessExpiryDialog: ko.Observable<boolean>;
public shouldShowContextSwitchPrompt: ko.Observable<boolean>;
public isSchemaEnabled: ko.Computed<boolean>; public isSchemaEnabled: ko.Computed<boolean>;
// Notebooks // Notebooks
@@ -244,12 +230,12 @@ export default class Explorer {
public isSynapseLinkUpdating: ko.Observable<boolean>; public isSynapseLinkUpdating: ko.Observable<boolean>;
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>; public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
public notebookManager?: any; // This is dynamically loaded public notebookManager?: any; // This is dynamically loaded
public openDialog: ExplorerParams["openDialog"];
public closeDialog: ExplorerParams["closeDialog"];
private _panes: ContextualPaneBase[] = []; private _panes: ContextualPaneBase[] = [];
private _importExplorerConfigComplete: boolean = false;
private _isSystemDatabasePredicate: (database: ViewModels.Database) => boolean = (database) => false; private _isSystemDatabasePredicate: (database: ViewModels.Database) => boolean = (database) => false;
private _isInitializingNotebooks: boolean; private _isInitializingNotebooks: boolean;
private _isInitializingSparkConnectionInfo: boolean;
private notebookBasePath: ko.Observable<string>; private notebookBasePath: ko.Observable<string>;
private _arcadiaManager: ArcadiaResourceManager; private _arcadiaManager: ArcadiaResourceManager;
private notebookToImport: { private notebookToImport: {
@@ -259,17 +245,19 @@ export default class Explorer {
// React adapters // React adapters
private commandBarComponentAdapter: CommandBarComponentAdapter; private commandBarComponentAdapter: CommandBarComponentAdapter;
private splashScreenAdapter: SplashScreenComponentAdapter;
private notificationConsoleComponentAdapter: NotificationConsoleComponentAdapter;
private dialogComponentAdapter: DialogComponentAdapter;
private _dialogProps: ko.Observable<DialogProps>;
private addSynapseLinkDialog: DialogComponentAdapter;
private _addSynapseLinkDialogProps: ko.Observable<DialogProps>;
private selfServeLoadingComponentAdapter: SelfServeLoadingComponentAdapter; private selfServeLoadingComponentAdapter: SelfServeLoadingComponentAdapter;
private static readonly MaxNbDatabasesToAutoExpand = 5; private static readonly MaxNbDatabasesToAutoExpand = 5;
constructor() { constructor(params?: ExplorerParams) {
this.setIsNotificationConsoleExpanded = params?.setIsNotificationConsoleExpanded;
this.setNotificationConsoleData = params?.setNotificationConsoleData;
this.setInProgressConsoleDataIdToBeDeleted = params?.setInProgressConsoleDataIdToBeDeleted;
this.openSidePanel = params?.openSidePanel;
this.closeSidePanel = params?.closeSidePanel;
this.closeDialog = params?.closeDialog;
this.openDialog = params?.openDialog;
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
@@ -303,7 +291,6 @@ export default class Explorer {
this.isAccountReady = ko.observable<boolean>(false); this.isAccountReady = ko.observable<boolean>(false);
this.selfServeType = ko.observable<SelfServeType>(undefined); this.selfServeType = ko.observable<SelfServeType>(undefined);
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
this._isInitializingSparkConnectionInfo = false;
this.arcadiaToken = ko.observable<string>(); this.arcadiaToken = ko.observable<string>();
this.arcadiaToken.subscribe((token: string) => { this.arcadiaToken.subscribe((token: string) => {
if (token) { if (token) {
@@ -393,33 +380,6 @@ export default class Explorer {
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.isAuthWithResourceToken = ko.observable<boolean>(false);
this.shareAccessData = ko.observable<AdHocAccessData>({
readWriteUrl: undefined,
readUrl: undefined,
});
this.tokenForRenewal = ko.observable<string>("");
this.renewTokenError = ko.observable<string>("");
this.shareAccessUrl = ko.observable<string>();
this.shareUrlCopyHelperText = ko.observable<string>("Click to copy");
this.shareTokenCopyHelperText = ko.observable<string>("Click to copy");
this.shareAccessToggleState = ko.observable<ShareAccessToggleState>(ShareAccessToggleState.ReadWrite);
this.shareAccessToggleState.subscribe((toggleState: ShareAccessToggleState) => {
if (toggleState === ShareAccessToggleState.ReadWrite) {
this.shareAccessUrl(this.shareAccessData && this.shareAccessData().readWriteUrl);
} else {
this.shareAccessUrl(this.shareAccessData && this.shareAccessData().readUrl);
}
});
this.shouldShowShareDialogContents = ko.observable<boolean>(false);
this.shouldShowDataAccessExpiryDialog = ko.observable<boolean>(false);
this.shouldShowContextSwitchPrompt = ko.observable<boolean>(false);
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
);
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
);
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);
@@ -430,7 +390,6 @@ export default class Explorer {
); );
this.isSchemaEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema)); this.isSchemaEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema));
this.isNotificationConsoleExpanded = ko.observable<boolean>(false);
this.isAutoscaleDefaultEnabled = ko.observable<boolean>(false); this.isAutoscaleDefaultEnabled = ko.observable<boolean>(false);
@@ -478,7 +437,6 @@ export default class Explorer {
bounds: splitterBounds, bounds: splitterBounds,
direction: SplitterDirection.Vertical, direction: SplitterDirection.Vertical,
}); });
this.notificationConsoleData = ko.observableArray<ConsoleData>([]);
this.defaultExperience = ko.observable<string>(); this.defaultExperience = ko.observable<string>();
this.databaseAccount.subscribe((databaseAccount) => { this.databaseAccount.subscribe((databaseAccount) => {
const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount( const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount(
@@ -700,13 +658,6 @@ export default class Explorer {
container: this, container: this,
}); });
this.renewAdHocAccessPane = new RenewAdHocAccessPane({
id: "renewadhocaccesspane",
visible: ko.observable<boolean>(false),
container: this,
});
this.uploadItemsPane = new UploadItemsPane({ this.uploadItemsPane = new UploadItemsPane({
id: "uploaditemspane", id: "uploaditemspane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -775,7 +726,6 @@ export default class Explorer {
this.cassandraAddCollectionPane, this.cassandraAddCollectionPane,
this.settingsPane, this.settingsPane,
this.executeSprocParamsPane, this.executeSprocParamsPane,
this.renewAdHocAccessPane,
this.uploadItemsPane, this.uploadItemsPane,
this.loadQueryPane, this.loadQueryPane,
this.saveQueryPane, this.saveQueryPane,
@@ -892,7 +842,6 @@ export default class Explorer {
this.commandBarComponentAdapter = new CommandBarComponentAdapter(this); this.commandBarComponentAdapter = new CommandBarComponentAdapter(this);
this.selfServeLoadingComponentAdapter = new SelfServeLoadingComponentAdapter(); this.selfServeLoadingComponentAdapter = new SelfServeLoadingComponentAdapter();
this.notificationConsoleComponentAdapter = new NotificationConsoleComponentAdapter(this);
this._initSettings(); this._initSettings();
@@ -911,7 +860,6 @@ export default class Explorer {
this.notebookManager = new notebookManagerModule.default(); this.notebookManager = new notebookManagerModule.default();
this.notebookManager.initialize({ this.notebookManager.initialize({
container: this, container: this,
dialogProps: this._dialogProps,
notebookBasePath: this.notebookBasePath, notebookBasePath: this.notebookBasePath,
resourceTree: this.resourceTree, resourceTree: this.resourceTree,
refreshCommandBarButtons: () => this.refreshCommandBarButtons(), refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
@@ -979,33 +927,7 @@ export default class Explorer {
featureSubcription.dispose(); featureSubcription.dispose();
}); });
this._dialogProps = ko.observable<DialogProps>({
isModal: false,
visible: false,
title: undefined,
subText: undefined,
primaryButtonText: undefined,
secondaryButtonText: undefined,
onPrimaryButtonClick: undefined,
onSecondaryButtonClick: undefined,
});
this.dialogComponentAdapter = new DialogComponentAdapter();
this.dialogComponentAdapter.parameters = this._dialogProps;
this.splashScreenAdapter = new SplashScreenComponentAdapter(this);
this.mostRecentActivity = new MostRecentActivity.MostRecentActivity(this); this.mostRecentActivity = new MostRecentActivity.MostRecentActivity(this);
this._addSynapseLinkDialogProps = ko.observable<DialogProps>({
isModal: false,
visible: false,
title: undefined,
subText: undefined,
primaryButtonText: undefined,
secondaryButtonText: undefined,
onPrimaryButtonClick: undefined,
onSecondaryButtonClick: undefined,
});
this.addSynapseLinkDialog = new DialogComponentAdapter();
this.addSynapseLinkDialog.parameters = this._addSynapseLinkDialogProps;
} }
public openEnableSynapseLinkDialog(): void { public openEnableSynapseLinkDialog(): void {
@@ -1067,257 +989,12 @@ export default class Explorer {
TelemetryProcessor.traceCancel(Action.EnableAzureSynapseLink); TelemetryProcessor.traceCancel(Action.EnableAzureSynapseLink);
}, },
}; };
this._addSynapseLinkDialogProps(addSynapseLinkDialogProps); this.openDialog(addSynapseLinkDialogProps);
TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink); TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink);
// TODO: return result // TODO: return result
} }
public copyUrlLink(src: any, event: MouseEvent): void {
const urlLinkInput: HTMLInputElement = document.getElementById("shareUrlLink") as HTMLInputElement;
urlLinkInput && urlLinkInput.select();
document.execCommand("copy");
this.shareUrlCopyHelperText("Copied");
setTimeout(() => this.shareUrlCopyHelperText("Click to copy"), Constants.ClientDefaults.copyHelperTimeoutMs);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Copy full screen URL",
databaseAccountName: this.databaseAccount() && this.databaseAccount().name,
defaultExperience: this.defaultExperience && this.defaultExperience(),
dataExplorerArea: Constants.Areas.ShareDialog,
});
}
public onCopyUrlLinkKeyPress(src: any, event: KeyboardEvent): boolean {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.copyUrlLink(src, null);
return false;
}
return true;
}
public copyToken(src: any, event: MouseEvent): void {
const tokenInput: HTMLInputElement = document.getElementById("shareToken") as HTMLInputElement;
tokenInput && tokenInput.select();
document.execCommand("copy");
this.shareTokenCopyHelperText("Copied");
setTimeout(() => this.shareTokenCopyHelperText("Click to copy"), Constants.ClientDefaults.copyHelperTimeoutMs);
}
public onCopyTokenKeyPress(src: any, event: KeyboardEvent): boolean {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.copyToken(src, null);
return false;
}
return true;
}
public renewToken = (): void => {
TelemetryProcessor.trace(Action.ConnectEncryptionToken);
this.renewTokenError("");
const id: string = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
"Initiating connection to account"
);
this.renewExplorerShareAccess(this, this.tokenForRenewal())
.fail((error: any) => {
const stringifiedError: string = getErrorMessage(error);
this.renewTokenError("Invalid connection string specified");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to initiate connection to account: ${stringifiedError}`
);
})
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
};
public generateSharedAccessData(): void {
const id: string = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Generating share url");
AuthHeadersUtil.generateEncryptedToken().then(
(tokenResponse: DataModels.GenerateTokenResponse) => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully generated share url");
this.shareAccessData({
readWriteUrl: this._getShareAccessUrlForToken(tokenResponse.readWrite),
readUrl: this._getShareAccessUrlForToken(tokenResponse.read),
});
!this.shareAccessData().readWriteUrl && this.shareAccessToggleState(ShareAccessToggleState.Read); // select read toggle by default for readers
this.shareAccessToggleState.valueHasMutated(); // to set initial url and token state
this.shareAccessData.valueHasMutated();
this._openShareDialog();
},
(error: any) => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to generate share url: ${getErrorMessage(error)}`
);
console.error(error);
}
);
}
public renewShareAccess(token: string): Q.Promise<void> {
if (!this.renewExplorerShareAccess) {
return Q.reject("Not implemented");
}
const deferred: Q.Deferred<void> = Q.defer<void>();
const id: string = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
"Initiating connection to account"
);
this.renewExplorerShareAccess(this, token)
.then(
() => {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Connection successful");
this.renewAdHocAccessPane && this.renewAdHocAccessPane.close();
deferred.resolve();
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to connect: ${getErrorMessage(error)}`
);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
public displayGuestAccessTokenRenewalPrompt(): void {
if (!$("#dataAccessTokenModal").dialog("instance")) {
const connectButton = {
text: "Connect",
class: "connectDialogButtons connectButton connectOkBtns",
click: () => {
this.renewAdHocAccessPane.open();
$("#dataAccessTokenModal").dialog("close");
},
};
const cancelButton = {
text: "Cancel",
class: "connectDialogButtons cancelBtn",
click: () => {
$("#dataAccessTokenModal").dialog("close");
},
};
$("#dataAccessTokenModal").dialog({
autoOpen: false,
buttons: [connectButton, cancelButton],
closeOnEscape: false,
draggable: false,
dialogClass: "no-close",
height: 180,
modal: true,
position: { my: "center center", at: "center center", of: window },
resizable: false,
title: "Temporary access expired",
width: 435,
close: (event: Event, ui: JQueryUI.DialogUIParams) => this.shouldShowDataAccessExpiryDialog(false),
});
$("#dataAccessTokenModal").dialog("option", "classes", {
"ui-dialog-titlebar": "connectTitlebar",
});
}
this.shouldShowDataAccessExpiryDialog(true);
$("#dataAccessTokenModal").dialog("open");
}
public isConnectExplorerVisible(): boolean {
return $("#connectExplorer").is(":visible") || false;
}
public displayContextSwitchPromptForConnectionString(connectionString: string): void {
const yesButton = {
text: "OK",
class: "connectDialogButtons okBtn connectOkBtns",
click: () => {
$("#contextSwitchPrompt").dialog("close");
this.tabsManager.closeTabs(); // clear all tabs so we dont leave any tabs from previous session open
this.renewShareAccess(connectionString);
},
};
const noButton = {
text: "Cancel",
class: "connectDialogButtons cancelBtn",
click: () => {
$("#contextSwitchPrompt").dialog("close");
},
};
if (!$("#contextSwitchPrompt").dialog("instance")) {
$("#contextSwitchPrompt").dialog({
autoOpen: false,
buttons: [yesButton, noButton],
closeOnEscape: false,
draggable: false,
dialogClass: "no-close",
height: 255,
modal: true,
position: { my: "center center", at: "center center", of: window },
resizable: false,
title: "Switch account",
width: 440,
close: (event: Event, ui: JQueryUI.DialogUIParams) => this.shouldShowDataAccessExpiryDialog(false),
});
$("#contextSwitchPrompt").dialog("option", "classes", {
"ui-dialog-titlebar": "connectTitlebar",
});
$("#contextSwitchPrompt").dialog("option", "open", (event: Event, ui: JQueryUI.DialogUIParams) => {
$(".ui-dialog ").css("z-index", 1001);
$("#contextSwitchPrompt").parent().siblings(".ui-widget-overlay").css("z-index", 1000);
});
}
$("#contextSwitchPrompt").dialog("option", "buttons", [yesButton, noButton]); // rebind buttons so callbacks accept current connection string
this.shouldShowContextSwitchPrompt(true);
$("#contextSwitchPrompt").dialog("open");
}
public displayConnectExplorerForm(): void {
$("#divExplorer").hide();
$("#connectExplorer").css("display", "flex");
}
public hideConnectExplorerForm(): void {
$("#connectExplorer").hide();
$("#divExplorer").show();
}
public isReadWriteToggled: () => boolean = (): boolean => {
return this.shareAccessToggleState() === ShareAccessToggleState.ReadWrite;
};
public isReadToggled: () => boolean = (): boolean => {
return this.shareAccessToggleState() === ShareAccessToggleState.Read;
};
public toggleReadWrite: (src: any, event: MouseEvent) => void = (src: any, event: MouseEvent) => {
this.shareAccessToggleState(ShareAccessToggleState.ReadWrite);
};
public toggleRead: (src: any, event: MouseEvent) => void = (src: any, event: MouseEvent) => {
this.shareAccessToggleState(ShareAccessToggleState.Read);
};
public onToggleKeyDown: (src: any, event: KeyboardEvent) => boolean = (src: any, event: KeyboardEvent) => {
if (event.keyCode === Constants.KeyCodes.LeftArrow) {
this.toggleReadWrite(src, null);
return false;
} else if (event.keyCode === Constants.KeyCodes.RightArrow) {
this.toggleRead(src, null);
return false;
}
return true;
};
public isDatabaseNodeOrNoneSelected(): boolean { public isDatabaseNodeOrNoneSelected(): boolean {
return this.isNoneSelected() || this.isDatabaseNodeSelected(); return this.isNoneSelected() || this.isDatabaseNodeSelected();
} }
@@ -1349,23 +1026,19 @@ export default class Explorer {
} }
public logConsoleData(consoleData: ConsoleData): void { public logConsoleData(consoleData: ConsoleData): void {
this.notificationConsoleData.splice(0, 0, consoleData); this.setNotificationConsoleData(consoleData);
} }
public deleteInProgressConsoleDataWithId(id: string): void { public deleteInProgressConsoleDataWithId(id: string): void {
const updatedConsoleData = _.reject( this.setInProgressConsoleDataIdToBeDeleted(id);
this.notificationConsoleData(),
(data: ConsoleData) => data.type === ConsoleDataType.InProgress && data.id === id
);
this.notificationConsoleData(updatedConsoleData);
} }
public expandConsole(): void { public expandConsole(): void {
this.isNotificationConsoleExpanded(true); this.setIsNotificationConsoleExpanded(true);
} }
public collapseConsole(): void { public collapseConsole(): void {
this.isNotificationConsoleExpanded(false); this.setIsNotificationConsoleExpanded(false);
} }
public toggleLeftPaneExpanded() { public toggleLeftPaneExpanded() {
@@ -1635,6 +1308,7 @@ export default class Explorer {
); );
return; return;
} }
const resetConfirmationDialogProps: DialogProps = { const resetConfirmationDialogProps: DialogProps = {
isModal: true, isModal: true,
visible: true, visible: true,
@@ -1645,7 +1319,7 @@ export default class Explorer {
onPrimaryButtonClick: this._resetNotebookWorkspace, onPrimaryButtonClick: this._resetNotebookWorkspace,
onSecondaryButtonClick: this._closeModalDialog, onSecondaryButtonClick: this._closeModalDialog,
}; };
this._dialogProps(resetConfirmationDialogProps); this.openDialog(resetConfirmationDialogProps);
} }
private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise<boolean> { private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise<boolean> {
@@ -1709,120 +1383,13 @@ export default class Explorer {
}; };
private _closeModalDialog = () => { private _closeModalDialog = () => {
this._dialogProps().visible = false; this.closeDialog();
this._dialogProps.valueHasMutated();
}; };
private _closeSynapseLinkModalDialog = () => { private _closeSynapseLinkModalDialog = () => {
this._addSynapseLinkDialogProps().visible = false; this.closeDialog();
this._addSynapseLinkDialogProps.valueHasMutated();
}; };
private _shouldProcessMessage(event: MessageEvent): boolean {
if (typeof event.data !== "object") {
return false;
}
if (event.data["signature"] !== "pcIframe") {
return false;
}
if (!("data" in event.data)) {
return false;
}
if (typeof event.data["data"] !== "object") {
return false;
}
// before initialization completed give exception
const message = event.data.data;
if (!this._importExplorerConfigComplete && message && message.type) {
const messageType = message.type;
switch (messageType) {
case MessageTypes.SendNotification:
case MessageTypes.ClearNotification:
case MessageTypes.LoadingStatus:
case MessageTypes.InitTestExplorer:
return true;
}
}
if (!("inputs" in event.data["data"]) && !this._importExplorerConfigComplete) {
return false;
}
return true;
}
public handleMessage(event: MessageEvent) {
if (isInvalidParentFrameOrigin(event)) {
return;
}
if (!this._shouldProcessMessage(event)) {
return;
}
const message: any = event.data.data;
const inputs: ViewModels.DataExplorerInputsFrame = message.inputs;
const isRunningInPortal = configContext.platform === Platform.Portal;
const isRunningInDevMode = process.env.NODE_ENV === "development";
if (inputs && configContext.BACKEND_ENDPOINT && isRunningInPortal && isRunningInDevMode) {
inputs.extensionEndpoint = configContext.PROXY_PATH;
}
this.initDataExplorerWithFrameInputs(inputs);
const openAction: ActionContracts.DataExplorerAction = message.openAction;
if (!!openAction) {
if (this.isRefreshingExplorer()) {
const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => {
handleOpenAction(openAction, this.nonSystemDatabases(), this);
subscription.dispose();
});
} else {
handleOpenAction(openAction, this.nonSystemDatabases(), this);
}
}
if (message.actionType === ActionContracts.ActionType.TransmitCachedData) {
handleCachedDataMessage(message);
return;
}
if (message.type) {
switch (message.type) {
case MessageTypes.UpdateLocationHash:
if (!message.locationHash) {
break;
}
hasher.replaceHash(message.locationHash);
RouteHandler.getInstance().parseHash(message.locationHash);
break;
case MessageTypes.SendNotification:
if (!message.message) {
break;
}
NotificationConsoleUtils.logConsoleMessage(
message.consoleDataType || ConsoleDataType.Info,
message.message,
message.id
);
break;
case MessageTypes.ClearNotification:
if (!message.id) {
break;
}
NotificationConsoleUtils.clearInProgressMessageWithId(message.id);
break;
case MessageTypes.LoadingStatus:
if (!message.text) {
break;
}
this._setLoadingStatusText(message.text, message.title);
break;
}
return;
}
this.splashScreenAdapter.forceRender();
}
public findSelectedDatabase(): ViewModels.Database { public findSelectedDatabase(): ViewModels.Database {
if (!this.selectedNode()) { if (!this.selectedNode()) {
return null; return null;
@@ -1874,7 +1441,7 @@ export default class Explorer {
} }
} }
public initDataExplorerWithFrameInputs(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.
// This allows webpack hot reload to funciton properly // This allows webpack hot reload to funciton properly
@@ -1889,19 +1456,20 @@ export default class Explorer {
this.collectionCreationDefaults = inputs.defaultCollectionThroughput; this.collectionCreationDefaults = inputs.defaultCollectionThroughput;
} }
this.features(inputs.features); this.features(inputs.features);
this.serverId(inputs.serverId); this.serverId(inputs.serverId ?? Constants.ServerIds.productionPortal);
this.databaseAccount(databaseAccount); this.databaseAccount(databaseAccount);
this.subscriptionType(inputs.subscriptionType); this.subscriptionType(inputs.subscriptionType ?? SharedConstants.CollectionCreation.DefaultSubscriptionType);
this.hasWriteAccess(inputs.hasWriteAccess); this.hasWriteAccess(inputs.hasWriteAccess ?? true);
this.flight(inputs.addCollectionDefaultFlight); if (inputs.addCollectionDefaultFlight) {
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription); this.flight(inputs.addCollectionDefaultFlight);
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken); }
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription ?? false);
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken ?? false);
this.setFeatureFlagsFromFlights(inputs.flights); this.setFeatureFlagsFromFlights(inputs.flights);
this.setSelfServeType(inputs); this.setSelfServeType(inputs);
this._importExplorerConfigComplete = true;
updateConfigContext({ updateConfigContext({
BACKEND_ENDPOINT: inputs.extensionEndpoint || "", BACKEND_ENDPOINT: inputs.extensionEndpoint || configContext.BACKEND_ENDPOINT,
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT), ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
}); });
@@ -2090,83 +1658,6 @@ export default class Explorer {
return deferred.promise; return deferred.promise;
} }
// TODO: Abstract this elsewhere
private _openShareDialog: () => void = (): void => {
if (!$("#shareDataAccessFlyout").dialog("instance")) {
const accountMetadataInfo = {
databaseAccountName: this.databaseAccount() && this.databaseAccount().name,
defaultExperience: this.defaultExperience && this.defaultExperience(),
dataExplorerArea: Constants.Areas.ShareDialog,
};
const openFullscreenButton = {
text: "Open",
class: "openFullScreenBtn openFullScreenCancelBtn",
click: () => {
TelemetryProcessor.trace(
Action.SelectItem,
ActionModifiers.Mark,
_.extend({}, { description: "Open full screen" }, accountMetadataInfo)
);
const hiddenAnchorElement: HTMLAnchorElement = document.createElement("a");
hiddenAnchorElement.href = this.shareAccessUrl();
hiddenAnchorElement.target = "_blank";
$("#shareDataAccessFlyout").dialog("close");
hiddenAnchorElement.click();
},
};
const cancelButton = {
text: "Cancel",
class: "shareCancelButton openFullScreenCancelBtn",
click: () => {
TelemetryProcessor.trace(
Action.SelectItem,
ActionModifiers.Mark,
_.extend({}, { description: "Cancel open full screen" }, accountMetadataInfo)
);
$("#shareDataAccessFlyout").dialog("close");
},
};
$("#shareDataAccessFlyout").dialog({
autoOpen: false,
buttons: [openFullscreenButton, cancelButton],
closeOnEscape: true,
draggable: false,
dialogClass: "no-close",
position: { my: "right top", at: "right bottom", of: $(".OpenFullScreen") },
resizable: false,
title: "Open Full Screen",
width: 400,
close: (event: Event, ui: JQueryUI.DialogUIParams) => this.shouldShowShareDialogContents(false),
});
$("#shareDataAccessFlyout").dialog("option", "classes", {
"ui-widget-content": "shareUrlDialog",
"ui-widget-header": "shareUrlTitle",
"ui-dialog-titlebar-close": "shareClose",
"ui-button": "shareCloseIcon",
"ui-button-icon": "cancelIcon",
"ui-icon": "",
});
$("#shareDataAccessFlyout").dialog("option", "open", (event: Event, ui: JQueryUI.DialogUIParams) =>
$(".openFullScreenBtn").focus()
);
}
$("#shareDataAccessFlyout").dialog("close");
this.shouldShowShareDialogContents(true);
$("#shareDataAccessFlyout").dialog("open");
};
private _getShareAccessUrlForToken(token: string): string {
if (!token) {
return undefined;
}
const urlPrefixWithKeyParam: string = `${configContext.hostedExplorerURL}?key=`;
const currentActiveTab = this.tabsManager.activeTab();
return `${urlPrefixWithKeyParam}${token}#/${(currentActiveTab && currentActiveTab.hashLocation()) || ""}`;
}
private _initSettings() { private _initSettings() {
if (!ExplorerSettings.hasSettingsDefined()) { if (!ExplorerSettings.hasSettingsDefined()) {
ExplorerSettings.createDefaultSettings(); ExplorerSettings.createDefaultSettings();
@@ -2298,14 +1789,9 @@ export default class Explorer {
return Promise.resolve(false); return Promise.resolve(false);
} }
public async publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): Promise<void> { public async publishNotebook(name: string, content: string | unknown, parentDomElement?: HTMLElement): Promise<void> {
if (this.notebookManager) { if (this.notebookManager) {
await this.notebookManager.openPublishNotebookPane( await this.notebookManager.openPublishNotebookPane(name, content, parentDomElement);
name,
content,
parentDomElement,
this.isLinkInjectionEnabled()
);
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter; this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
this.isPublishNotebookPaneEnabled(true); this.isPublishNotebookPaneEnabled(true);
} }
@@ -2320,7 +1806,7 @@ export default class Explorer {
} }
public showOkModalDialog(title: string, msg: string): void { public showOkModalDialog(title: string, msg: string): void {
this._dialogProps({ this.openDialog({
isModal: true, isModal: true,
visible: true, visible: true,
title, title,
@@ -2343,7 +1829,7 @@ export default class Explorer {
textFieldProps?: TextFieldProps, textFieldProps?: TextFieldProps,
isPrimaryButtonDisabled?: boolean isPrimaryButtonDisabled?: boolean
): void { ): void {
this._dialogProps({ this.openDialog({
isModal: true, isModal: true,
visible: true, visible: true,
title, title,
@@ -2546,7 +2032,7 @@ export default class Explorer {
} }
private async _refreshNotebooksEnabledStateForAccount(): Promise<void> { private async _refreshNotebooksEnabledStateForAccount(): Promise<void> {
const authType = window.authType as AuthType; const authType = userContext.authType;
if ( if (
authType === AuthType.EncryptedToken || authType === AuthType.EncryptedToken ||
authType === AuthType.ResourceToken || authType === AuthType.ResourceToken ||
@@ -2595,7 +2081,7 @@ export default class Explorer {
public _refreshSparkEnabledStateForAccount = async (): Promise<void> => { public _refreshSparkEnabledStateForAccount = async (): Promise<void> => {
const subscriptionId = userContext.subscriptionId; const subscriptionId = userContext.subscriptionId;
const armEndpoint = configContext.ARM_ENDPOINT; const armEndpoint = configContext.ARM_ENDPOINT;
const authType = window.authType as AuthType; const authType = userContext.authType;
if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) { if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
// explorer is not aware of the database account yet // explorer is not aware of the database account yet
this.isSparkEnabledForAccount(false); this.isSparkEnabledForAccount(false);
@@ -2624,7 +2110,7 @@ export default class Explorer {
public _isAfecFeatureRegistered = async (featureName: string): Promise<boolean> => { public _isAfecFeatureRegistered = async (featureName: string): Promise<boolean> => {
const subscriptionId = userContext.subscriptionId; const subscriptionId = userContext.subscriptionId;
const armEndpoint = configContext.ARM_ENDPOINT; const armEndpoint = configContext.ARM_ENDPOINT;
const authType = window.authType as AuthType; const authType = userContext.authType;
if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) { if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
// explorer is not aware of the database account yet // explorer is not aware of the database account yet
return false; return false;
@@ -2677,7 +2163,7 @@ export default class Explorer {
} }
if (item.type === NotebookContentItemType.Directory && item.children && item.children.length > 0) { if (item.type === NotebookContentItemType.Directory && item.children && item.children.length > 0) {
this._dialogProps({ this.openDialog({
isModal: true, isModal: true,
visible: true, visible: true,
title: "Unable to delete file", title: "Unable to delete file",
@@ -2859,10 +2345,36 @@ export default class Explorer {
} }
} }
public async openGallery(notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) { public async openGallery(
selectedTab?: GalleryTab,
notebookUrl?: string,
galleryItem?: IGalleryItem,
isFavorite?: boolean
) {
let title: string = "Gallery"; let title: string = "Gallery";
let hashLocation: string = "gallery"; let hashLocation: string = "gallery";
const galleryTabOptions: any = {
// GalleryTabOptions
account: userContext.databaseAccount,
container: this,
junoClient: this.notebookManager?.junoClient,
selectedTab: selectedTab || GalleryTab.OfficialSamples,
notebookUrl,
galleryItem,
isFavorite,
// TabOptions
tabKind: ViewModels.CollectionTabKind.Gallery,
title: title,
tabPath: title,
documentClientUtility: null,
isActive: ko.observable(false),
hashLocation: hashLocation,
onUpdateTabsButtons: this.onUpdateTabsButtons,
isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null,
};
const galleryTabs = this.tabsManager.getTabs( const galleryTabs = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.Gallery, ViewModels.CollectionTabKind.Gallery,
(tab) => tab.hashLocation() == hashLocation (tab) => tab.hashLocation() == hashLocation
@@ -2871,31 +2383,12 @@ export default class Explorer {
if (galleryTab) { if (galleryTab) {
this.tabsManager.activateTab(galleryTab); this.tabsManager.activateTab(galleryTab);
(galleryTab as any).reset(galleryTabOptions);
} else { } else {
if (!this.galleryTab) { if (!this.galleryTab) {
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab"); this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
} }
const newTab = new this.galleryTab.default(galleryTabOptions);
const newTab = new this.galleryTab.default({
// GalleryTabOptions
account: userContext.databaseAccount,
container: this,
junoClient: this.notebookManager?.junoClient,
notebookUrl,
galleryItem,
isFavorite,
// TabOptions
tabKind: ViewModels.CollectionTabKind.Gallery,
title: title,
tabPath: title,
documentClientUtility: null,
isActive: ko.observable(false),
hashLocation: hashLocation,
onUpdateTabsButtons: this.onUpdateTabsButtons,
isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null,
});
this.tabsManager.activateNewTab(newTab); this.tabsManager.activateNewTab(newTab);
} }
} }
@@ -3077,4 +2570,17 @@ export default class Explorer {
return false; return false;
}); });
} }
public openDeleteCollectionConfirmationPane(): void {
this.isFeatureEnabled(Constants.Features.enableKOPanel)
? this.deleteCollectionConfirmationPane.open()
: this.openSidePanel(
"Delete Collection",
<DeleteCollectionConfirmationPanel
explorer={this}
closePanel={() => this.closeSidePanel()}
openNotificationConsole={() => this.expandConsole()}
/>
);
}
} }

View File

@@ -7,7 +7,7 @@ import * as ko from "knockout";
import * as React from "react"; import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { CommandBarComponentButtonFactory } from "./CommandBarComponentButtonFactory"; import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar"; import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import { StyleConstants } from "../../../Common/Constants"; import { StyleConstants } from "../../../Common/Constants";
import * as CommandBarUtil from "./CommandBarUtil"; import * as CommandBarUtil from "./CommandBarUtil";

View File

@@ -1,5 +1,5 @@
import * as ko from "knockout"; import * as ko from "knockout";
import { CommandBarComponentButtonFactory } from "./CommandBarComponentButtonFactory"; import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import NotebookManager from "../../Notebook/NotebookManager"; import NotebookManager from "../../Notebook/NotebookManager";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
@@ -19,7 +19,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isNotebookEnabled = ko.observable(false); mockExplorer.isNotebookEnabled = ko.observable(false);
@@ -61,7 +60,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false); mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
@@ -125,7 +123,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false); mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
@@ -207,7 +204,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false); mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
@@ -295,7 +291,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false); mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = ko.observable(false); mockExplorer.isRunningOnNationalCloud = ko.observable(false);
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.notebookManager = new NotebookManager(); mockExplorer.notebookManager = new NotebookManager();
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined); mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false); mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);

View File

@@ -1,587 +0,0 @@
import * as ViewModels from "../../../Contracts/ViewModels";
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 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 SettingsIcon from "../../../../images/settings_15x15.svg";
import AddUdfIcon from "../../../../images/AddUdf.svg";
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
import ScaleIcon from "../../../../images/Scale_15x15.svg";
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import EnableNotebooksIcon from "../../../../images/notebook/Notebook-enable.svg";
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
import GitHubIcon from "../../../../images/github.svg";
import SynapseIcon from "../../../../images/synapse-link.svg";
import { configContext, Platform } from "../../../ConfigContext";
import Explorer from "../../Explorer";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
export class CommandBarComponentButtonFactory {
private static counter: number = 0;
public static createStaticCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
if (container.isAuthWithResourceToken()) {
return CommandBarComponentButtonFactory.createStaticCommandBarButtonsForResourceToken(container);
}
const newCollectionBtn = CommandBarComponentButtonFactory.createNewCollectionGroup(container);
const buttons: CommandButtonComponentProps[] = [newCollectionBtn];
const addSynapseLink = CommandBarComponentButtonFactory.createOpenSynapseLinkDialogButton(container);
if (addSynapseLink) {
buttons.push(CommandBarComponentButtonFactory.createDivider());
buttons.push(addSynapseLink);
}
if (!container.isPreferredApiTable()) {
newCollectionBtn.children = [CommandBarComponentButtonFactory.createNewCollectionGroup(container)];
const newDatabaseBtn = CommandBarComponentButtonFactory.createNewDatabase(container);
newCollectionBtn.children.push(newDatabaseBtn);
}
buttons.push(CommandBarComponentButtonFactory.createDivider());
if (container.isNotebookEnabled()) {
const newNotebookButton = CommandBarComponentButtonFactory.createNewNotebookButton(container);
newNotebookButton.children = [
CommandBarComponentButtonFactory.createNewNotebookButton(container),
CommandBarComponentButtonFactory.createuploadNotebookButton(container),
];
buttons.push(newNotebookButton);
if (container.notebookManager?.gitHubOAuthService) {
buttons.push(CommandBarComponentButtonFactory.createManageGitHubAccountButton(container));
}
}
if (!container.isRunningOnNationalCloud()) {
if (!container.isNotebookEnabled()) {
buttons.push(CommandBarComponentButtonFactory.createEnableNotebooksButton(container));
}
if (container.isPreferredApiMongoDB()) {
buttons.push(CommandBarComponentButtonFactory.createOpenMongoTerminalButton(container));
}
if (container.isPreferredApiCassandra()) {
buttons.push(CommandBarComponentButtonFactory.createOpenCassandraTerminalButton(container));
}
}
if (container.isNotebookEnabled()) {
buttons.push(CommandBarComponentButtonFactory.createOpenTerminalButton(container));
buttons.push(CommandBarComponentButtonFactory.createNotebookWorkspaceResetButton(container));
}
if (!container.isDatabaseNodeOrNoneSelected()) {
if (container.isNotebookEnabled()) {
buttons.push(CommandBarComponentButtonFactory.createDivider());
}
const isSqlQuerySupported = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
if (isSqlQuerySupported) {
const newSqlQueryBtn = CommandBarComponentButtonFactory.createNewSQLQueryButton(container);
buttons.push(newSqlQueryBtn);
}
const isSupportedOpenQueryApi =
container.isPreferredApiDocumentDB() || container.isPreferredApiMongoDB() || container.isPreferredApiGraph();
const isSupportedOpenQueryFromDiskApi = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
if (isSupportedOpenQueryApi && container.selectedNode() && container.findSelectedCollection()) {
const openQueryBtn = CommandBarComponentButtonFactory.createOpenQueryButton(container);
openQueryBtn.children = [
CommandBarComponentButtonFactory.createOpenQueryButton(container),
CommandBarComponentButtonFactory.createOpenQueryFromDiskButton(container),
];
buttons.push(openQueryBtn);
} else if (isSupportedOpenQueryFromDiskApi && container.selectedNode() && container.findSelectedCollection()) {
buttons.push(CommandBarComponentButtonFactory.createOpenQueryFromDiskButton(container));
}
if (CommandBarComponentButtonFactory.areScriptsSupported(container)) {
const label = "New Stored Procedure";
const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
};
newStoredProcedureBtn.children = CommandBarComponentButtonFactory.createScriptCommandButtons(container);
buttons.push(newStoredProcedureBtn);
}
}
return buttons;
}
public static createContextCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (!container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()) {
const label = "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && (<any>selectedCollection).onNewMongoShellClick();
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB(),
};
buttons.push(newMongoShellBtn);
}
return buttons;
}
public static createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (configContext.platform === Platform.Hosted) {
return buttons;
}
if (!container.isPreferredApiCassandra()) {
const label = "Settings";
const settingsPaneButton: CommandButtonComponentProps = {
iconSrc: SettingsIcon,
iconAlt: label,
onCommandClick: () => container.settingsPane.open(),
commandButtonLabel: null,
ariaLabel: label,
tooltipText: label,
hasPopup: true,
disabled: false,
};
buttons.push(settingsPaneButton);
}
if (container.isHostedDataExplorerEnabled()) {
const label = "Open Full Screen";
const fullScreenButton: CommandButtonComponentProps = {
iconSrc: OpenInTabIcon,
iconAlt: label,
onCommandClick: () => container.generateSharedAccessData(),
commandButtonLabel: null,
ariaLabel: label,
tooltipText: label,
hasPopup: false,
disabled: !container.isHostedDataExplorerEnabled(),
className: "OpenFullScreen",
};
buttons.push(fullScreenButton);
}
if (configContext.platform !== Platform.Emulator) {
const label = "Feedback";
const feedbackButtonOptions: CommandButtonComponentProps = {
iconSrc: FeedbackIcon,
iconAlt: label,
onCommandClick: () => container.provideFeedbackEmail(),
commandButtonLabel: null,
ariaLabel: label,
tooltipText: label,
hasPopup: false,
disabled: false,
};
buttons.push(feedbackButtonOptions);
}
return buttons;
}
public static createDivider(): CommandButtonComponentProps {
const label = `divider${CommandBarComponentButtonFactory.counter++}`;
return {
isDivider: true,
commandButtonLabel: label,
hasPopup: false,
iconSrc: null,
iconAlt: null,
onCommandClick: null,
ariaLabel: label,
};
}
private static areScriptsSupported(container: Explorer): boolean {
return container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
}
private static createNewCollectionGroup(container: Explorer): CommandButtonComponentProps {
const label = container.addCollectionText();
return {
iconSrc: AddCollectionIcon,
iconAlt: label,
onCommandClick: () => container.onNewCollectionClicked(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
id: "createNewContainerCommandButton",
};
}
private static createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
if (configContext.platform === Platform.Emulator) {
return null;
}
if (container.isServerlessEnabled()) {
return null;
}
if (
container.databaseAccount &&
container.databaseAccount() &&
container.databaseAccount().properties &&
container.databaseAccount().properties.enableAnalyticalStorage
) {
return null;
}
const capabilities =
(container.databaseAccount &&
container.databaseAccount() &&
container.databaseAccount().properties &&
container.databaseAccount().properties.capabilities) ||
[];
if (capabilities.some((capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics)) {
return null;
}
const label = "Enable Azure Synapse Link";
return {
iconSrc: SynapseIcon,
iconAlt: label,
onCommandClick: () => container.openEnableSynapseLinkDialog(),
commandButtonLabel: label,
hasPopup: false,
disabled: container.isSynapseLinkUpdating(),
ariaLabel: label,
};
}
private static createNewDatabase(container: Explorer): CommandButtonComponentProps {
const label = container.addDatabaseText();
return {
iconSrc: AddDatabaseIcon,
iconAlt: label,
onCommandClick: () => {
container.addDatabasePane.open();
document.getElementById("linkAddDatabase").focus();
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
};
}
private static createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps {
if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) {
const label = "New SQL Query";
return {
iconSrc: AddSqlQueryIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
};
} else if (container.isPreferredApiMongoDB()) {
const label = "New Query";
return {
iconSrc: AddSqlQueryIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && (<any>selectedCollection).onNewMongoQueryClick(selectedCollection, null);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
};
}
return null;
}
public static createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const shouldEnableScriptsCommands: boolean =
!container.isDatabaseNodeOrNoneSelected() && CommandBarComponentButtonFactory.areScriptsSupported(container);
if (shouldEnableScriptsCommands) {
const label = "New Stored Procedure";
const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newStoredProcedureBtn);
}
if (shouldEnableScriptsCommands) {
const label = "New UDF";
const newUserDefinedFunctionBtn: CommandButtonComponentProps = {
iconSrc: AddUdfIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, null);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newUserDefinedFunctionBtn);
}
if (shouldEnableScriptsCommands) {
const label = "New Trigger";
const newTriggerBtn: CommandButtonComponentProps = {
iconSrc: AddTriggerIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, null);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newTriggerBtn);
}
return buttons;
}
private static createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "New Notebook";
return {
iconSrc: NewNotebookIcon,
iconAlt: label,
onCommandClick: () => container.onNewNotebookClicked(),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
ariaLabel: label,
};
}
private static createuploadNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "Upload to Notebook Server";
return {
iconSrc: NewNotebookIcon,
iconAlt: label,
onCommandClick: () => container.onUploadToNotebookServerClicked(),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
ariaLabel: label,
};
}
private static createOpenQueryButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Query";
return {
iconSrc: BrowseQueriesIcon,
iconAlt: label,
onCommandClick: () => container.browseQueriesPane.open(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: false,
};
}
private static createOpenQueryFromDiskButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Query From Disk";
return {
iconSrc: OpenQueryFromDiskIcon,
iconAlt: label,
onCommandClick: () => container.loadQueryPane.open(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: false,
};
}
private static createEnableNotebooksButton(container: Explorer): CommandButtonComponentProps {
if (configContext.platform === Platform.Emulator) {
return null;
}
const label = "Enable Notebooks (Preview)";
const tooltip =
"Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const description =
"Looks like you have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
return {
iconSrc: EnableNotebooksIcon,
iconAlt: label,
onCommandClick: () => container.setupNotebooksPane.openWithTitleAndDescription(label, description),
commandButtonLabel: label,
hasPopup: false,
disabled: !container.isNotebooksEnabledForAccount(),
ariaLabel: label,
tooltipText: container.isNotebooksEnabledForAccount() ? "" : tooltip,
};
}
private static createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Terminal";
return {
iconSrc: CosmosTerminalIcon,
iconAlt: label,
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
ariaLabel: label,
};
}
private static createOpenMongoTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Mongo Shell";
const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const title = "Set up workspace";
const description =
"Looks like you have not created a workspace for this account. To proceed and start using features including mongo shell and notebook, we will need to create a default workspace in this account.";
const disableButton = !container.isNotebooksEnabledForAccount() && !container.isNotebookEnabled();
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (container.isNotebookEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
container.setupNotebooksPane.openWithTitleAndDescription(title, description);
}
},
commandButtonLabel: label,
hasPopup: false,
disabled: disableButton,
ariaLabel: label,
tooltipText: !disableButton ? "" : tooltip,
};
}
private static createOpenCassandraTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Cassandra Shell";
const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const title = "Set up workspace";
const description =
"Looks like you have not created a workspace for this account. To proceed and start using features including cassandra shell and notebook, we will need to create a default workspace in this account.";
const disableButton = !container.isNotebooksEnabledForAccount() && !container.isNotebookEnabled();
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (container.isNotebookEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
} else {
container.setupNotebooksPane.openWithTitleAndDescription(title, description);
}
},
commandButtonLabel: label,
hasPopup: false,
disabled: disableButton,
ariaLabel: label,
tooltipText: !disableButton ? "" : tooltip,
};
}
private static createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps {
const label = "Reset Workspace";
return {
iconSrc: ResetWorkspaceIcon,
iconAlt: label,
onCommandClick: () => container.resetNotebookWorkspace(),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
ariaLabel: label,
};
}
private static createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps {
let connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
return {
iconSrc: GitHubIcon,
iconAlt: label,
onCommandClick: () => {
if (!connectedToGitHub) {
TelemetryProcessor.trace(Action.NotebooksGitHubConnect, ActionModifiers.Mark, {
databaseAccountName: container.databaseAccount() && container.databaseAccount().name,
defaultExperience: container.defaultExperience && container.defaultExperience(),
dataExplorerArea: Areas.Notebook,
});
}
container.gitHubReposPane.open();
},
commandButtonLabel: label,
hasPopup: false,
disabled: false,
ariaLabel: label,
};
}
private static createStaticCommandBarButtonsForResourceToken(container: Explorer): CommandButtonComponentProps[] {
const newSqlQueryBtn = CommandBarComponentButtonFactory.createNewSQLQueryButton(container);
const openQueryBtn = CommandBarComponentButtonFactory.createOpenQueryButton(container);
newSqlQueryBtn.disabled = !container.isResourceTokenCollectionNodeSelected();
newSqlQueryBtn.onCommandClick = () => {
const resourceTokenCollection: ViewModels.CollectionBase = container.resourceTokenCollection();
resourceTokenCollection && resourceTokenCollection.onNewQueryClick(resourceTokenCollection, undefined);
};
openQueryBtn.disabled = !container.isResourceTokenCollectionNodeSelected();
if (!openQueryBtn.disabled) {
openQueryBtn.children = [
CommandBarComponentButtonFactory.createOpenQueryButton(container),
CommandBarComponentButtonFactory.createOpenQueryFromDiskButton(container),
];
}
return [newSqlQueryBtn, openQueryBtn];
}
}

View File

@@ -0,0 +1,581 @@
import * as ViewModels from "../../../Contracts/ViewModels";
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 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 SettingsIcon from "../../../../images/settings_15x15.svg";
import AddUdfIcon from "../../../../images/AddUdf.svg";
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import EnableNotebooksIcon from "../../../../images/notebook/Notebook-enable.svg";
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
import GitHubIcon from "../../../../images/github.svg";
import SynapseIcon from "../../../../images/synapse-link.svg";
import { configContext, Platform } from "../../../ConfigContext";
import Explorer from "../../Explorer";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import * as React from "react";
import { OpenFullScreen } from "../../OpenFullScreen";
let counter = 0;
export function createStaticCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
if (container.isAuthWithResourceToken()) {
return createStaticCommandBarButtonsForResourceToken(container);
}
const newCollectionBtn = createNewCollectionGroup(container);
const buttons: CommandButtonComponentProps[] = [];
buttons.push(newCollectionBtn);
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
if (addSynapseLink) {
buttons.push(createDivider());
buttons.push(addSynapseLink);
}
if (!container.isPreferredApiTable()) {
newCollectionBtn.children = [createNewCollectionGroup(container)];
const newDatabaseBtn = createNewDatabase(container);
newCollectionBtn.children.push(newDatabaseBtn);
}
buttons.push(createDivider());
if (container.isNotebookEnabled()) {
const newNotebookButton = createNewNotebookButton(container);
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
buttons.push(newNotebookButton);
if (container.notebookManager?.gitHubOAuthService) {
buttons.push(createManageGitHubAccountButton(container));
}
}
if (!container.isRunningOnNationalCloud()) {
if (!container.isNotebookEnabled()) {
buttons.push(createEnableNotebooksButton(container));
}
if (container.isPreferredApiMongoDB()) {
buttons.push(createOpenMongoTerminalButton(container));
}
if (container.isPreferredApiCassandra()) {
buttons.push(createOpenCassandraTerminalButton(container));
}
}
if (container.isNotebookEnabled()) {
buttons.push(createOpenTerminalButton(container));
buttons.push(createNotebookWorkspaceResetButton(container));
}
if (!container.isDatabaseNodeOrNoneSelected()) {
if (container.isNotebookEnabled()) {
buttons.push(createDivider());
}
const isSqlQuerySupported = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
if (isSqlQuerySupported) {
const newSqlQueryBtn = createNewSQLQueryButton(container);
buttons.push(newSqlQueryBtn);
}
const isSupportedOpenQueryApi =
container.isPreferredApiDocumentDB() || container.isPreferredApiMongoDB() || container.isPreferredApiGraph();
const isSupportedOpenQueryFromDiskApi = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
if (isSupportedOpenQueryApi && container.selectedNode() && container.findSelectedCollection()) {
const openQueryBtn = createOpenQueryButton(container);
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)];
buttons.push(openQueryBtn);
} else if (isSupportedOpenQueryFromDiskApi && container.selectedNode() && container.findSelectedCollection()) {
buttons.push(createOpenQueryFromDiskButton(container));
}
if (areScriptsSupported(container)) {
const label = "New Stored Procedure";
const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
};
newStoredProcedureBtn.children = createScriptCommandButtons(container);
buttons.push(newStoredProcedureBtn);
}
}
return buttons;
}
export function createContextCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (!container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()) {
const label = "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoShellClick();
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB(),
};
buttons.push(newMongoShellBtn);
}
return buttons;
}
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (configContext.platform === Platform.Hosted) {
return buttons;
}
if (!container.isPreferredApiCassandra()) {
const label = "Settings";
const settingsPaneButton: CommandButtonComponentProps = {
iconSrc: SettingsIcon,
iconAlt: label,
onCommandClick: () => container.settingsPane.open(),
commandButtonLabel: undefined,
ariaLabel: label,
tooltipText: label,
hasPopup: true,
disabled: false,
};
buttons.push(settingsPaneButton);
}
if (container.isHostedDataExplorerEnabled()) {
const label = "Open Full Screen";
const fullScreenButton: CommandButtonComponentProps = {
iconSrc: OpenInTabIcon,
iconAlt: label,
onCommandClick: () => {
container.openSidePanel("Open Full Screen", <OpenFullScreen />);
},
commandButtonLabel: undefined,
ariaLabel: label,
tooltipText: label,
hasPopup: false,
disabled: !container.isHostedDataExplorerEnabled(),
className: "OpenFullScreen",
};
buttons.push(fullScreenButton);
}
if (configContext.platform !== Platform.Emulator) {
const label = "Feedback";
const feedbackButtonOptions: CommandButtonComponentProps = {
iconSrc: FeedbackIcon,
iconAlt: label,
onCommandClick: () => container.provideFeedbackEmail(),
commandButtonLabel: undefined,
ariaLabel: label,
tooltipText: label,
hasPopup: false,
disabled: false,
};
buttons.push(feedbackButtonOptions);
}
return buttons;
}
export function createDivider(): CommandButtonComponentProps {
const label = `divider${counter++}`;
return {
isDivider: true,
commandButtonLabel: label,
hasPopup: false,
iconSrc: undefined,
iconAlt: undefined,
onCommandClick: undefined,
ariaLabel: label,
};
}
function areScriptsSupported(container: Explorer): boolean {
return container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
}
function createNewCollectionGroup(container: Explorer): CommandButtonComponentProps {
const label = container.addCollectionText();
return {
iconSrc: AddCollectionIcon,
iconAlt: label,
onCommandClick: () => container.onNewCollectionClicked(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
id: "createNewContainerCommandButton",
};
}
function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
if (configContext.platform === Platform.Emulator) {
return undefined;
}
if (container.isServerlessEnabled()) {
return undefined;
}
if (
container.databaseAccount &&
container.databaseAccount() &&
container.databaseAccount().properties &&
container.databaseAccount().properties.enableAnalyticalStorage
) {
return undefined;
}
const capabilities =
(container.databaseAccount &&
container.databaseAccount() &&
container.databaseAccount().properties &&
container.databaseAccount().properties.capabilities) ||
[];
if (capabilities.some((capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics)) {
return undefined;
}
const label = "Enable Azure Synapse Link";
return {
iconSrc: SynapseIcon,
iconAlt: label,
onCommandClick: () => container.openEnableSynapseLinkDialog(),
commandButtonLabel: label,
hasPopup: false,
disabled: container.isSynapseLinkUpdating(),
ariaLabel: label,
};
}
function createNewDatabase(container: Explorer): CommandButtonComponentProps {
const label = container.addDatabaseText();
return {
iconSrc: AddDatabaseIcon,
iconAlt: label,
onCommandClick: () => {
container.addDatabasePane.open();
document.getElementById("linkAddDatabase").focus();
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
};
}
function createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps {
if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) {
const label = "New SQL Query";
return {
iconSrc: AddSqlQueryIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
};
} else if (container.isPreferredApiMongoDB()) {
const label = "New Query";
return {
iconSrc: AddSqlQueryIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
};
}
return undefined;
}
export function createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const shouldEnableScriptsCommands: boolean =
!container.isDatabaseNodeOrNoneSelected() && areScriptsSupported(container);
if (shouldEnableScriptsCommands) {
const label = "New Stored Procedure";
const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newStoredProcedureBtn);
}
if (shouldEnableScriptsCommands) {
const label = "New UDF";
const newUserDefinedFunctionBtn: CommandButtonComponentProps = {
iconSrc: AddUdfIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newUserDefinedFunctionBtn);
}
if (shouldEnableScriptsCommands) {
const label = "New Trigger";
const newTriggerBtn: CommandButtonComponentProps = {
iconSrc: AddTriggerIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newTriggerBtn);
}
return buttons;
}
function createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "New Notebook";
return {
iconSrc: NewNotebookIcon,
iconAlt: label,
onCommandClick: () => container.onNewNotebookClicked(),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
ariaLabel: label,
};
}
function createuploadNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "Upload to Notebook Server";
return {
iconSrc: NewNotebookIcon,
iconAlt: label,
onCommandClick: () => container.onUploadToNotebookServerClicked(),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
ariaLabel: label,
};
}
function createOpenQueryButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Query";
return {
iconSrc: BrowseQueriesIcon,
iconAlt: label,
onCommandClick: () => container.browseQueriesPane.open(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: false,
};
}
function createOpenQueryFromDiskButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Query From Disk";
return {
iconSrc: OpenQueryFromDiskIcon,
iconAlt: label,
onCommandClick: () => container.loadQueryPane.open(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: false,
};
}
function createEnableNotebooksButton(container: Explorer): CommandButtonComponentProps {
if (configContext.platform === Platform.Emulator) {
return undefined;
}
const label = "Enable Notebooks (Preview)";
const tooltip =
"Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const description =
"Looks like you have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
return {
iconSrc: EnableNotebooksIcon,
iconAlt: label,
onCommandClick: () => container.setupNotebooksPane.openWithTitleAndDescription(label, description),
commandButtonLabel: label,
hasPopup: false,
disabled: !container.isNotebooksEnabledForAccount(),
ariaLabel: label,
tooltipText: container.isNotebooksEnabledForAccount() ? "" : tooltip,
};
}
function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Terminal";
return {
iconSrc: CosmosTerminalIcon,
iconAlt: label,
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
ariaLabel: label,
};
}
function createOpenMongoTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Mongo Shell";
const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const title = "Set up workspace";
const description =
"Looks like you have not created a workspace for this account. To proceed and start using features including mongo shell and notebook, we will need to create a default workspace in this account.";
const disableButton = !container.isNotebooksEnabledForAccount() && !container.isNotebookEnabled();
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (container.isNotebookEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
container.setupNotebooksPane.openWithTitleAndDescription(title, description);
}
},
commandButtonLabel: label,
hasPopup: false,
disabled: disableButton,
ariaLabel: label,
tooltipText: !disableButton ? "" : tooltip,
};
}
function createOpenCassandraTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Cassandra Shell";
const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const title = "Set up workspace";
const description =
"Looks like you have not created a workspace for this account. To proceed and start using features including cassandra shell and notebook, we will need to create a default workspace in this account.";
const disableButton = !container.isNotebooksEnabledForAccount() && !container.isNotebookEnabled();
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (container.isNotebookEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
} else {
container.setupNotebooksPane.openWithTitleAndDescription(title, description);
}
},
commandButtonLabel: label,
hasPopup: false,
disabled: disableButton,
ariaLabel: label,
tooltipText: !disableButton ? "" : tooltip,
};
}
function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps {
const label = "Reset Workspace";
return {
iconSrc: ResetWorkspaceIcon,
iconAlt: label,
onCommandClick: () => container.resetNotebookWorkspace(),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
ariaLabel: label,
};
}
function createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps {
const connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
return {
iconSrc: GitHubIcon,
iconAlt: label,
onCommandClick: () => {
if (!connectedToGitHub) {
TelemetryProcessor.trace(Action.NotebooksGitHubConnect, ActionModifiers.Mark, {
databaseAccountName: container.databaseAccount() && container.databaseAccount().name,
defaultExperience: container.defaultExperience && container.defaultExperience(),
dataExplorerArea: Areas.Notebook,
});
}
container.gitHubReposPane.open();
},
commandButtonLabel: label,
hasPopup: false,
disabled: false,
ariaLabel: label,
};
}
function createStaticCommandBarButtonsForResourceToken(container: Explorer): CommandButtonComponentProps[] {
const newSqlQueryBtn = createNewSQLQueryButton(container);
const openQueryBtn = createOpenQueryButton(container);
newSqlQueryBtn.disabled = !container.isResourceTokenCollectionNodeSelected();
newSqlQueryBtn.onCommandClick = () => {
const resourceTokenCollection: ViewModels.CollectionBase = container.resourceTokenCollection();
resourceTokenCollection && resourceTokenCollection.onNewQueryClick(resourceTokenCollection, undefined);
};
openQueryBtn.disabled = !container.isResourceTokenCollectionNodeSelected();
if (!openQueryBtn.disabled) {
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)];
}
return [newSqlQueryBtn, openQueryBtn];
}

View File

@@ -2,7 +2,6 @@ import React from "react";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import { import {
NotificationConsoleComponentProps, NotificationConsoleComponentProps,
ConsoleData,
NotificationConsoleComponent, NotificationConsoleComponent,
ConsoleDataType, ConsoleDataType,
} from "./NotificationConsoleComponent"; } from "./NotificationConsoleComponent";
@@ -10,38 +9,40 @@ import {
describe("NotificationConsoleComponent", () => { describe("NotificationConsoleComponent", () => {
const createBlankProps = (): NotificationConsoleComponentProps => { const createBlankProps = (): NotificationConsoleComponentProps => {
return { return {
consoleData: [], consoleData: undefined,
isConsoleExpanded: true, isConsoleExpanded: false,
onConsoleDataChange: (consoleData: ConsoleData[]) => {}, inProgressConsoleDataIdToBeDeleted: "",
onConsoleExpandedChange: (isExpanded: boolean) => {}, setIsConsoleExpanded: (isExpanded: boolean): void => {},
}; };
}; };
it("renders the console (expanded)", () => { it("renders the console", () => {
const props = createBlankProps(); const props = createBlankProps();
props.consoleData.push({ const wrapper = shallow(<NotificationConsoleComponent {...props} />);
expect(wrapper).toMatchSnapshot();
props.consoleData = {
type: ConsoleDataType.Info, type: ConsoleDataType.Info,
date: "date", date: "date",
message: "message", message: "message",
}); };
wrapper.setProps(props);
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("shows proper progress count", () => { it("shows proper progress count", () => {
const count = 100; const count = 100;
const props = createBlankProps(); const props = createBlankProps();
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
props.consoleData.push({ props.consoleData = {
type: ConsoleDataType.InProgress, type: ConsoleDataType.InProgress,
date: "date", date: "date" + i,
message: "message", message: "message",
}); };
wrapper.setProps(props);
} }
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual(count.toString()); expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual(count.toString());
expect(wrapper.find(".notificationConsoleHeader .numErroredItems").text()).toEqual("0"); expect(wrapper.find(".notificationConsoleHeader .numErroredItems").text()).toEqual("0");
expect(wrapper.find(".notificationConsoleHeader .numInfoItems").text()).toEqual("0"); expect(wrapper.find(".notificationConsoleHeader .numInfoItems").text()).toEqual("0");
@@ -50,16 +51,17 @@ describe("NotificationConsoleComponent", () => {
it("shows proper error count", () => { it("shows proper error count", () => {
const count = 100; const count = 100;
const props = createBlankProps(); const props = createBlankProps();
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
props.consoleData.push({ props.consoleData = {
type: ConsoleDataType.Error, type: ConsoleDataType.Error,
date: "date", date: "date" + i,
message: "message", message: "message",
}); };
wrapper.setProps(props);
} }
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("0"); expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("0");
expect(wrapper.find(".notificationConsoleHeader .numErroredItems").text()).toEqual(count.toString()); expect(wrapper.find(".notificationConsoleHeader .numErroredItems").text()).toEqual(count.toString());
expect(wrapper.find(".notificationConsoleHeader .numInfoItems").text()).toEqual("0"); expect(wrapper.find(".notificationConsoleHeader .numInfoItems").text()).toEqual("0");
@@ -68,31 +70,34 @@ describe("NotificationConsoleComponent", () => {
it("shows proper info count", () => { it("shows proper info count", () => {
const count = 100; const count = 100;
const props = createBlankProps(); const props = createBlankProps();
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
props.consoleData.push({ props.consoleData = {
type: ConsoleDataType.Info, type: ConsoleDataType.Info,
date: "date", date: "date" + i,
message: "message", message: "message",
}); };
wrapper.setProps(props);
} }
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("0"); expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("0");
expect(wrapper.find(".notificationConsoleHeader .numErroredItems").text()).toEqual("0"); expect(wrapper.find(".notificationConsoleHeader .numErroredItems").text()).toEqual("0");
expect(wrapper.find(".notificationConsoleHeader .numInfoItems").text()).toEqual(count.toString()); expect(wrapper.find(".notificationConsoleHeader .numInfoItems").text()).toEqual(count.toString());
}); });
const testRenderNotification = (date: string, msg: string, type: ConsoleDataType, iconClassName: string) => { const testRenderNotification = (date: string, message: string, type: ConsoleDataType, iconClassName: string) => {
const props = createBlankProps(); const props = createBlankProps();
props.consoleData.push({
date: date,
message: msg,
type: type,
});
const wrapper = shallow(<NotificationConsoleComponent {...props} />); const wrapper = shallow(<NotificationConsoleComponent {...props} />);
props.consoleData = {
type,
date,
message,
};
wrapper.setProps(props);
expect(wrapper.find(".notificationConsoleData .date").text()).toEqual(date); expect(wrapper.find(".notificationConsoleData .date").text()).toEqual(date);
expect(wrapper.find(".notificationConsoleData .message").text()).toEqual(msg); expect(wrapper.find(".notificationConsoleData .message").text()).toEqual(message);
expect(wrapper.exists(`.notificationConsoleData .${iconClassName}`)); expect(wrapper.exists(`.notificationConsoleData .${iconClassName}`));
}; };
@@ -110,55 +115,78 @@ describe("NotificationConsoleComponent", () => {
it("clears notifications", () => { it("clears notifications", () => {
const props = createBlankProps(); const props = createBlankProps();
props.consoleData.push({ const wrapper = shallow(<NotificationConsoleComponent {...props} />);
props.consoleData = {
type: ConsoleDataType.InProgress, type: ConsoleDataType.InProgress,
date: "date", date: "date",
message: "message1", message: "message1",
}); };
props.consoleData.push({ wrapper.setProps(props);
props.consoleData = {
type: ConsoleDataType.Error, type: ConsoleDataType.Error,
date: "date", date: "date",
message: "message2", message: "message2",
}); };
props.consoleData.push({ wrapper.setProps(props);
props.consoleData = {
type: ConsoleDataType.Info, type: ConsoleDataType.Info,
date: "date", date: "date",
message: "message3", message: "message3",
}); };
wrapper.setProps(props);
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
wrapper.find(".clearNotificationsButton").simulate("click"); wrapper.find(".clearNotificationsButton").simulate("click");
expect(!wrapper.exists(".notificationConsoleData")); expect(!wrapper.exists(".notificationConsoleData"));
}); });
it("collapses and hide content", () => { it("collapses and hide content", () => {
const props = createBlankProps(); const props = createBlankProps();
props.consoleData.push({ const wrapper = shallow(<NotificationConsoleComponent {...props} />);
props.consoleData = {
type: ConsoleDataType.Info,
date: "date", date: "date",
message: "message", message: "message",
type: ConsoleDataType.Info, };
});
props.isConsoleExpanded = true; props.isConsoleExpanded = true;
wrapper.setProps(props);
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
wrapper.find(".notificationConsoleHeader").simulate("click"); wrapper.find(".notificationConsoleHeader").simulate("click");
expect(!wrapper.exists(".notificationConsoleContent")); expect(!wrapper.exists(".notificationConsoleContent"));
}); });
it("display latest data in header", () => { it("display latest data in header", () => {
const latestData = "latest data"; const latestData = "latest data";
const props1 = createBlankProps(); const props = createBlankProps();
const props2 = createBlankProps(); const wrapper = shallow(<NotificationConsoleComponent {...props} />);
props2.consoleData.push({
props.consoleData = {
type: ConsoleDataType.Info,
date: "date", date: "date",
message: latestData, message: latestData,
type: ConsoleDataType.Info, };
}); props.isConsoleExpanded = true;
props2.isConsoleExpanded = true; wrapper.setProps(props);
const wrapper = shallow(<NotificationConsoleComponent {...props1} />);
wrapper.setProps(props2);
expect(wrapper.find(".headerStatusEllipsis").text()).toEqual(latestData); expect(wrapper.find(".headerStatusEllipsis").text()).toEqual(latestData);
}); });
it("delete in progress message", () => {
const props = createBlankProps();
props.consoleData = {
type: ConsoleDataType.InProgress,
date: "date",
message: "message",
id: "1",
};
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("1");
props.inProgressConsoleDataIdToBeDeleted = "1";
wrapper.setProps(props);
expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("0");
});
}); });

View File

@@ -37,15 +37,15 @@ export interface ConsoleData {
export interface NotificationConsoleComponentProps { export interface NotificationConsoleComponentProps {
isConsoleExpanded: boolean; isConsoleExpanded: boolean;
onConsoleExpandedChange: (isExpanded: boolean) => void; consoleData: ConsoleData;
consoleData: ConsoleData[]; inProgressConsoleDataIdToBeDeleted: string;
onConsoleDataChange: (consoleData: ConsoleData[]) => void; setIsConsoleExpanded: (isExpanded: boolean) => void;
} }
interface NotificationConsoleComponentState { interface NotificationConsoleComponentState {
headerStatus: string; headerStatus: string;
selectedFilter: string; selectedFilter: string;
isExpanded: boolean; allConsoleData: ConsoleData[];
} }
export class NotificationConsoleComponent extends React.Component< export class NotificationConsoleComponent extends React.Component<
@@ -60,28 +60,28 @@ export class NotificationConsoleComponent extends React.Component<
{ key: "Error", text: "Error" }, { key: "Error", text: "Error" },
]; ];
private headerTimeoutId?: number; private headerTimeoutId?: number;
private prevHeaderStatus: string | null; private prevHeaderStatus: string;
private consoleHeaderElement?: HTMLElement; private consoleHeaderElement?: HTMLElement;
constructor(props: NotificationConsoleComponentProps) { constructor(props: NotificationConsoleComponentProps) {
super(props); super(props);
this.state = { this.state = {
headerStatus: "", headerStatus: undefined,
selectedFilter: NotificationConsoleComponent.FilterOptions[0].key || "", selectedFilter: NotificationConsoleComponent.FilterOptions[0].key,
isExpanded: props.isConsoleExpanded, allConsoleData: props.consoleData ? [props.consoleData] : [],
}; };
this.prevHeaderStatus = null; this.prevHeaderStatus = undefined;
} }
public componentDidUpdate( public componentDidUpdate(
prevProps: NotificationConsoleComponentProps, prevProps: NotificationConsoleComponentProps,
prevState: NotificationConsoleComponentState prevState: NotificationConsoleComponentState
) { ) {
const currentHeaderStatus = NotificationConsoleComponent.extractHeaderStatus(this.props); const currentHeaderStatus = NotificationConsoleComponent.extractHeaderStatus(this.props.consoleData);
if ( if (
this.prevHeaderStatus !== currentHeaderStatus && this.prevHeaderStatus !== currentHeaderStatus &&
currentHeaderStatus !== null && currentHeaderStatus !== undefined &&
prevState.headerStatus !== currentHeaderStatus prevState.headerStatus !== currentHeaderStatus
) { ) {
this.setHeaderStatus(currentHeaderStatus); this.setHeaderStatus(currentHeaderStatus);
@@ -92,10 +92,8 @@ export class NotificationConsoleComponent extends React.Component<
// updates: currentHeaderStatus -> "" -> currentHeaderStatus -> "" etc. // updates: currentHeaderStatus -> "" -> currentHeaderStatus -> "" etc.
this.prevHeaderStatus = currentHeaderStatus; this.prevHeaderStatus = currentHeaderStatus;
if (prevProps.isConsoleExpanded !== this.props.isConsoleExpanded) { if (this.props.consoleData || this.props.inProgressConsoleDataIdToBeDeleted) {
// Sync state and props this.updateConsoleData(prevProps);
// TODO react anti-pattern: remove isExpanded from state which duplicates prop's isConsoleExpanded
this.setState({ isExpanded: this.props.isConsoleExpanded });
} }
} }
@@ -104,16 +102,19 @@ export class NotificationConsoleComponent extends React.Component<
}; };
public render(): JSX.Element { public render(): JSX.Element {
const numInProgress = this.props.consoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.InProgress) const numInProgress = this.state.allConsoleData.filter(
(data: ConsoleData) => data.type === ConsoleDataType.InProgress
).length;
const numErroredItems = this.state.allConsoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.Error)
.length; .length;
const numErroredItems = this.props.consoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.Error) const numInfoItems = this.state.allConsoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.Info)
.length;
const numInfoItems = this.props.consoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.Info)
.length; .length;
return ( return (
<div className="notificationConsoleContainer"> <div className="notificationConsoleContainer">
<div <div
className="notificationConsoleHeader" className="notificationConsoleHeader"
id="notificationConsoleHeader"
ref={this.setElememntRef} ref={this.setElememntRef}
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.expandCollapseConsole()} onClick={(event: React.MouseEvent<HTMLDivElement>) => this.expandCollapseConsole()}
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)} onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
@@ -143,18 +144,18 @@ export class NotificationConsoleComponent extends React.Component<
className="expandCollapseButton" className="expandCollapseButton"
role="button" role="button"
tabIndex={0} tabIndex={0}
aria-label={"console button" + (this.state.isExpanded ? " collapsed" : " expanded")} aria-label={"console button" + (this.props.isConsoleExpanded ? " collapsed" : " expanded")}
aria-expanded={!this.state.isExpanded} aria-expanded={!this.props.isConsoleExpanded}
> >
<img <img
src={this.state.isExpanded ? ChevronDownIcon : ChevronUpIcon} src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
alt={this.state.isExpanded ? "ChevronDownIcon" : "ChevronUpIcon"} alt={this.props.isConsoleExpanded ? "ChevronDownIcon" : "ChevronUpIcon"}
/> />
</div> </div>
</div> </div>
<AnimateHeight <AnimateHeight
duration={NotificationConsoleComponent.transitionDurationMs} duration={NotificationConsoleComponent.transitionDurationMs}
height={this.state.isExpanded ? "auto" : 0} height={this.props.isConsoleExpanded ? "auto" : 0}
onAnimationEnd={this.onConsoleWasExpanded} onAnimationEnd={this.onConsoleWasExpanded}
> >
<div className="notificationConsoleContents"> <div className="notificationConsoleContents">
@@ -189,7 +190,7 @@ export class NotificationConsoleComponent extends React.Component<
); );
} }
private expandCollapseConsole() { private expandCollapseConsole() {
this.setState({ isExpanded: !this.state.isExpanded }); this.props.setIsConsoleExpanded(!this.props.isConsoleExpanded);
} }
private onExpandCollapseKeyPress = (event: React.KeyboardEvent<HTMLDivElement>): void => { private onExpandCollapseKeyPress = (event: React.KeyboardEvent<HTMLDivElement>): void => {
@@ -209,7 +210,7 @@ export class NotificationConsoleComponent extends React.Component<
}; };
private clearNotifications(): void { private clearNotifications(): void {
this.props.onConsoleDataChange([]); this.setState({ allConsoleData: [] });
} }
private renderAllFilteredConsoleData(rowData: ConsoleData[]): JSX.Element[] { private renderAllFilteredConsoleData(rowData: ConsoleData[]): JSX.Element[] {
@@ -229,12 +230,9 @@ export class NotificationConsoleComponent extends React.Component<
}; };
private getFilteredConsoleData(): ConsoleData[] { private getFilteredConsoleData(): ConsoleData[] {
let filterType: ConsoleDataType | null = null; let filterType: ConsoleDataType;
switch (this.state.selectedFilter) { switch (this.state.selectedFilter) {
case "All":
filterType = null;
break;
case "In Progress": case "In Progress":
filterType = ConsoleDataType.InProgress; filterType = ConsoleDataType.InProgress;
break; break;
@@ -245,12 +243,12 @@ export class NotificationConsoleComponent extends React.Component<
filterType = ConsoleDataType.Error; filterType = ConsoleDataType.Error;
break; break;
default: default:
filterType = null; filterType = undefined;
} }
return filterType == null return filterType
? this.props.consoleData ? this.state.allConsoleData.filter((data: ConsoleData) => data.type === filterType)
: this.props.consoleData.filter((data: ConsoleData) => data.type === filterType); : this.state.allConsoleData;
} }
private setHeaderStatus(statusMessage: string): void { private setHeaderStatus(statusMessage: string): void {
@@ -266,18 +264,43 @@ export class NotificationConsoleComponent extends React.Component<
); );
} }
private static extractHeaderStatus(props: NotificationConsoleComponentProps) { private static extractHeaderStatus(consoleData: ConsoleData) {
if (props.consoleData && props.consoleData.length > 0) { return consoleData?.message.split(":\n")[0];
return props.consoleData[0].message.split(":\n")[0];
} else {
return null;
}
} }
private onConsoleWasExpanded = (): void => { private onConsoleWasExpanded = (): void => {
this.props.onConsoleExpandedChange(this.state.isExpanded); if (this.props.isConsoleExpanded && this.consoleHeaderElement) {
if (this.state.isExpanded && this.consoleHeaderElement) {
this.consoleHeaderElement.focus(); this.consoleHeaderElement.focus();
} }
}; };
private updateConsoleData = (prevProps: NotificationConsoleComponentProps): void => {
if (!this.areConsoleDataEqual(this.props.consoleData, prevProps.consoleData)) {
this.setState({ allConsoleData: [this.props.consoleData, ...this.state.allConsoleData] });
}
if (
this.props.inProgressConsoleDataIdToBeDeleted &&
prevProps.inProgressConsoleDataIdToBeDeleted !== this.props.inProgressConsoleDataIdToBeDeleted
) {
const allConsoleData = this.state.allConsoleData.filter(
(data: ConsoleData) =>
!(data.type === ConsoleDataType.InProgress && data.id === this.props.inProgressConsoleDataIdToBeDeleted)
);
this.setState({ allConsoleData });
}
};
private areConsoleDataEqual = (currentData: ConsoleData, prevData: ConsoleData): boolean => {
if (!currentData || !prevData) {
return !currentData && !prevData;
}
return (
currentData.date === prevData.date &&
currentData.message === prevData.message &&
currentData.type === prevData.type &&
currentData.id === prevData.id
);
};
} }

View File

@@ -1,47 +0,0 @@
import * as ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import * as ViewModels from "../../../Contracts/ViewModels";
import { NotificationConsoleComponent } from "./NotificationConsoleComponent";
import { ConsoleData } from "./NotificationConsoleComponent";
import Explorer from "../../Explorer";
export class NotificationConsoleComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
public container: Explorer;
private consoleData: ko.ObservableArray<ConsoleData>;
constructor(container: Explorer) {
this.container = container;
this.consoleData = container.notificationConsoleData;
this.consoleData.subscribe((newValue: ConsoleData[]) => this.triggerRender());
container.isNotificationConsoleExpanded.subscribe(() => this.triggerRender());
this.parameters = ko.observable(Date.now());
}
private onConsoleExpandedChange(isExpanded: boolean): void {
isExpanded ? this.container.expandConsole() : this.container.collapseConsole();
this.triggerRender();
}
private onConsoleDataChange(consoleData: ConsoleData[]): void {
this.consoleData(consoleData);
this.triggerRender();
}
public renderComponent(): JSX.Element {
return (
<NotificationConsoleComponent
isConsoleExpanded={this.container.isNotificationConsoleExpanded()}
onConsoleExpandedChange={this.onConsoleExpandedChange.bind(this)}
consoleData={this.consoleData()}
onConsoleDataChange={this.onConsoleDataChange.bind(this)}
/>
);
}
private triggerRender() {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
}

View File

@@ -1,11 +1,176 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NotificationConsoleComponent renders the console (expanded) 1`] = ` exports[`NotificationConsoleComponent renders the console 1`] = `
<div <div
className="notificationConsoleContainer" className="notificationConsoleContainer"
> >
<div <div
className="notificationConsoleHeader" className="notificationConsoleHeader"
id="notificationConsoleHeader"
onClick={[Function]}
onKeyDown={[Function]}
tabIndex={0}
>
<div
className="statusBar"
>
<span
className="dataTypeIcons"
>
<span
className="notificationConsoleHeaderIconWithData"
>
<img
alt="in progress items"
src=""
/>
<span
className="numInProgress"
>
0
</span>
</span>
<span
className="notificationConsoleHeaderIconWithData"
>
<img
alt="error items"
src=""
/>
<span
className="numErroredItems"
>
0
</span>
</span>
<span
className="notificationConsoleHeaderIconWithData"
>
<img
alt="info items"
src=""
/>
<span
className="numInfoItems"
>
0
</span>
</span>
</span>
<span
className="consoleSplitter"
/>
<span
className="headerStatus"
>
<span
className="headerStatusEllipsis"
/>
</span>
</div>
<div
aria-expanded={true}
aria-label="console button expanded"
className="expandCollapseButton"
role="button"
tabIndex={0}
>
<img
alt="ChevronUpIcon"
src=""
/>
</div>
</div>
<AnimateHeight
animateOpacity={false}
animationStateClasses={
Object {
"animating": "rah-animating",
"animatingDown": "rah-animating--down",
"animatingToHeightAuto": "rah-animating--to-height-auto",
"animatingToHeightSpecific": "rah-animating--to-height-specific",
"animatingToHeightZero": "rah-animating--to-height-zero",
"animatingUp": "rah-animating--up",
"static": "rah-static",
"staticHeightAuto": "rah-static--height-auto",
"staticHeightSpecific": "rah-static--height-specific",
"staticHeightZero": "rah-static--height-zero",
}
}
applyInlineTransitions={true}
delay={0}
duration={200}
easing="ease"
height={0}
onAnimationEnd={[Function]}
style={Object {}}
>
<div
className="notificationConsoleContents"
>
<div
className="notificationConsoleControls"
>
<StyledWithResponsiveMode
aria-label="All"
aria-labelledby="consoleFilterLabel"
label="Filter:"
onChange={[Function]}
options={
Array [
Object {
"key": "All",
"text": "All",
},
Object {
"key": "In Progress",
"text": "In progress",
},
Object {
"key": "Info",
"text": "Info",
},
Object {
"key": "Error",
"text": "Error",
},
]
}
role="combobox"
selectedKey="All"
/>
<span
className="consoleSplitter"
/>
<span
className="clearNotificationsButton"
onClick={[Function]}
onKeyDown={[Function]}
role="button"
tabIndex={0}
>
<img
alt="clear notifications image"
src=""
/>
Clear Notifications
</span>
</div>
<div
className="notificationConsoleData"
/>
</div>
</AnimateHeight>
</div>
`;
exports[`NotificationConsoleComponent renders the console 2`] = `
<div
className="notificationConsoleContainer"
>
<div
className="notificationConsoleHeader"
id="notificationConsoleHeader"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
tabIndex={0} tabIndex={0}
@@ -64,18 +229,20 @@ exports[`NotificationConsoleComponent renders the console (expanded) 1`] = `
> >
<span <span
className="headerStatusEllipsis" className="headerStatusEllipsis"
/> >
message
</span>
</span> </span>
</div> </div>
<div <div
aria-expanded={false} aria-expanded={true}
aria-label="console button collapsed" aria-label="console button expanded"
className="expandCollapseButton" className="expandCollapseButton"
role="button" role="button"
tabIndex={0} tabIndex={0}
> >
<img <img
alt="ChevronDownIcon" alt="ChevronUpIcon"
src="" src=""
/> />
</div> </div>
@@ -100,7 +267,7 @@ exports[`NotificationConsoleComponent renders the console (expanded) 1`] = `
delay={0} delay={0}
duration={200} duration={200}
easing="ease" easing="ease"
height="auto" height={0}
onAnimationEnd={[Function]} onAnimationEnd={[Function]}
style={Object {}} style={Object {}}
> >

View File

@@ -18,7 +18,6 @@ import { contents } from "rx-jupyter";
import { NotebookContainerClient } from "./NotebookContainerClient"; import { NotebookContainerClient } from "./NotebookContainerClient";
import { MemoryUsageInfo } from "../../Contracts/DataModels"; import { MemoryUsageInfo } from "../../Contracts/DataModels";
import { NotebookContentClient } from "./NotebookContentClient"; import { NotebookContentClient } from "./NotebookContentClient";
import { DialogProps } from "../Controls/DialogReactComponent/DialogComponent";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter"; import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
import { getFullName } from "../../Utils/UserUtils"; import { getFullName } from "../../Utils/UserUtils";
@@ -31,7 +30,6 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
export interface NotebookManagerOptions { export interface NotebookManagerOptions {
container: Explorer; container: Explorer;
notebookBasePath: ko.Observable<string>; notebookBasePath: ko.Observable<string>;
dialogProps: ko.Observable<DialogProps>;
resourceTree: ResourceTreeAdapter; resourceTree: ResourceTreeAdapter;
refreshCommandBarButtons: () => void; refreshCommandBarButtons: () => void;
refreshNotebookList: () => void; refreshNotebookList: () => void;
@@ -89,9 +87,7 @@ export default class NotebookManager {
this.notebookContentProvider this.notebookContentProvider
); );
if (this.params.container.isGalleryPublishEnabled()) { this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
}
this.copyNotebookPaneAdapter = new CopyNotebookPaneAdapter( this.copyNotebookPaneAdapter = new CopyNotebookPaneAdapter(
this.params.container, this.params.container,
@@ -127,10 +123,9 @@ export default class NotebookManager {
public async openPublishNotebookPane( public async openPublishNotebookPane(
name: string, name: string,
content: string | ImmutableNotebook, content: string | ImmutableNotebook,
parentDomElement: HTMLElement, parentDomElement: HTMLElement
isLinkInjectionEnabled: boolean
): Promise<void> { ): Promise<void> {
await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement, isLinkInjectionEnabled); await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement);
} }
public openCopyNotebookPane(name: string, content: string): void { public openCopyNotebookPane(name: string, content: string): void {
@@ -181,10 +176,8 @@ export default class NotebookManager {
multiline: true, multiline: true,
defaultValue: commitMsg, defaultValue: commitMsg,
rows: 3, rows: 3,
onChange: (_, newValue: string) => { onChange: (_: unknown, newValue: string) => {
commitMsg = newValue; commitMsg = newValue;
this.params.dialogProps().primaryButtonDisabled = !commitMsg;
this.params.dialogProps.valueHasMutated();
}, },
}, },
!commitMsg !commitMsg

View File

@@ -45,17 +45,18 @@ type NotebookRendererProps = NotebookRendererBaseProps & NotebookRendererDispatc
const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, children: React.ReactNode) => { const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, children: React.ReactNode) => {
const Cell = () => ( const Cell = () => (
<DraggableCell id={id} contentRef={contentRef}> // TODO Draggable and HijackScroll not working anymore. Fix or remove when reworking MarkdownCell.
<HijackScroll id={id} contentRef={contentRef}> // <DraggableCell id={id} contentRef={contentRef}>
<CellCreator id={id} contentRef={contentRef}> // <HijackScroll id={id} contentRef={contentRef}>
<CellLabeler id={id} contentRef={contentRef}> <CellCreator id={id} contentRef={contentRef}>
<HoverableCell id={id} contentRef={contentRef}> <CellLabeler id={id} contentRef={contentRef}>
{children} <HoverableCell id={id} contentRef={contentRef}>
</HoverableCell> {children}
</CellLabeler> </HoverableCell>
</CellCreator> </CellLabeler>
</HijackScroll> </CellCreator>
</DraggableCell> // </HijackScroll>
// </DraggableCell>
); );
Cell.defaultProps = { cell_type }; Cell.defaultProps = { cell_type };

View File

@@ -20,7 +20,6 @@ describe("OpenActions", () => {
explorer.cassandraAddCollectionPane = {} as CassandraAddCollectionPane; explorer.cassandraAddCollectionPane = {} as CassandraAddCollectionPane;
explorer.cassandraAddCollectionPane.open = jest.fn(); explorer.cassandraAddCollectionPane.open = jest.fn();
explorer.closeAllPanes = () => {}; explorer.closeAllPanes = () => {};
explorer.isConnectExplorerVisible = () => false;
database = { database = {
id: ko.observable("db"), id: ko.observable("db"),

View File

@@ -133,25 +133,19 @@ function openPane(action: ActionContracts.OpenPane, explorer: Explorer) {
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection] (<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection]
) { ) {
explorer.closeAllPanes(); explorer.closeAllPanes();
!explorer.isConnectExplorerVisible() && explorer.addCollectionPane.open(); explorer.addCollectionPane.open();
} else if ( } else if (
action.paneKind === ActionContracts.PaneKind.CassandraAddCollection || action.paneKind === ActionContracts.PaneKind.CassandraAddCollection ||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection] (<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection]
) { ) {
explorer.closeAllPanes(); explorer.closeAllPanes();
!explorer.isConnectExplorerVisible() && explorer.cassandraAddCollectionPane.open(); explorer.cassandraAddCollectionPane.open();
} else if ( } else if (
action.paneKind === ActionContracts.PaneKind.GlobalSettings || action.paneKind === ActionContracts.PaneKind.GlobalSettings ||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings] (<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings]
) { ) {
explorer.closeAllPanes(); explorer.closeAllPanes();
!explorer.isConnectExplorerVisible() && explorer.settingsPane.open(); explorer.settingsPane.open();
} else if (
action.paneKind === ActionContracts.PaneKind.AdHocAccess ||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AdHocAccess]
) {
explorer.closeAllPanes();
!explorer.isConnectExplorerVisible() && explorer.renewAdHocAccessPane.open();
} }
} }

View File

@@ -0,0 +1,17 @@
jest.mock("../hooks/useFullScreenURLs");
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import React from "react";
import { useFullScreenURLs } from "../hooks/useFullScreenURLs";
import { OpenFullScreen } from "./OpenFullScreen";
it("renders the correct URLs", () => {
(useFullScreenURLs as jest.Mock).mockReturnValue({
readWrite: "read and write url",
read: "read only url",
});
render(<OpenFullScreen />);
expect(screen.getByLabelText("Read and Write")).toHaveValue("https://cosmos.azure.com/?key=read and write url");
expect(screen.getByLabelText("Read Only")).toHaveValue("https://cosmos.azure.com/?key=read only url");
});

View File

@@ -0,0 +1,61 @@
import { Spinner, Stack, Text, TextField } from "office-ui-fabric-react";
import { DefaultButton, PrimaryButton } from "office-ui-fabric-react/lib/Button";
import * as React from "react";
import { useFullScreenURLs } from "../hooks/useFullScreenURLs";
import copyToClipboard from "clipboard-copy";
export const OpenFullScreen: React.FunctionComponent = () => {
const result = useFullScreenURLs();
if (!result) {
return <Spinner label="Generating URLs..." ariaLive="assertive" labelPosition="right" />;
}
const readWriteUrl = `https://cosmos.azure.com/?key=${result.readWrite}`;
const readUrl = `https://cosmos.azure.com/?key=${result.read}`;
return (
<>
<Stack tokens={{ childrenGap: 10 }}>
<Text>
Open this database account in a new browser tab with Cosmos DB Explorer. Or copy the read-write or read only
access urls below to share with others. For security purposes, the URLs grant time-bound access to the
account. When access expires, you can reconnect, using a valid connection string for the account.
</Text>
<TextField label="Read and Write" readOnly defaultValue={readWriteUrl} />
<Stack horizontal tokens={{ childrenGap: 10 }}>
<DefaultButton
onClick={() => {
copyToClipboard(readWriteUrl);
}}
text="Copy"
iconProps={{ iconName: "Copy" }}
/>
<PrimaryButton
onClick={() => {
window.open(readWriteUrl, "_blank");
}}
text="Open"
iconProps={{ iconName: "OpenInNewWindow" }}
/>
</Stack>
<TextField label="Read Only" readOnly defaultValue={readUrl} />
<Stack horizontal tokens={{ childrenGap: 10 }}>
<DefaultButton
onClick={() => {
copyToClipboard(readUrl);
}}
text="Copy"
iconProps={{ iconName: "Copy" }}
/>
<PrimaryButton
onClick={() => {
window.open(readUrl, "_blank");
}}
text="Open"
iconProps={{ iconName: "OpenInNewWindow" }}
/>
</Stack>
</Stack>
</>
);
};

View File

@@ -680,6 +680,10 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.formWarnings(""); this.formWarnings("");
this.databaseCreateNewShared(this.getSharedThroughputDefault()); this.databaseCreateNewShared(this.getSharedThroughputDefault());
this.shouldCreateMongoWildcardIndex(this.container.isMongoIndexingEnabled()); this.shouldCreateMongoWildcardIndex(this.container.isMongoIndexingEnabled());
if (!this.container.isServerlessEnabled()) {
this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
this.isSharedAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
}
if (this.isPreferredApiTable() && !databaseId) { if (this.isPreferredApiTable() && !databaseId) {
databaseId = SharedConstants.CollectionCreation.TablesAPIDefaultDatabase; databaseId = SharedConstants.CollectionCreation.TablesAPIDefaultDatabase;
} }
@@ -818,7 +822,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
let indexingPolicy: DataModels.IndexingPolicy; let indexingPolicy: DataModels.IndexingPolicy;
let createMongoWildcardIndex: boolean; let createMongoWildcardIndex: boolean;
// todo - remove mongo indexing policy ticket # 616274 // todo - remove mongo indexing policy ticket # 616274
if (this.container.isPreferredApiMongoDB()) { if (this.container.isPreferredApiMongoDB() && this.container.isEnableMongoCapabilityPresent()) {
createMongoWildcardIndex = this.shouldCreateMongoWildcardIndex(); createMongoWildcardIndex = this.shouldCreateMongoWildcardIndex();
} else if (this.showIndexingOptionsForSharedThroughput()) { } else if (this.showIndexingOptionsForSharedThroughput()) {
if (this.useIndexingForSharedThroughput()) { if (this.useIndexingForSharedThroughput()) {

View File

@@ -289,6 +289,9 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
public open() { public open() {
super.open(); super.open();
if (!this.container.isServerlessEnabled()) {
this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
}
const addCollectionPaneOpenMessage = { const addCollectionPaneOpenMessage = {
databaseAccountName: this.container.databaseAccount().name, databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(), defaultExperience: this.container.defaultExperience(),

View File

@@ -29,10 +29,6 @@ export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
this.title = ko.observable<string>(); this.title = ko.observable<string>();
this.formErrorsDetails = ko.observable<string>(); this.formErrorsDetails = ko.observable<string>();
this.isExecuting = ko.observable<boolean>(false); this.isExecuting = ko.observable<boolean>(false);
this.container.isNotificationConsoleExpanded.subscribe((isExpanded: boolean) => {
this.resizePane();
});
this.container.isNotificationConsoleExpanded.extend({ rateLimit: 10 });
} }
public cancel() { public cancel() {

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