Compare commits

..

70 Commits

Author SHA1 Message Date
Victor Meng
66a00c67ca Turn on feature flag 2023-06-19 17:50:19 -07:00
Victor Meng
8eccbd2806 Comment change 2023-06-19 17:49:48 -07:00
vchske
4617fa9364 Update throughput settings tab with new elasticity properties (#1461)
* Adding RU thermometer to settings throughput tab

* Finalizing RU thermometer on throughput settings

* Updated snapshot

* Fixing formatting

* Fixing lint errors

* Rerun prettier

* Fixing Offer properties

* Fixing Types

* Updating ARM clients, and enabling new elasticity properties

* Updating snapshots

* Updating an issue caused by updating ARM client

* Latest changes based on feedback

* Fixing lint and unit tests

* Minor fix

* Minor text change

* Changed some formatting
2023-06-16 15:54:29 -07:00
victor-meng
a282ad9242 integrate copilot UI with backend (#1478) 2023-06-16 00:25:23 -07:00
microsoft-github-policy-service[bot]
b954b14f56 Microsoft mandatory file (#1276)
Co-authored-by: microsoft-github-policy-service[bot] <77245923+microsoft-github-policy-service[bot]@users.noreply.github.com>
2023-06-08 18:32:42 -07:00
vchske
ed9c7dbb6b Text fix based on feedback (#1471) 2023-06-06 17:25:17 -07:00
victor-meng
aadbb50e7d Implement query copilot UI (#1452) 2023-06-06 11:43:53 -07:00
bogercraig
abff435e88 Hierarchical Partitioning - Expose container type and hashkey version for all containers. (#1458)
* Able to find needed data without RP calls.  Quick test blurb on settings page for SQL accounts.

* Added hierarchical partition message for SQL accounts.

* Rearranging import to match master and bypass auto formatting.

* Update snapshot tests with new partition hierarchy message.

* Updating formatting

---------

Co-authored-by: Craig Boger <craig.boger@microsoft.com>
2023-05-24 15:42:42 -04:00
sindhuba
d1c4059320 Fix gremlin timeout (#1457)
* Increase Gremlin request timeout from 6min to 1hr

* Increase query MAX_RESULT_SIZE limit from 10k to 100k.

* Run npm format

---------

Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com>
2023-05-23 13:49:40 -07:00
sunghyunkang1111
9b214a3171 Remove preview tag for hierarchical_partition_key (#1447)
* Remove preview tag for hierarchical_partition_key
2023-05-18 14:28:53 -05:00
MokireddySampath
ba66aa939b Defect 2280249 (#1432)
* autoscale and manual radiobuutton fixes

* alt text attribute for images

* Revert "alt text attribute for images"

This reverts commit 5a660551c6.

* alt text for decorative images

* sev2 accessibilitydefects in data explorer

* Commit to my local branch

* role has been added to graphic tag

* Update TableEntity.tsx

* Update ThroughputInput.tsx

* Update ThroughputInput.test.tsx.snap

* Update TreeComponent.tsx

* Update TableEntity.tsx

* Update AddCollectionPanel.tsx

* Update AddCollectionPanel.tsx

* Update AddCollectionPanel.tsx

* Update AddCollectionPanel.tsx

* Update AddCollectionPanel.tsx

* Update SplashScreen.tsx

* Update TreeComponent.test.tsx.snap

* Update TreeComponent.test.tsx.snap
2023-05-09 23:42:04 +05:30
MokireddySampath
f9af595eee 2276949test (#1433)
* autoscale and manual radiobuutton fixes

* alt text attribute for images

* Revert "alt text attribute for images"

This reverts commit 5a660551c6.

* alt text for decorative images

* sev2 accessibilitydefects in data explorer

* Commit to my local branch

* Autofocus removed

* Update TableEntity.tsx

* Update TableEntity.tsx

* Update TableEntity.tsx

* Update ThroughputInput.tsx

* Update ThroughputInput.test.tsx.snap

* Update TreeComponent.tsx

* Update TreeComponent.test.tsx.snap

* Update AddCollectionPanel.tsx

* Update SplashScreen.tsx
2023-05-09 23:33:05 +05:30
MokireddySampath
7ce8c7a5a1 anchor link removed, since interactive elements cannot be nested (#1434)
* anchor link removed, since interactive elements cannot be nested

* Update Tabs.tsx
2023-05-09 23:32:36 +05:30
MokireddySampath
9ad3f589cc aria required and asterisk has been added to indicate mandatory input for new vertex key input (#1435) 2023-05-09 23:32:09 +05:30
MokireddySampath
9267b2cc18 role has been corrected to combobox for the dropdown (#1438) 2023-05-09 23:29:56 +05:30
MokireddySampath
c7d1b2dcdb Defect2280542 (#1441)
* Changes made for the screenreader to readout the label and suffix associated with the input

* Added an empty href to restore the chactristics of link in high contrast mode

* Update SubSettingsComponent.tsx

* Update SubSettingsComponent.tsx

* Update SubSettingsComponent.tsx

* Update SubSettingsComponent.tsx

* Update SubSettingsComponent.test.tsx.snap

* Update SubSettingsComponent.test.tsx.snap

* Update SubSettingsComponent.test.tsx.snap
2023-05-09 23:29:25 +05:30
MokireddySampath
a526410e44 Defect 2280429 (#1416)
* autoscale and manual radiobuutton fixes

* alt text attribute for images

* Revert "alt text attribute for images"

This reverts commit 5a660551c6.

* alt text for decorative images

* sev2 accessibilitydefects in data explorer

* Revert "sev2 accessibilitydefects in data explorer"

This reverts commit b84d5b572c.

* Sev2 accessibilitydefects

* Revert "Sev2 accessibilitydefects"

This reverts commit a4e60f106c.

* accessibilitydefects-data explorer

* Accessibility sev-2 defects-2

* corrections for 2278347,2278096 and fix for 2264174

* corrections for 2278347,2278096 and fix for 2264174

* fix for defect 2276938

* wrong aria attibute used

* refresh, expand, collapse tree does not have proper label for screenreader

* wrong role is assigned to anchor link

* descriptive alt added for iconspresent for DATA and NOTEBOOK accordion headers

* Update CollapsedResourceTree.tsx

* Update ResourceTreeContainer.tsx

* Update ResourceTreeContainer.tsx

* Update TableEntity.tsx

* Update treeComponent.less

* Update MiddlePaneComponent.tsx

* Update PanelInfoErrorComponent.tsx
2023-04-25 21:04:29 +05:30
MokireddySampath
58cb85156c Defect 2278539 (#1415)
* autoscale and manual radiobuutton fixes

* alt text attribute for images

* Revert "alt text attribute for images"

This reverts commit 5a660551c6.

* alt text for decorative images

* sev2 accessibilitydefects in data explorer

* Revert "sev2 accessibilitydefects in data explorer"

This reverts commit b84d5b572c.

* Sev2 accessibilitydefects

* Revert "Sev2 accessibilitydefects"

This reverts commit a4e60f106c.

* accessibilitydefects-data explorer

* Accessibility sev-2 defects-2

* corrections for 2278347,2278096 and fix for 2264174

* corrections for 2278347,2278096 and fix for 2264174

* fix for defect 2276938

* wrong aria attibute used

* refresh, expand, collapse tree does not have proper label for screenreader

* wrong role is assigned to anchor link

* Update MiddlePaneComponent.tsx

* Update treeComponent.less

* Update CollapsedResourceTree.tsx

* Update ResourceTreeContainer.tsx

* Update TableEntity.tsx

* Update ResourceTreeContainer.tsx
2023-04-25 21:04:12 +05:30
MokireddySampath
dd5ccade2b Defect 2280418 (#1414)
* autoscale and manual radiobuutton fixes

* alt text attribute for images

* Revert "alt text attribute for images"

This reverts commit 5a660551c6.

* alt text for decorative images

* sev2 accessibilitydefects in data explorer

* Revert "sev2 accessibilitydefects in data explorer"

This reverts commit b84d5b572c.

* Sev2 accessibilitydefects

* Revert "Sev2 accessibilitydefects"

This reverts commit a4e60f106c.

* accessibilitydefects-data explorer

* Accessibility sev-2 defects-2

* corrections for 2278347,2278096 and fix for 2264174

* corrections for 2278347,2278096 and fix for 2264174

* fix for defect 2276938

* wrong aria attibute used

* refresh, expand, collapse tree does not have proper label for screenreader

* Update treeComponent.less

* Update TableEntity.tsx

* Update MiddlePaneComponent.tsx
2023-04-25 21:03:51 +05:30
MokireddySampath
38ebef6c02 Defect 2276941 (#1413)
* autoscale and manual radiobuutton fixes

* alt text attribute for images

* Revert "alt text attribute for images"

This reverts commit 5a660551c6.

* alt text for decorative images

* sev2 accessibilitydefects in data explorer

* Revert "sev2 accessibilitydefects in data explorer"

This reverts commit b84d5b572c.

* Sev2 accessibilitydefects

* Revert "Sev2 accessibilitydefects"

This reverts commit a4e60f106c.

* accessibilitydefects-data explorer

* Accessibility sev-2 defects-2

* corrections for 2278347,2278096 and fix for 2264174

* corrections for 2278347,2278096 and fix for 2264174

* fix for defect 2276938

* wrong aria attibute used

* aria-label for table max RU/s input element is not defined properly

* Update TableEntity.tsx

* Update treeComponent.less

* Update MiddlePaneComponent.tsx
2023-04-25 21:03:35 +05:30
MokireddySampath
7ad5862e8f Defect 2280272 (#1412)
* autoscale and manual radiobuutton fixes

* alt text attribute for images

* Revert "alt text attribute for images"

This reverts commit 5a660551c6.

* alt text for decorative images

* sev2 accessibilitydefects in data explorer

* Revert "sev2 accessibilitydefects in data explorer"

This reverts commit b84d5b572c.

* Sev2 accessibilitydefects

* Revert "Sev2 accessibilitydefects"

This reverts commit a4e60f106c.

* accessibilitydefects-data explorer

* Accessibility sev-2 defects-2

* corrections for 2278347,2278096 and fix for 2264174

* corrections for 2278347,2278096 and fix for 2264174

* fix for defect 2276938

* wrong aria attibute used

* Update treeComponent.less
2023-04-25 21:03:12 +05:30
sindhuba
755b732532 Remove Enable Azure synapse link for Cassandra (#1427) 2023-04-10 10:18:57 -07:00
sindhuba
1b5a9b83ff Remove Enable Azure Synapse link button for Tables API (#1425) 2023-04-07 08:47:23 -07:00
Armando Trejo Oliver
fb8871cfbf Load Legacy Mongo Shell V2 by default (#1424)
- Load Legacy Mongo Shell V2 by default
- Add/Keep feature flags to load from portal BE and V1
- Skip code coverage if skipCodeCoverage environment variable is set to "true"
2023-04-06 10:13:05 -07:00
Armando Trejo Oliver
874cec26fc Fix CORS issue using Mongo Shell from new origin (#1422)
* Fix CORS issue using Mongo Shell from new origin

* Add unit tests for getMongoShellOrigin and getMongoShellUrl

* Fix lint errors
2023-04-04 18:19:18 -07:00
Armando Trejo Oliver
9d2d0e4754 Add feature flags to test Legacy Mongo Shell (#1421)
Add feature flags to test Legacy Mongo Shell
2023-04-04 10:28:42 -07:00
MokireddySampath
c7c894d6d9 Sampath accessibility sev 2 2 (#1404)
* autoscale and manual radiobuutton fixes

* alt text attribute for images

* Revert "alt text attribute for images"

This reverts commit 5a660551c6.

* alt text for decorative images

* sev2 accessibilitydefects in data explorer

* Revert "sev2 accessibilitydefects in data explorer"

This reverts commit b84d5b572c.

* Sev2 accessibilitydefects

* Revert "Sev2 accessibilitydefects"

This reverts commit a4e60f106c.

* accessibilitydefects-data explorer

* Accessibility sev-2 defects-2

* corrections for 2278347,2278096 and fix for 2264174

* color for placeholder changed to 767474, margin is set to accommodate height between treeheader elements

* padding added for databaseheader, removed margin and restored padding to treenodeheader
2023-04-03 22:11:40 +05:30
Asier Isayas
547954c3dc Lazy loading containers (#1411)
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2023-03-30 14:53:36 -07:00
sindhuba
7f220bf8be Add additional teaching bubbles in Quickstart (#1407)
* Add additional teaching bubbles in Quickstart

* Run npm format

* Fix lint error

* Add unit tests

* Add Mongo teaching bubbles for Try CosmosDB and Launch full screen

* Add additional tests for UrlUtility

* Run npm format

* Add tests for Notebook Utils
2023-03-27 15:33:55 -07:00
jawelton74
1ee6abf890 Change AAD endpoint from /common to /organizations. (#1408) 2023-03-27 10:47:04 -07:00
MokireddySampath
72c3605dbe Sampath accessibility sev 2 (#1400)
* autoscale and manual radiobuutton fixes

* alt text attribute for images

* Revert "alt text attribute for images"

This reverts commit 5a660551c6.

* alt text for decorative images

* sev2 accessibilitydefects in data explorer

* Revert "sev2 accessibilitydefects in data explorer"

This reverts commit b84d5b572c.

* Sev2 accessibilitydefects

* Revert "Sev2 accessibilitydefects"

This reverts commit a4e60f106c.

* accessibilitydefects-data explorer

* Remove extra white space

---------

Co-authored-by: Victor Meng <vimeng@microsoft.com>
2023-03-14 23:49:21 +05:30
MokireddySampath
a7a38807df Defect2236159 (#1396)
* autoscale and manual radiobuutton fixes

* alt text attribute for images

* Revert "alt text attribute for images"

This reverts commit 5a660551c6.

* alt text for decorative images
2023-02-28 09:11:52 +05:30
sindhuba
1285ffc440 Refresh collection automatically when container is created using Quickstart pop up (#1394)
* Quickstart Refresh collection automatically when container is created

* Fix unit tests

* Fix unit tests and address comments

* Minor fixes in message handler

* Minor changes to fix tsconfig.strict.json

* Resolve npm compile:strict errors by fixing the code implementation

* Remove cache refresh code in configureHosted function

* Fix spacing

* Run npm format
2023-02-24 10:58:36 -08:00
jawelton74
b4bca3d41a Updates user to use for nuget push. (#1393) 2023-02-16 17:51:57 -08:00
jawelton74
2a47e4311c Set overwrite to true for both blob uploads to storage. (#1392) 2023-02-16 16:23:24 -08:00
bogercraig
49d9f489d8 Users/bogercraig/add cdb live show link (#1391)
* Exchanged links on DE home page with link to the Azure Cosmos DB Live TV show.

* Adding back accidentally removed terminating ,

* Added cdb TV to Postgres quick start splash page.

* Removing cdb tv description from next steps and moved up.

* Moved cdb tv to the tips and learn more column on postgres getting started page.

* Shortening postgrest cdb tv line.

* Removing link from PG since only 1 episode so far for PG.

* Adding terminating comma back.  Formatting.

* Consolidating Cosmos DB Live TV to a single variable.

* Updating prettier formatting.
2023-02-15 09:24:55 -05:00
sindhuba
31cc129aa7 Update address sample data for SQL and Mongo (#1389) 2023-02-07 18:34:10 -08:00
MokireddySampath
99af4acca4 autoscale and manual radiobuutton fixes (#1387) 2023-02-07 11:48:45 +05:30
victor-meng
80dd161a6f Remove networking warning message for sql, gremlin, and tables api (#1388) 2023-02-06 12:51:04 -08:00
MokireddySampath
850f1dfb97 rework for defect-1704149 (#1384) 2023-02-04 01:51:11 +05:30
sunghyunkang1111
a827e79317 remove feature flag and add preview text in the button (#1383) 2023-02-03 10:15:35 -06:00
MokireddySampath
7dbccff41d fix for defect 1703851&1703931 (#1379) 2023-02-01 22:20:03 +05:30
victor-meng
9184684e75 Improve network settings warning message (#1380) 2023-01-25 15:04:39 -08:00
sunghyunkang1111
701f486d8f Add hierachical partition key in add containers in SQL (#1377)
* Add hierachical partition key in add containers in SQL

* Add hierachical partition key in add containers in SQL

* fix unit test cases and update snapshot

* add learn more links and feature flag

* update snapsho

* separate subpartition key logic

* separate subpartition key logic
2023-01-23 09:09:29 -06:00
victor-meng
5059917edf Only show networking settings warning for Mongo and Cassandra accounts (#1376) 2023-01-18 11:12:10 -08:00
Asier Isayas
ab1409efb1 Show delete database error (#1373)
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2023-01-13 14:06:24 -05:00
MokireddySampath
5de9e682ba Defect1711833 (#1370)
* keyboard navigation for defects 1722611,1722618

* Fixes for keyboard navigation of add new clause,edit,remove property,insert filter line, remove filter line

* Revert "keyboard navigation for defects 1722611,1722618"

This reverts commit 9383609a22.

* html,css changes corected after reversion

* Revert "html,css changes corected after reversion"

This reverts commit 712e0e0c1e.

* committing changes for the keyboard navigation

* format fixes

* changes to addcollectionpanel.test.tsx snp file

* changes in infotooltip for defct

* Revert "changes in infotooltip for defct"

This reverts commit ca9833e208.

* commit for tooltip in defect 1704149

* Revert "commit for tooltip in defect 1704149"

This reverts commit 44766e8213.

* InfoTooltip changes

* update snapshot

* defect1722595 Bug 1722595: [Screen readers  Azure Cosmos DB  Scale& Settings: Screen reader (NVDA) is not announcing status message which is displayed on the screen after radio button is selected under scale tab.

* more options in delete entity dialog is not accessible through keyboard

* Revert "more options in delete entity dialog is not accessible through keyboard"

This reverts commit 23a05ef18e.

* more options in delete entity dialog is not accessible throgh keyboard

* remove native html with role='alert' for messagebar

* role added for messagebar fluentui component
2023-01-10 21:32:29 +05:30
jawelton74
b2ab979360 Display large partition key message for SQL API accounts only. (#1371) 2023-01-09 14:22:32 -08:00
vchske
2f32a676d0 Fixes issue when CRUD with no parition key for all APIs (#1362)
* Fixes issue where empty partition key is treated like nonexistent key for Tables API

* Updated format

* Refactor fix

* Prettier

* Fixes issue when CRUD with no parition key for all APIs

* Format fix

* Refactor to explicitly check for Tables
2022-12-13 11:36:39 -08:00
victor-meng
950c8ee470 Fix "Change network settings" button send the wrong message type (#1360) 2022-12-09 15:48:23 -08:00
vchske
b0eaac5b84 Fixes issue where empty partition key is treated like nonexistent key… (#1359) 2022-12-09 15:14:46 -08:00
victor-meng
952491a3ad Empty commit to trigger new build (#1351) 2022-11-14 13:46:27 -08:00
victor-meng
5dde66b032 Add check and guidance for networking settings (#1348) 2022-11-10 10:46:55 -08:00
jawelton74
5b1db2778c Fix typo in 'tutorial' in Quick start. (#1346) 2022-10-27 09:59:14 -07:00
Armando Trejo Oliver
1213788f9c Remove share-link feature (#1345)
* Remove share-link feature

Cosmos DB supports sharing access to an account data using AAD/RBAC now so this feature is unnecessary.

* Fix format issues

* Fix unit tests

* undo package changes
2022-10-20 10:58:37 -07:00
victor-meng
1ce3adff0f Hide launch quick start button and postgres teaching bubbles if account is a replica (#1344) 2022-10-19 17:12:41 -07:00
victor-meng
00eb07da11 Add retries when checking if account is allowed to access phoenix (#1342) 2022-10-19 17:12:24 -07:00
victor-meng
afe59c1589 Postgres fixes (#1341) 2022-10-11 16:03:58 -07:00
victor-meng
53b5ebd39c Add firewall notification in quickstart tab (#1337) 2022-10-10 19:30:52 -07:00
sunghyunkang1111
5b365e642f show introductory video and password reset for first time try postgresql (#1338) (#1340) 2022-10-10 18:53:54 -05:00
victor-meng
333b3de587 Add new message type for opening postgres networking blade (#1336) 2022-10-06 14:25:10 -07:00
victor-meng
e909ac43f4 Integrate PSQL shell in quick start guide (#1333) 2022-10-06 11:32:19 -07:00
vchske
8433a027ad Text changes for API rebranding (#1330)
* Text changes for API rebranding

* Text changes and bug fixes for API rebranding

* Format updates
2022-10-05 17:35:03 -07:00
victor-meng
81dfd76198 Implement connection string tab for postgres (#1334) 2022-10-05 17:32:05 -07:00
sunghyunkang1111
7c77ffda6c Add password reset callout (#1332)
* Add password reset callout

* Add create password text

* Add password reset callout
2022-10-04 11:50:47 -04:00
jawelton74
a34d3bb000 Disable wild card index for Mongo 16MB documents (#1331)
* Disable wild card index option if Mongo 16 MB document capability is
present.

* Formatted with prettier
2022-09-29 15:36:00 -07:00
victor-meng
42731d1aae Quickstart UI changes (#1327) 2022-09-27 14:03:28 -07:00
Armando Trejo Oliver
3abbb63adc Add support for psql shell (#1324)
Add support for psql shell
- add new terminal type
- handle missing documentEndpoint property
2022-09-22 15:39:35 -07:00
victor-meng
2e618cb3c4 Use my own ms account to upload nuget packages (#1325) 2022-09-21 16:10:07 -07:00
victor-meng
beca0d6608 Properly load a postgres account in data explorer (#1323) 2022-09-20 10:42:09 -07:00
169 changed files with 25255 additions and 3226 deletions

View File

@@ -92,11 +92,11 @@ jobs:
name: dist
path: dist/
- name: Upload build to preview blob storage
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name cosmosexplorerpreview --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --account-key="${PREVIEW_STORAGE_KEY}"
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name cosmosexplorerpreview --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
env:
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
- name: Upload preview config to blob storage
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}"
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
env:
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
endtoendemulator:
@@ -182,7 +182,7 @@ jobs:
with:
name: dist
- run: cp ./configs/prod.json config.json
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "GitHub" -Password "$AZURE_DEVOPS_PAT"
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
- uses: actions/upload-artifact@v2
@@ -207,7 +207,7 @@ jobs:
name: dist
- run: cp ./configs/mpac.json config.json
- run: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.MPAC/g' DataExplorer.nuspec
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "GitHub" -Password "$AZURE_DEVOPS_PAT"
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
- uses: actions/upload-artifact@v2

41
SECURITY.md Normal file
View File

@@ -0,0 +1,41 @@
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.7 BLOCK -->
## Security
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
## Reporting Security Issues
**Please do not report security vulnerabilities through public GitHub issues.**
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
* Full paths of source file(s) related to the manifestation of the issue
* The location of the affected source code (tag/branch/commit or direct URL)
* Any special configuration required to reproduce the issue
* Step-by-step instructions to reproduce the issue
* Proof-of-concept or exploit code (if possible)
* Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
## Preferred Languages
We prefer all communications to be in English.
## Policy
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
<!-- END MICROSOFT SECURITY.MD BLOCK -->

View File

@@ -1,5 +1,6 @@
<!doctype html>
<html class="default no-js">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
@@ -9,256 +10,292 @@
<link rel="stylesheet" href="../assets/css/main.css">
<script async src="../assets/js/search.js" id="search-script"></script>
</head>
<body>
<header>
<div class="tsd-page-toolbar">
<div class="container">
<div class="table-wrap">
<div class="table-cell" id="tsd-search" data-index="../assets/js/search.json" data-base="..">
<div class="field">
<label for="tsd-search-field" class="tsd-widget search no-caption">Search</label>
<input id="tsd-search-field" type="text" />
</div>
<ul class="results">
<li class="state loading">Preparing search index...</li>
<li class="state failure">The search index is not available</li>
</ul>
<a href="../index.html" class="title">cosmos-explorer</a>
</div>
<div class="table-cell" id="tsd-widgets">
<div id="tsd-filter">
<a href="#" class="tsd-widget options no-caption" data-toggle="options">Options</a>
<div class="tsd-filter-group">
<div class="tsd-select" id="tsd-filter-visibility">
<span class="tsd-select-label">All</span>
<ul class="tsd-select-list">
<li data-value="public">Public</li>
<li data-value="protected">Public/Protected</li>
<li data-value="private" class="selected">All</li>
</ul>
</div>
<input type="checkbox" id="tsd-filter-inherited" checked />
<label class="tsd-widget" for="tsd-filter-inherited">Inherited</label>
<input type="checkbox" id="tsd-filter-externals" checked />
<label class="tsd-widget" for="tsd-filter-externals">Externals</label>
<header>
<div class="tsd-page-toolbar">
<div class="container">
<div class="table-wrap">
<div class="table-cell" id="tsd-search" data-index="../assets/js/search.json" data-base="..">
<div class="field">
<label for="tsd-search-field" class="tsd-widget search no-caption">Search</label>
<input id="tsd-search-field" type="text" />
</div>
<ul class="results">
<li class="state loading">Preparing search index...</li>
<li class="state failure">The search index is not available</li>
</ul>
<a href="../index.html" class="title">cosmos-explorer</a>
</div>
<div class="table-cell" id="tsd-widgets">
<div id="tsd-filter">
<a href="#" class="tsd-widget options no-caption" data-toggle="options">Options</a>
<div class="tsd-filter-group">
<div class="tsd-select" id="tsd-filter-visibility">
<span class="tsd-select-label">All</span>
<ul class="tsd-select-list">
<li data-value="public">Public</li>
<li data-value="protected">Public/Protected</li>
<li data-value="private" class="selected">All</li>
</ul>
</div>
<input type="checkbox" id="tsd-filter-inherited" checked />
<label class="tsd-widget" for="tsd-filter-inherited">Inherited</label>
<input type="checkbox" id="tsd-filter-externals" checked />
<label class="tsd-widget" for="tsd-filter-externals">Externals</label>
</div>
</div>
<a href="#" class="tsd-widget menu no-caption" data-toggle="menu">Menu</a>
</div>
<a href="#" class="tsd-widget menu no-caption" data-toggle="menu">Menu</a>
</div>
</div>
</div>
</div>
<div class="tsd-page-title">
<div class="container">
<ul class="tsd-breadcrumb">
<li>
<a href="../modules.html">cosmos-explorer</a>
</li>
<li>
<a href="../modules/selfserve_selfserveutils.html">SelfServe/SelfServeUtils</a>
</li>
<li>
<a href="selfserve_selfserveutils.bladetype.html">BladeType</a>
</li>
</ul>
<h1>Enumeration BladeType</h1>
<div class="tsd-page-title">
<div class="container">
<ul class="tsd-breadcrumb">
<li>
<a href="../modules.html">cosmos-explorer</a>
</li>
<li>
<a href="../modules/selfserve_selfserveutils.html">SelfServe/SelfServeUtils</a>
</li>
<li>
<a href="selfserve_selfserveutils.bladetype.html">BladeType</a>
</li>
</ul>
<h1>Enumeration BladeType</h1>
</div>
</div>
</div>
</header>
<div class="container container-main">
<div class="row">
<div class="col-8 col-content">
<section class="tsd-panel tsd-comment">
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Portal Blade types</p>
</header>
<div class="container container-main">
<div class="row">
<div class="col-8 col-content">
<section class="tsd-panel tsd-comment">
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Portal Blade types</p>
</div>
</div>
</div>
</section>
<section class="tsd-panel-group tsd-index-group">
<h2>Index</h2>
<section class="tsd-panel tsd-index-panel">
<div class="tsd-index-content">
<section class="tsd-index-section ">
<h3>Enumeration members</h3>
<ul class="tsd-index-list">
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="selfserve_selfserveutils.bladetype.html#cassandrakeys" class="tsd-kind-icon">Cassandra<wbr>Keys</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="selfserve_selfserveutils.bladetype.html#gremlinkeys" class="tsd-kind-icon">Gremlin<wbr>Keys</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="selfserve_selfserveutils.bladetype.html#metrics" class="tsd-kind-icon">Metrics</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="selfserve_selfserveutils.bladetype.html#mongokeys" class="tsd-kind-icon">Mongo<wbr>Keys</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="selfserve_selfserveutils.bladetype.html#sqlkeys" class="tsd-kind-icon">Sql<wbr>Keys</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="selfserve_selfserveutils.bladetype.html#tablekeys" class="tsd-kind-icon">Table<wbr>Keys</a></li>
</section>
<section class="tsd-panel-group tsd-index-group">
<h2>Index</h2>
<section class="tsd-panel tsd-index-panel">
<div class="tsd-index-content">
<section class="tsd-index-section ">
<h3>Enumeration members</h3>
<ul class="tsd-index-list">
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a
href="selfserve_selfserveutils.bladetype.html#cassandrakeys"
class="tsd-kind-icon">Cassandra<wbr>Keys</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a
href="selfserve_selfserveutils.bladetype.html#gremlinkeys"
class="tsd-kind-icon">Gremlin<wbr>Keys</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a
href="selfserve_selfserveutils.bladetype.html#metrics"
class="tsd-kind-icon">Metrics</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a
href="selfserve_selfserveutils.bladetype.html#mongokeys"
class="tsd-kind-icon">Mongo<wbr>Keys</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a
href="selfserve_selfserveutils.bladetype.html#sqlkeys"
class="tsd-kind-icon">Sql<wbr>Keys</a></li>
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a
href="selfserve_selfserveutils.bladetype.html#tablekeys"
class="tsd-kind-icon">Table<wbr>Keys</a></li>
</ul>
</section>
</div>
</section>
</section>
<section class="tsd-panel-group tsd-member-group ">
<h2>Enumeration members</h2>
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
<a name="cassandrakeys" class="tsd-anchor"></a>
<h3>Cassandra<wbr>Keys</h3>
<div class="tsd-signature tsd-kind-icon">Cassandra<wbr>Keys<span
class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> =
&quot;cassandraDbKeys&quot;</span></div>
<aside class="tsd-sources">
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Keys blade of a Azure Cosmos DB for Apache Cassandra account.</p>
</div>
</div>
</section>
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
<a name="gremlinkeys" class="tsd-anchor"></a>
<h3>Gremlin<wbr>Keys</h3>
<div class="tsd-signature tsd-kind-icon">Gremlin<wbr>Keys<span
class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> =
&quot;keys&quot;</span></div>
<aside class="tsd-sources">
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Keys blade of a Azure Cosmos DB for Apache Gremlin account.</p>
</div>
</div>
</section>
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
<a name="metrics" class="tsd-anchor"></a>
<h3>Metrics</h3>
<div class="tsd-signature tsd-kind-icon">Metrics<span class="tsd-signature-symbol">:</span>
<span class="tsd-signature-symbol"> = &quot;metrics&quot;</span></div>
<aside class="tsd-sources">
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Metrics blade of an Azure Cosmos DB account.</p>
</div>
</div>
</section>
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
<a name="mongokeys" class="tsd-anchor"></a>
<h3>Mongo<wbr>Keys</h3>
<div class="tsd-signature tsd-kind-icon">Mongo<wbr>Keys<span
class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> =
&quot;mongoDbKeys&quot;</span></div>
<aside class="tsd-sources">
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Keys blade of a Azure Cosmos DB for MongoDB account.</p>
</div>
</div>
</section>
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
<a name="sqlkeys" class="tsd-anchor"></a>
<h3>Sql<wbr>Keys</h3>
<div class="tsd-signature tsd-kind-icon">Sql<wbr>Keys<span class="tsd-signature-symbol">:</span>
<span class="tsd-signature-symbol"> = &quot;keys&quot;</span></div>
<aside class="tsd-sources">
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Keys blade of a Azure Cosmos DB for NoSQL account.</p>
</div>
</div>
</section>
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
<a name="tablekeys" class="tsd-anchor"></a>
<h3>Table<wbr>Keys</h3>
<div class="tsd-signature tsd-kind-icon">Table<wbr>Keys<span
class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> =
&quot;tableKeys&quot;</span></div>
<aside class="tsd-sources">
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Keys blade of a Azure Cosmos DB for Table account.</p>
</div>
</div>
</section>
</section>
</div>
<div class="col-4 col-menu menu-sticky-wrap menu-highlight">
<nav class="tsd-navigation primary">
<ul>
<li class=" ">
<a href="../modules.html">Modules</a>
</li>
<li class=" tsd-kind-module">
<a href="../modules/selfserve.html">Self<wbr>Serve</a>
</li>
<li class=" tsd-kind-module">
<a href="../modules/selfserve___what_is_currently_supported_.html">Self<wbr>Serve -<wbr>
<wbr>What is currently supported?</a>
</li>
<li class=" tsd-kind-module">
<a href="../modules/selfserve_decorators.html">Self<wbr>Serve/<wbr>Decorators</a>
</li>
<li class=" tsd-kind-module">
<a
href="../modules/selfserve_selfservetelemetryprocessor.html">Self<wbr>Serve/<wbr>Self<wbr>Serve<wbr>Telemetry<wbr>Processor</a>
</li>
<li class=" tsd-kind-module">
<a
href="../modules/selfserve_selfservetypes.html">Self<wbr>Serve/<wbr>Self<wbr>Serve<wbr>Types</a>
</li>
<li class="current tsd-kind-module">
<a
href="../modules/selfserve_selfserveutils.html">Self<wbr>Serve/<wbr>Self<wbr>Serve<wbr>Utils</a>
</li>
</ul>
</nav>
<nav class="tsd-navigation secondary menu-sticky">
<ul class="before-current">
</ul>
<ul class="current">
<li class="current tsd-kind-enum tsd-parent-kind-module">
<a href="selfserve_selfserveutils.bladetype.html" class="tsd-kind-icon">Blade<wbr>Type</a>
<ul>
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
<a href="selfserve_selfserveutils.bladetype.html#cassandrakeys"
class="tsd-kind-icon">Cassandra<wbr>Keys</a>
</li>
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
<a href="selfserve_selfserveutils.bladetype.html#gremlinkeys"
class="tsd-kind-icon">Gremlin<wbr>Keys</a>
</li>
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
<a href="selfserve_selfserveutils.bladetype.html#metrics"
class="tsd-kind-icon">Metrics</a>
</li>
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
<a href="selfserve_selfserveutils.bladetype.html#mongokeys"
class="tsd-kind-icon">Mongo<wbr>Keys</a>
</li>
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
<a href="selfserve_selfserveutils.bladetype.html#sqlkeys"
class="tsd-kind-icon">Sql<wbr>Keys</a>
</li>
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
<a href="selfserve_selfserveutils.bladetype.html#tablekeys"
class="tsd-kind-icon">Table<wbr>Keys</a>
</li>
</ul>
</section>
</div>
</section>
</section>
<section class="tsd-panel-group tsd-member-group ">
<h2>Enumeration members</h2>
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
<a name="cassandrakeys" class="tsd-anchor"></a>
<h3>Cassandra<wbr>Keys</h3>
<div class="tsd-signature tsd-kind-icon">Cassandra<wbr>Keys<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> = &quot;cassandraDbKeys&quot;</span></div>
<aside class="tsd-sources">
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Keys blade of a Cassandra API account.</p>
</div>
</div>
</section>
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
<a name="gremlinkeys" class="tsd-anchor"></a>
<h3>Gremlin<wbr>Keys</h3>
<div class="tsd-signature tsd-kind-icon">Gremlin<wbr>Keys<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> = &quot;keys&quot;</span></div>
<aside class="tsd-sources">
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Keys blade of a Gremlin API account.</p>
</div>
</div>
</section>
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
<a name="metrics" class="tsd-anchor"></a>
<h3>Metrics</h3>
<div class="tsd-signature tsd-kind-icon">Metrics<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> = &quot;metrics&quot;</span></div>
<aside class="tsd-sources">
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Metrics blade of an Azure Cosmos DB account.</p>
</div>
</div>
</section>
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
<a name="mongokeys" class="tsd-anchor"></a>
<h3>Mongo<wbr>Keys</h3>
<div class="tsd-signature tsd-kind-icon">Mongo<wbr>Keys<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> = &quot;mongoDbKeys&quot;</span></div>
<aside class="tsd-sources">
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Keys blade of a Mongo API account.</p>
</div>
</div>
</section>
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
<a name="sqlkeys" class="tsd-anchor"></a>
<h3>Sql<wbr>Keys</h3>
<div class="tsd-signature tsd-kind-icon">Sql<wbr>Keys<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> = &quot;keys&quot;</span></div>
<aside class="tsd-sources">
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Keys blade of a SQL API account.</p>
</div>
</div>
</section>
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
<a name="tablekeys" class="tsd-anchor"></a>
<h3>Table<wbr>Keys</h3>
<div class="tsd-signature tsd-kind-icon">Table<wbr>Keys<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> = &quot;tableKeys&quot;</span></div>
<aside class="tsd-sources">
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Keys blade of a Table API account.</p>
</div>
</div>
</section>
</section>
</div>
<div class="col-4 col-menu menu-sticky-wrap menu-highlight">
<nav class="tsd-navigation primary">
<ul>
<li class=" ">
<a href="../modules.html">Modules</a>
</li>
<li class=" tsd-kind-module">
<a href="../modules/selfserve.html">Self<wbr>Serve</a>
</li>
<li class=" tsd-kind-module">
<a href="../modules/selfserve___what_is_currently_supported_.html">Self<wbr>Serve -<wbr> <wbr>What is currently supported?</a>
</li>
<li class=" tsd-kind-module">
<a href="../modules/selfserve_decorators.html">Self<wbr>Serve/<wbr>Decorators</a>
</li>
<li class=" tsd-kind-module">
<a href="../modules/selfserve_selfservetelemetryprocessor.html">Self<wbr>Serve/<wbr>Self<wbr>Serve<wbr>Telemetry<wbr>Processor</a>
</li>
<li class=" tsd-kind-module">
<a href="../modules/selfserve_selfservetypes.html">Self<wbr>Serve/<wbr>Self<wbr>Serve<wbr>Types</a>
</li>
<li class="current tsd-kind-module">
<a href="../modules/selfserve_selfserveutils.html">Self<wbr>Serve/<wbr>Self<wbr>Serve<wbr>Utils</a>
</li>
</ul>
</nav>
<nav class="tsd-navigation secondary menu-sticky">
<ul class="before-current">
</ul>
<ul class="current">
<li class="current tsd-kind-enum tsd-parent-kind-module">
<a href="selfserve_selfserveutils.bladetype.html" class="tsd-kind-icon">Blade<wbr>Type</a>
<ul>
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
<a href="selfserve_selfserveutils.bladetype.html#cassandrakeys" class="tsd-kind-icon">Cassandra<wbr>Keys</a>
</li>
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
<a href="selfserve_selfserveutils.bladetype.html#gremlinkeys" class="tsd-kind-icon">Gremlin<wbr>Keys</a>
</li>
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
<a href="selfserve_selfserveutils.bladetype.html#metrics" class="tsd-kind-icon">Metrics</a>
</li>
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
<a href="selfserve_selfserveutils.bladetype.html#mongokeys" class="tsd-kind-icon">Mongo<wbr>Keys</a>
</li>
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
<a href="selfserve_selfserveutils.bladetype.html#sqlkeys" class="tsd-kind-icon">Sql<wbr>Keys</a>
</li>
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
<a href="selfserve_selfserveutils.bladetype.html#tablekeys" class="tsd-kind-icon">Table<wbr>Keys</a>
</li>
</ul>
</li>
</ul>
<ul class="after-current">
<li class=" tsd-kind-enum tsd-parent-kind-module">
<a href="selfserve_selfserveutils.selfservetype.html" class="tsd-kind-icon">Self<wbr>Serve<wbr>Type</a>
</li>
<li class=" tsd-kind-function tsd-parent-kind-module">
<a href="../modules/selfserve_selfserveutils.html#generatebladelink" class="tsd-kind-icon">generate<wbr>Blade<wbr>Link</a>
</li>
</ul>
</nav>
</li>
</ul>
<ul class="after-current">
<li class=" tsd-kind-enum tsd-parent-kind-module">
<a href="selfserve_selfserveutils.selfservetype.html"
class="tsd-kind-icon">Self<wbr>Serve<wbr>Type</a>
</li>
<li class=" tsd-kind-function tsd-parent-kind-module">
<a href="../modules/selfserve_selfserveutils.html#generatebladelink"
class="tsd-kind-icon">generate<wbr>Blade<wbr>Link</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
<footer class="with-border-bottom">
<div class="container">
<h2>Legend</h2>
<div class="tsd-legend-group">
<ul class="tsd-legend">
<li class="tsd-kind-function"><span class="tsd-kind-icon">Function</span></li>
<li class="tsd-kind-type-alias"><span class="tsd-kind-icon">Type alias</span></li>
</ul>
<ul class="tsd-legend">
<li class="tsd-kind-enum"><span class="tsd-kind-icon">Enumeration</span></li>
</ul>
<ul class="tsd-legend">
<li class="tsd-kind-interface"><span class="tsd-kind-icon">Interface</span></li>
</ul>
<ul class="tsd-legend">
<li class="tsd-kind-class"><span class="tsd-kind-icon">Class</span></li>
</ul>
<footer class="with-border-bottom">
<div class="container">
<h2>Legend</h2>
<div class="tsd-legend-group">
<ul class="tsd-legend">
<li class="tsd-kind-function"><span class="tsd-kind-icon">Function</span></li>
<li class="tsd-kind-type-alias"><span class="tsd-kind-icon">Type alias</span></li>
</ul>
<ul class="tsd-legend">
<li class="tsd-kind-enum"><span class="tsd-kind-icon">Enumeration</span></li>
</ul>
<ul class="tsd-legend">
<li class="tsd-kind-interface"><span class="tsd-kind-icon">Interface</span></li>
</ul>
<ul class="tsd-legend">
<li class="tsd-kind-class"><span class="tsd-kind-icon">Class</span></li>
</ul>
</div>
</div>
</footer>
<div class="container tsd-generator">
<p>Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p>
</div>
</footer>
<div class="container tsd-generator">
<p>Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p>
</div>
<div class="overlay"></div>
<script src="../assets/js/main.js"></script>
<div class="overlay"></div>
<script src="../assets/js/main.js"></script>
</body>
</html>

35
images/Copilot.svg Normal file
View File

@@ -0,0 +1,35 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" fill="white"/>
<path d="M17 3.73926L20 4.04436V12.6862L15.2 13.8013L13.6 15.9967V19.4479C13.6 21.7669 14.8545 23.9045 16.879 25.0353L21.4036 27.5626L13.6 31.69L10.3559 32.1523L7.27902 30.4337C5.25445 29.3028 4 27.1652 4 24.8462V14.7591C4 12.4395 5.25512 10.3015 7.28055 9.17085L16.3174 4.12639L16.3121 4.13012L17 3.73926Z" fill="url(#paint0_radial_420_101763)"/>
<path d="M17 3.73926L20 4.04436V12.6862L15.2 13.8013L13.6 15.9967V19.4479C13.6 21.7669 14.8545 23.9045 16.879 25.0353L21.4036 27.5626L13.6 31.69L10.3559 32.1523L7.27902 30.4337C5.25445 29.3028 4 27.1652 4 24.8462V14.7591C4 12.4395 5.25512 10.3015 7.28055 9.17085L16.3174 4.12639L16.3121 4.13012L17 3.73926Z" fill="url(#paint1_linear_420_101763)"/>
<path d="M26.399 15.001L34.399 19.801L35.999 21.401V24.8452C35.999 27.1642 34.7446 29.3018 32.72 30.4327L23.12 35.7949C21.1804 36.8784 18.8177 36.8784 16.878 35.7949L7.27804 30.4327C7.09146 30.3284 6.91141 30.2157 6.73828 30.095L7.278 30.3965C9.21762 31.4799 11.5803 31.4799 13.52 30.3965L23.12 25.0342C25.1445 23.9033 26.399 21.7657 26.399 19.4467V15.001Z" fill="url(#paint2_radial_420_101763)"/>
<path d="M26.399 15.001L34.399 19.801L35.999 21.401V24.8452C35.999 27.1642 34.7446 29.3018 32.72 30.4327L23.12 35.7949C21.1804 36.8784 18.8177 36.8784 16.878 35.7949L7.27804 30.4327C7.09146 30.3284 6.91141 30.2157 6.73828 30.095L7.278 30.3965C9.21762 31.4799 11.5803 31.4799 13.52 30.3965L23.12 25.0342C25.1445 23.9033 26.399 21.7657 26.399 19.4467V15.001Z" fill="url(#paint3_linear_420_101763)"/>
<path d="M32.7191 9.17053L23.119 3.8117C21.1802 2.72943 18.819 2.72943 16.8802 3.8117L16.3151 4.1271C14.6239 5.31737 13.5996 7.26472 13.5996 9.36025V16.0461L16.8802 14.2149C18.819 13.1326 21.1802 13.1326 23.119 14.2149L32.7191 19.5737C34.6984 20.6786 35.9421 22.7456 35.9977 25.0039C35.999 24.9514 35.9996 24.8987 35.9996 24.8459V14.7588C35.9996 12.4392 34.7445 10.3011 32.7191 9.17053Z" fill="url(#paint4_radial_420_101763)"/>
<path d="M32.7191 9.17053L23.119 3.8117C21.1802 2.72943 18.819 2.72943 16.8802 3.8117L16.3151 4.1271C14.6239 5.31737 13.5996 7.26472 13.5996 9.36025V16.0461L16.8802 14.2149C18.819 13.1326 21.1802 13.1326 23.119 14.2149L32.7191 19.5737C34.6984 20.6786 35.9421 22.7456 35.9977 25.0039C35.999 24.9514 35.9996 24.8987 35.9996 24.8459V14.7588C35.9996 12.4392 34.7445 10.3011 32.7191 9.17053Z" fill="url(#paint5_linear_420_101763)"/>
<defs>
<radialGradient id="paint0_radial_420_101763" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(17.5054 12.8837) rotate(112.544) scale(23.4519 19.3067)">
<stop offset="0.206732" stop-color="#45D586"/>
<stop offset="0.875628" stop-color="#128245"/>
</radialGradient>
<linearGradient id="paint1_linear_420_101763" x1="15.0625" y1="29.6174" x2="13.787" y2="27.2436" gradientUnits="userSpaceOnUse">
<stop offset="0.9999" stop-color="#0F773F"/>
<stop offset="1" stop-color="#0078D4" stop-opacity="0"/>
</linearGradient>
<radialGradient id="paint2_radial_420_101763" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10.899 27.5214) rotate(-3.9995) scale(24.6197 15.4594)">
<stop offset="0.140029" stop-color="#FBFF47"/>
<stop offset="0.952721" stop-color="#54B228"/>
</radialGradient>
<linearGradient id="paint3_linear_420_101763" x1="30.8932" y1="18.5508" x2="29.5491" y2="20.8921" gradientUnits="userSpaceOnUse">
<stop offset="0.9999" stop-color="#27770B"/>
<stop offset="1" stop-color="#8C66BA" stop-opacity="0"/>
</linearGradient>
<radialGradient id="paint4_radial_420_101763" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(30.632 19.3878) rotate(-138.33) scale(22.8015 13.0047)">
<stop stop-color="#95FEA0"/>
<stop offset="0.839255" stop-color="#10B7B7"/>
</radialGradient>
<linearGradient id="paint5_linear_420_101763" x1="13.5996" y1="11.7134" x2="16.2131" y2="11.7134" gradientUnits="userSpaceOnUse">
<stop offset="0.9999" stop-color="#0A7B7B"/>
<stop offset="1" stop-color="#436DCD" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="334" height="154" viewBox="0 0 334 154" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="333" height="153" fill="#F3F2F1" stroke="#E1DFDD"/>
</svg>

After

Width:  |  Height:  |  Size: 188 B

BIN
images/firewallRule.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" ?>
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
<svg enable-background="new 0 0 256 256" height="256px" id="Layer_1" version="1.1" viewBox="0 0 256 256" width="256px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path stroke="white" stroke-width="0.5" fill="#b5a3a3" d="M179.199,38.399c0,1.637-0.625,3.274-1.875,4.524l-85.076,85.075l85.076,85.075c2.5,2.5,2.5,6.55,0,9.05s-6.55,2.5-9.05,0 l-89.601-89.6c-2.5-2.5-2.5-6.551,0-9.051l89.601-89.6c2.5-2.5,6.55-2.5,9.05,0C178.574,35.124,179.199,36.762,179.199,38.399z"/>
<path stroke="white" stroke-width="0.5" fill="#000" d="M179.199,38.399c0,1.637-0.625,3.274-1.875,4.524l-85.076,85.075l85.076,85.075c2.5,2.5,2.5,6.55,0,9.05s-6.55,2.5-9.05,0 l-89.601-89.6c-2.5-2.5-2.5-6.551,0-9.051l89.601-89.6c2.5-2.5,6.55-2.5,9.05,0C178.574,35.124,179.199,36.762,179.199,38.399z"/>
</svg>

Before

Width:  |  Height:  |  Size: 652 B

After

Width:  |  Height:  |  Size: 649 B

View File

@@ -18,7 +18,8 @@ module.exports = {
// clearMocks: false,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
collectCoverage: process.env.skipCodeCoverage === "true" ? false : true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}"],
@@ -36,7 +37,7 @@ module.exports = {
coverageThreshold: {
global: {
branches: 25,
functions: 25,
functions: 24,
lines: 28,
statements: 28,
},

View File

@@ -3,337 +3,337 @@
@import "../Common/Constants";
.query-panel {
display: table;
display: none;
width: 100%;
border-top: 1px solid #DDDDDD;
/*[{environment-commandbar-toolbar-separator}]*/
background-color: #ffffff;
/*[{plugin-background-color}]*/
padding: 2px 0px 0px 2px;
resize: vertical;
display: table;
display: none;
width: 100%;
border-top: 1px solid #dddddd;
/*[{environment-commandbar-toolbar-separator}]*/
background-color: #ffffff;
/*[{plugin-background-color}]*/
padding: 2px 0px 0px 2px;
resize: vertical;
}
.query-panel .row {
display: table-row;
display: table-row;
}
.query-panel .row .cell {
display: table-cell;
display: table-cell;
}
.query-panel.transition-in {
display: table;
top: 0px;
-webkit-transition: top 2s linear;
-ms-transition: top 2s linear;
-moz-transition: top 2s linear;
-khtml-transition: top 2s linear;
-o-transition: top 2s linear;
transition: top 2s linear;
display: table;
top: 0px;
-webkit-transition: top 2s linear;
-ms-transition: top 2s linear;
-moz-transition: top 2s linear;
-khtml-transition: top 2s linear;
-o-transition: top 2s linear;
transition: top 2s linear;
}
.query-builder {
width:100%;
padding-right: @DefaultSpace;
border-bottom: 1px solid @BaseMedium;
margin-bottom: @DefaultSpace;
width: 100%;
padding-right: @DefaultSpace;
border-bottom: 1px solid @BaseMedium;
margin-bottom: @DefaultSpace;
}
.query-builder-toolbar {
background-color: #ffffff;
/*[{plugin-background-color}]*/
min-width: 600px;
height: 30px;
border-bottom: 1px solid #DDDDDD;
/*[1px solid {environment-commandbar-toolbar-separator}]*/
background-color: #ffffff;
/*[{plugin-background-color}]*/
min-width: 600px;
height: 30px;
border-bottom: 1px solid #dddddd;
/*[1px solid {environment-commandbar-toolbar-separator}]*/
}
.query-builder-toolbar .query-toolbar-group {
display: inline-block;
height: 24px;
margin: 2px 0px;
vertical-align: middle;
display: inline-block;
height: 24px;
margin: 2px 0px;
vertical-align: middle;
}
.query-builder-toolbar .query-toolbar-group .query-toolbar-button {
min-width: 0px;
padding: 0px;
margin-left: 2px;
background-color: transparent;
border: solid transparent;
min-width: 0px;
padding: 0px;
margin-left: 2px;
background-color: transparent;
border: solid transparent;
}
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:active {
outline: 2px solid dodgerblue;
/*[2px solid {common-common-controls-button-border-hover}]*/
outline: 2px solid dodgerblue;
/*[2px solid {common-common-controls-button-border-hover}]*/
}
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:hover {
background-color: #CCCEDB;
/*[{common-controls-button-hover-background}]*/
background-color: #cccedb;
/*[{common-controls-button-hover-background}]*/
}
.query-builder-toolbar .query-toolbar-group .query-toolbar-button.active {
background-color: #E6E7ED;
/*[{common-controls-inner-tab-active-background}]*/
outline: none
background-color: #e6e7ed;
/*[{common-controls-inner-tab-active-background}]*/
outline: none;
}
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:disabled,
.query-builder-toolbar .query-toolbar-group .query-toolbar-button.disabled {
background-color: #ffffff;
/*[{common-controls-button-disabled-background}]*/
background: transparent;
border: 1px solid transparent;
outline: none;
opacity: 0.4;
background-color: #ffffff;
/*[{common-controls-button-disabled-background}]*/
background: transparent;
border: 1px solid transparent;
outline: none;
opacity: 0.4;
}
.tableContainer {
overflow: hidden;
.flex-display();
.flex-direction();
overflow: hidden;
.flex-display();
.flex-direction();
}
.tablesQueryTab{
padding-left: @MediumSpace;
width: 100%;
margin-bottom:@LargeSpace;
.tablesQueryTab {
padding-left: @MediumSpace;
width: 100%;
margin-bottom: @LargeSpace;
}
.entity-error-Img {
width: @WarningErrorIconSize;
height: @WarningErrorIconSize;
margin: @DefaultSpace 0px 0px @SmallSpace;
width: @WarningErrorIconSize;
height: @WarningErrorIconSize;
margin: @DefaultSpace 0px 0px @SmallSpace;
}
.query-editor-panel {
margin-right: 16px;
margin-left: 16px;
margin-top: 25px;
position: relative;
vertical-align: middle;
cursor: default;
margin-right: 16px;
margin-left: 16px;
margin-top: 25px;
position: relative;
vertical-align: middle;
cursor: default;
}
.query-editor-text {
width: 100%;
margin: 2px;
border: solid 1px #A9ACB3;
/*[{plugin-textbox-disabled-color}]*/
resize: none;
margin-top: -39px;
background-color: #ddd;
padding: 5px;
width: 100%;
margin: 2px;
border: solid 1px #a9acb3;
/*[{plugin-textbox-disabled-color}]*/
resize: none;
margin-top: -39px;
background-color: #ddd;
padding: 5px;
}
.error-bar {
padding: @LargeSpace 34px @MediumSpace 24px;
padding: @LargeSpace 34px @MediumSpace 24px;
}
.error-message {
background-color: @BaseLow;
padding: @DefaultSpace;
display: inline-flex;
background-color: @BaseLow;
padding: @DefaultSpace;
display: inline-flex;
}
.error-text {
padding-left: @MediumSpace;
padding-left: @MediumSpace;
}
.query-editor-text-invalid {
width: 100%;
margin: 2px;
border: 1px solid #e51400;
resize: none;
margin-top: -30px;
width: 100%;
margin: 2px;
border: 1px solid #e51400;
resize: none;
margin-top: -30px;
}
.query-editor-panel .warning-bar {
width: 100%;
height: 20px;
background-color: #ffffff;
/*[{plugin-background-color}]*/
position: absolute;
top: -24px;
width: 100%;
height: 20px;
background-color: #ffffff;
/*[{plugin-background-color}]*/
position: absolute;
top: -24px;
}
.query-editor-panel .warning-bar .warning-message {
display: inline-flex;
padding-top: 2px;
vertical-align: middle;
display: inline-flex;
padding-top: 2px;
vertical-align: middle;
}
.query-editor-panel .warning-bar .warning-message .warning-text {
margin-left: 2px;
margin-left: 2px;
}
.advanced-options-panel{
margin-bottom: @DefaultSpace;
.advanced-options-panel {
margin-bottom: @DefaultSpace;
}
.advanced-options-panel .advanced-heading .advanced-title {
display: inline-flex;
margin-left: 27px;
margin-top: 10px;
cursor: default;
display: inline-flex;
margin-left: 27px;
margin-top: 10px;
cursor: default;
}
.advanced-options-panel .advanced-options {
margin-left: 32px;
margin-top: 5px;
border: 1px solid transparent;
margin-left: 32px;
margin-top: 5px;
border: 1px solid transparent;
}
hr {
margin-top: 10px;
margin-bottom: 12px;
border: 0;
border-top: 1px solid #ccc;
margin-top: 10px;
margin-bottom: 12px;
border: 0;
border-top: 1px solid #ccc;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
-webkit-appearance: none;
}
.advanced-options-panel .advanced-options .top .top-input {
width: 100px;
word-spacing: normal;
color: #1E1E1E;
/*[{common-controls-button-foreground}]*/
border: 1px solid #CCCEDB;
/*[1px solid {plugin-textbox-border-color}]*/
height: 20px;
margin-left: 8px;
width: 100px;
word-spacing: normal;
color: #1e1e1e;
/*[{common-controls-button-foreground}]*/
border: 1px solid #cccedb;
/*[1px solid {plugin-textbox-border-color}]*/
height: 20px;
margin-left: 8px;
}
.advanced-options-panel .advanced-options .top .invalid-top {
color: red;
color: red;
}
.advanced-options-panel .advanced-options .select {
margin-top: 18px;
display: inline-flex;
margin-top: 18px;
display: inline-flex;
}
.advanced-options-icon {
margin-left: 2px;
vertical-align: sub;
margin-left: 2px;
vertical-align: sub;
}
.advanced-options-panel .advanced-options .select .select-options-text {
margin-left: 4px;
margin-left: 4px;
}
.advanced-options-panel .advanced-options .select .select-options-link {
margin-left: 4px;
cursor: pointer;
outline: none;
margin-left: 4px;
cursor: pointer;
outline: none;
}
.query-panel .row .column-headers .Field {
padding-left: 95px;
padding-right: 0px;
padding-bottom: 6px;
padding-left: 95px;
padding-right: 0px;
padding-bottom: 6px;
}
.clause-table {
border-spacing: 0px;
display: table;
width: 100%;
margin-top: -3px;
border-spacing: 0px;
display: table;
width: 100%;
margin-top: -3px;
}
.clause-table-row {
display: row;
margin-bottom: 10px;
display: row;
margin-bottom: 10px;
}
.clause-table-cell {
display: table-cell;
text-align: left;
vertical-align: middle;
display: table-cell;
text-align: left;
vertical-align: middle;
}
.action-column>button,
.group-control-header>button,
.group-indicator-column>button {
min-width: 20px;
width: 20px;
padding: 0px;
background-color: transparent;
border-color: transparent;
cursor: pointer;
.action-column > button,
.group-control-header > button,
.group-indicator-column > button {
min-width: 20px;
width: 20px;
padding: 0px;
background-color: transparent;
border-color: transparent;
cursor: pointer;
}
.group-control-header>button:disabled {
min-width: 20px;
width: 20px;
padding: 0px;
background-color: transparent;
border-color: transparent;
outline: none;
opacity: 0.4;
cursor: default;
.group-control-header > button:disabled {
min-width: 20px;
width: 20px;
padding: 0px;
background-color: transparent;
border-color: transparent;
outline: none;
opacity: 0.4;
cursor: default;
}
.clause-table-field {
width: 100%;
border: 1px solid #bbbbbb;
width: 100%;
border: 1px solid #bbbbbb;
}
.clause-table-cell button {
height: 20px;
height: 20px;
}
.clause-table-cell input[type="checkbox"] {
padding: 0px;
margin-bottom: 12px;
padding: 0px;
margin-bottom: 12px;
}
.and-or-svg {
margin-top: -8px;
margin-right: -5px;
margin-top: -8px;
margin-right: -5px;
}
.scroll-box {
border-bottom: 1px transparent #DDD;
/*[1px solid {plugin-table-border-color}]*/
border-top: 1px transparent #DDD;
/*[1px solid {plugin-table-border-color}]*/
max-height: 20vh;
width: 100%;
border-bottom: 1px transparent #ddd;
/*[1px solid {plugin-table-border-color}]*/
border-top: 1px transparent #ddd;
/*[1px solid {plugin-table-border-color}]*/
max-height: 20vh;
width: 100%;
}
.scrollable {
overflow: auto;
overflow-x: hidden;
overflow: auto;
overflow-x: hidden;
}
.and-or-column,
.and-or-header {
min-width: 65px;
padding-right: 10px;
padding-left: 5px;
min-width: 65px;
padding-right: 10px;
padding-left: 5px;
}
.operator-column,
.operator-header {
min-width: 65px;
padding-right: 10px;
min-width: 65px;
padding-right: 10px;
}
.field-header,
.field-column {
min-width: 125px;
padding-right: 10px;
min-width: 125px;
padding-right: 10px;
}
.type-header,
.type-column {
min-width: 85px;
min-width: 85px;
}
.and-or-column,
@@ -345,41 +345,41 @@ input::-webkit-inner-spin-button {
.type-header,
.type-column,
.action-header {
padding-right: 10px;
margin-bottom: 8px;
padding-right: 10px;
margin-bottom: 8px;
}
.value-header,
.value-column,
.time-column {
min-width: 230px;
padding: 0px 4px 0px 0px;
width: 100%;
margin-bottom: 8px;
min-width: 230px;
padding: 0px 4px 0px 0px;
width: 100%;
margin-bottom: 8px;
}
.group-control-header,
.group-control-column {
min-width: 25px;
text-align: right;
min-width: 25px;
text-align: right;
}
.group-indicator-table {
border-spacing: 0px;
min-height: 24px
border-spacing: 0px;
min-height: 24px;
}
.group-indicator-column {
min-width: 21px;
padding: 0px;
border-style: none;
height: 29px;
min-width: 21px;
padding: 0px;
border-style: none;
height: 29px;
}
.clause-table-cell.action-column,
.clause-table-cell.action-column,
.clause-table-cell.action-header {
min-width: 60px;
padding-left: @SmallSpace;
min-width: 60px;
padding-left: @SmallSpace;
}
.action-header,
@@ -388,15 +388,14 @@ input::-webkit-inner-spin-button {
.operator-header,
.value-header,
.and-or-header {
padding-right: 4px;
padding-bottom: 5px;
padding-right: 4px;
padding-bottom: 5px;
}
.header-background {
background-color: #ffffff;
background-color: #ffffff;
}
/*.type-header {
padding-right: 4px;
}
@@ -410,112 +409,111 @@ input::-webkit-inner-spin-button {
}*/
.clause-table-field[readonly] {
background-color: #EEEEF2;
/*[{plugin-table-header-background-color}]*/
border: 1px solid #CCCEDB;
/*[{plugin-table-border-color}]*/
background-color: #eeeef2;
/*[{plugin-table-header-background-color}]*/
border: 1px solid #cccedb;
/*[{plugin-table-border-color}]*/
}
.addClause-title {
/*[{common-common-controls-button-border-hover}]*/
cursor: pointer;
margin-left: -5px;
/*[{common-common-controls-button-border-hover}]*/
cursor: pointer;
margin-left: -5px;
}
.addClause {
width: 125px;
padding: 8px 0px 5px 5px;
border: 1px solid #fff;
margin-left: 5px;
width: 125px;
padding: 8px 0px 5px 5px;
border: 1px solid #fff;
margin-left: 5px;
}
.addClause:hover {
.hover();
.hover();
}
.addClause:active {
.active();
border: 1px dashed @AccentMedium;
.active();
border: 1px dashed @AccentMedium;
}
.clause-table-row {
min-width: 550px;
width: 100%;
min-width: 550px;
width: 100%;
}
.clause-table-field field-column {
min-width: 75px;
height: 30px;
width: 100%;
min-width: 75px;
height: 30px;
width: 100%;
}
.clause-table-field field-input {
min-width: 54px;
margin-left: -78px;
height: 25px;
border: none;
min-width: 54px;
margin-left: -78px;
height: 25px;
border: none;
}
.query-panel .row .spacing {
padding-bottom: 6px;
padding-bottom: 6px;
}
.query-panel .divider.horizontal {
height: 10px;
width: 100%
height: 10px;
width: 100%;
}
.inline-div {
display: inline
display: inline;
}
.querybuilder-addpropertyImg,
.querybuilder-cancelImg {
width: 14px;
height: 14px;
margin-left: 3px;
margin-bottom: 8px;
width: 14px;
height: 14px;
margin-left: 3px;
margin-bottom: 8px;
}
.addclauseProperty-Img {
width: 14px;
height: 14px;
margin-bottom: 5px;
margin-left: 12px;
width: 14px;
height: 14px;
margin-bottom: 5px;
margin-left: 12px;
}
.entity-Add-Cancel {
padding: @DefaultSpace @SmallSpace @SmallSpace;
cursor: pointer;
// padding: @DefaultSpace @SmallSpace @SmallSpace;
cursor: pointer;
}
.entity-Add-Cancel:hover {
.hover();
.hover();
}
.entity-Add-Cancel:active {
.active();
.active();
}
.query-builder-isDisabled {
border: 1px solid #CCCEDB;
color: #ccc;
border: 1px solid #cccedb;
color: #ccc;
}
.edit-value-text {
padding-left: @DefaultSpace;
padding-left: @DefaultSpace;
}
.expand-triangle {
width: 10px;
height: 10px;
width: 10px;
height: 10px;
}
.expand-triangle-right {
margin-bottom: 5px;
margin-bottom: 5px;
}
/*
@media only screen and (max-width: 1200px) {
.clause-table {
@@ -524,4 +522,4 @@ input::-webkit-inner-spin-button {
width: 100%;
padding-top: 10px;
}
}*/
}*/

View File

@@ -2576,6 +2576,10 @@ a:link {
.querydropdown.placeholderVisible {
font-style: italic;
}
.querydropdown.placeholderVisible::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: #767474;
opacity: 1;
}
.querydropdown:hover {
background-color: @AccentLow;
@@ -3087,3 +3091,4 @@ a:link {
background: white;
height: 100%;
}

View File

@@ -5,12 +5,17 @@
overflow: auto;
.databaseHeader {
padding: 1px;
font-size: 14px;
}
.collectionHeader {
font-size: 12px;
}
.loadMoreHeader {
color: RGB(5, 99, 193);
}
}
.notebookResourceTree {
@@ -23,6 +28,4 @@
.clickDisabled {
pointer-events: none;
}
}
}

View File

@@ -212,7 +212,7 @@
"strict:find": "node ./strict-null-checks/find.js",
"strict:add": "node ./strict-null-checks/auto-add.js",
"compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks",
"generateARMClients": "npx ts-node --compiler-options '{\"module\":\"commonjs\"}' utils/armClientGenerator/generator.ts"
"generateARMClients": "npx ts-node utils/armClientGenerator/generator.ts"
},
"repository": {
"type": "git",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
import { userContext } from "../UserContext";
import { getApiShortDisplayName } from "../Utils/APITypeUtils";
import { NormalizedEventKey } from "./Constants";
export interface CollapsedResourceTreeProps {
@@ -36,7 +36,7 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
id="collapseToggleLeftPaneButton"
role="button"
tabIndex={0}
aria-label="Expand Tree"
aria-label={getApiShortDisplayName() + `Expand tree`}
onClick={toggleLeftPaneExpanded}
onKeyPress={onKeyPressToggleLeftPaneExpanded}
ref={focusButton}
@@ -45,7 +45,7 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
</span>
<span className="collectionCollapsed">
<span>{userContext.apiType} API</span>
<span>{getApiShortDisplayName()}</span>
</span>
</li>
</ul>

View File

@@ -45,6 +45,8 @@ export class ArmResourceTypes {
export class BackendDefaults {
public static partitionKeyKind = "Hash";
public static partitionKeyMultiHash = "MultiHash";
public static maxNumMultiHashPartition = 2;
public static singlePartitionStorageInGb: string = "10";
public static multiPartitionStorageInGb: string = "100";
public static maxChangeFeedRetentionDuration: number = 10;
@@ -139,7 +141,7 @@ export class Queries {
public static UnlimitedPageOption: string = "unlimited";
public static itemsPerPage: number = 100;
public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions
public static containersPerPage: number = 50;
public static QueryEditorMinHeightRatio: number = 0.1;
public static QueryEditorMaxHeightRatio: number = 0.4;
public static readonly DefaultMaxDegreeOfParallelism = 6;
@@ -426,3 +428,91 @@ export class JunoEndpoints {
public static readonly Prod = "https://tools.cosmos.azure.com";
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
}
export const QueryCopilotSampleDatabaseId = "CopilotSampleDb";
export const QueryCopilotSampleContainerId = "SampleContainer";
export const QueryCopilotSampleContainerSchema = {
product: {
sampleData: {
id: "de6fadec-0384-43c8-93ea-16c0170b5460",
name: "Premium Phone Mini (Red)",
price: 652.04,
category: "Electronics",
description:
"This Premium Phone Mini (Red) is designed by the company under agreement with the FCC, so we'd give it a pass in either direction, but no one should be using this handset without a compatible handset. All in all, this phone is one of the most affordable Android handsets out there at $100. Check them out.\n\n9. HTC One M9 4",
stock: 74,
countryOfOrigin: "Mexico",
firstAvailable: "2018-07-07 17:42:28",
priceHistory: [592.81],
customerRatings: [
{ username: "dannyhowell", stars: 1, date: "2022-03-12 17:01:23", verifiedUser: true },
{ username: "lindsay26", stars: 1, date: "2022-12-29 07:18:20", verifiedUser: false },
{ username: "smithmiguel", stars: 3, date: "2022-09-08 11:43:27", verifiedUser: false },
{ username: "julie07", stars: 3, date: "2021-03-14 23:54:10", verifiedUser: true },
{ username: "kelly93", stars: 3, date: "2022-11-05 12:45:43", verifiedUser: false },
{ username: "katherinereynolds", stars: 2, date: "2022-09-14 11:49:36", verifiedUser: false },
{ username: "chandlerkenneth", stars: 1, date: "2022-01-14 12:14:43", verifiedUser: true },
],
rareProperty: true,
},
schema: {
properties: {
id: {
type: "string",
},
name: {
type: "string",
},
price: {
type: "number",
},
category: {
type: "string",
},
description: {
type: "string",
},
stock: {
type: "number",
},
countryOfOrigin: {
type: "string",
},
firstAvailable: {
type: "string",
},
priceHistory: {
items: {
type: "number",
},
type: "array",
},
customerRatings: {
items: {
properties: {
username: {
type: "string",
},
stars: {
type: "number",
},
date: {
type: "string",
},
verifiedUser: {
type: "boolean",
},
},
type: "object",
},
type: "array",
},
rareProperty: {
type: "boolean",
},
},
type: "object",
},
},
};

View File

@@ -55,7 +55,6 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
label={entityValueLabel && entityValueLabel}
className="addEntityTextField"
id="entityValueId"
autoFocus
disabled={isEntityValueDisable}
type={entityValueType}
placeholder={entityValuePlaceholder}

View File

@@ -0,0 +1,14 @@
import * as EnvironmentUtility from "./EnvironmentUtility";
describe("Environment Utility Test", () => {
it("Test sample URI with /", () => {
const uri = "test/";
expect(EnvironmentUtility.normalizeArmEndpoint(uri)).toEqual(uri);
});
it("Test sample URI without /", () => {
const uri = "test";
const expectedResult = "test/";
expect(EnvironmentUtility.normalizeArmEndpoint(uri)).toEqual(expectedResult);
});
});

View File

@@ -1,6 +1,6 @@
import * as OfferUtility from "./OfferUtility";
import { SDKOfferDefinition, Offer } from "../Contracts/DataModels";
import { OfferResponse } from "@azure/cosmos";
import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
import * as OfferUtility from "./OfferUtility";
describe("parseSDKOfferResponse", () => {
it("manual throughput", () => {
@@ -31,6 +31,26 @@ describe("parseSDKOfferResponse", () => {
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
});
it("offerContent not defined", () => {
const mockOfferDefinition = {
id: "test",
} as SDKOfferDefinition;
const mockResponse = {
resource: mockOfferDefinition,
} as OfferResponse;
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(undefined);
});
it("offerDefinition is null", () => {
const mockResponse = {
resource: undefined,
} as OfferResponse;
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(undefined);
});
it("autoscale throughput", () => {
const mockOfferDefinition = {
content: {

View File

@@ -6,6 +6,7 @@ import Explorer from "../Explorer/Explorer";
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
import { userContext } from "../UserContext";
import { getApiShortDisplayName } from "../Utils/APITypeUtils";
import { NormalizedEventKey } from "./Constants";
export interface ResourceTreeContainerProps {
@@ -42,7 +43,7 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
{/* Collections Window Title/Command Bar - Start */}
<div className="collectiontitle">
<div className="coltitle">
<span className="titlepadcol">{userContext.apiType} API</span>
<span className="titlepadcol">{getApiShortDisplayName()}</span>
<div className="float-right">
<span
className="padimgcolrefresh"
@@ -50,7 +51,7 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
role="button"
data-bind="click: onRefreshResourcesClick, clickBubble: false, event: { keypress: onRefreshDatabasesKeyPress }"
tabIndex={0}
aria-label="Refresh tree"
aria-label={getApiShortDisplayName() + `Refresh tree`}
title="Refresh tree"
>
<img className="refreshcol" src={refreshImg} alt="Refresh Tree" />
@@ -62,7 +63,7 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
onClick={toggleLeftPaneExpanded}
onKeyPress={onKeyPressToggleLeftPaneExpanded}
tabIndex={0}
aria-label="Collapse Tree"
aria-label={getApiShortDisplayName() + `Collapse Tree`}
title="Collapse Tree"
ref={focusButton}
>

View File

@@ -73,6 +73,17 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
const sectionStackTokens: IStackTokens = { childrenGap: 12 };
const handleKeyPress = (event: React.KeyboardEvent<HTMLElement>) => {
if (event.key === "Enter" || event.key === "Space") {
onEditEntity();
}
};
const handleKeyPressdelete = (event: React.KeyboardEvent<HTMLElement>) => {
if (event.key === "Enter" || event.key === "Space") {
onDeleteEntity();
}
};
const getEntityValueType = (): string => {
const { Int, Smallint, Tinyint } = CassandraType;
const { Double, Int32, Int64 } = TableType;
@@ -126,12 +137,29 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
/>
{!isEntityValueDisable && (
<TooltipHost content="Edit property" id="editTooltip">
<Image {...imageProps} src={EditIcon} alt="editEntity" id="editEntity" onClick={onEditEntity} />
<div tabIndex={0}>
<Image
{...imageProps}
src={EditIcon}
alt="editEntity"
onClick={onEditEntity}
tabIndex={0}
onKeyPress={handleKeyPress}
/>
</div>
</TooltipHost>
)}
{isDeleteOptionVisible && userContext.apiType !== "Cassandra" && (
<TooltipHost content="Delete property" id="deleteTooltip">
<Image {...imageProps} src={DeleteIcon} alt="delete entity" id="deleteEntity" onClick={onDeleteEntity} />
<Image
{...imageProps}
src={DeleteIcon}
alt="delete entity"
id="deleteEntity"
onClick={onDeleteEntity}
tabIndex={0}
onKeyPress={handleKeyPressdelete}
/>
</TooltipHost>
)}
</Stack>

View File

@@ -9,7 +9,7 @@ export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }:
return (
<span>
<TooltipHost content={children}>
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" tabIndex={0} />
<Icon iconName="Info" ariaLabel={children} className="panelInfoIcon" tabIndex={0} />
</TooltipHost>
</span>
);

View File

@@ -0,0 +1,49 @@
import * as UrlUtility from "./UrlUtility";
describe("parseDocumentsPath", () => {
it("empty resource path", () => {
const resourcePath = "";
expect(UrlUtility.parseDocumentsPath(resourcePath)).toEqual({});
});
it("resourcePath does not begin or end with /", () => {
const resourcePath = "localhost/portal/home";
const expectedResult = {
type: "home",
objectBody: {
id: "portal",
self: "/localhost/portal/home/",
},
};
expect(UrlUtility.parseDocumentsPath(resourcePath)).toEqual(expectedResult);
});
it("resourcePath length is even", () => {
const resourcePath = "/localhost/portal/src/home/";
const expectedResult = {
type: "src",
objectBody: {
id: "home",
self: resourcePath,
},
};
expect(UrlUtility.parseDocumentsPath(resourcePath)).toEqual(expectedResult);
});
it("createUri", () => {
const baseUri = "http://foo.com/bar/";
const relativeUri = "/index.html";
const expectedUri = "http://foo.com/bar/index.html";
expect(UrlUtility.createUri(baseUri, relativeUri)).toEqual(expectedUri);
});
it("should throw an error if baseUri is empty", () => {
expect(() => {
UrlUtility.createUri("", "/home");
}).toThrow("baseUri is null or empty");
});
});

View File

@@ -4,6 +4,7 @@ import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationCons
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { getPartitionKeyValue } from "./getPartitionKeyValue";
export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<void> => {
const entityName: string = getEntityName();
@@ -13,7 +14,7 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
await client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue)
.item(documentId.id(), getPartitionKeyValue(documentId))
.delete();
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
} catch (error) {

View File

@@ -0,0 +1,12 @@
import { userContext } from "UserContext";
import DocumentId from "../../Explorer/Tree/DocumentId";
export const getPartitionKeyValue = (documentId: DocumentId) => {
if (userContext.apiType === "Tables" && documentId.partitionKeyValue?.length === 0) {
return "";
}
if (documentId.partitionKeyValue?.length === 0) {
return undefined;
}
return documentId.partitionKeyValue;
};

View File

@@ -96,6 +96,14 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
? parseInt(resource.minimumThroughput)
: resource.minimumThroughput;
const autoscaleSettings = resource.autoscaleSettings;
const instantMaximumThroughput: number =
typeof resource.instantMaximumThroughput === "string"
? parseInt(resource.instantMaximumThroughput)
: resource.instantMaximumThroughput;
const softAllowedMaximumThroughput: number =
typeof resource.softAllowedMaximumThroughput === "string"
? parseInt(resource.softAllowedMaximumThroughput)
: resource.softAllowedMaximumThroughput;
if (autoscaleSettings) {
return {
@@ -104,6 +112,8 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
manualThroughput: undefined,
minimumThroughput,
offerReplacePending: resource.offerReplacePending === "true",
instantMaximumThroughput,
softAllowedMaximumThroughput,
};
}
@@ -113,6 +123,8 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
manualThroughput: resource.throughput,
minimumThroughput,
offerReplacePending: resource.offerReplacePending === "true",
instantMaximumThroughput,
softAllowedMaximumThroughput,
};
}

View File

@@ -1,3 +1,4 @@
import { Queries } from "Common/Constants";
import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
@@ -31,6 +32,35 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
}
}
export async function readCollectionsWithPagination(
databaseId: string,
continuationToken?: string
): Promise<DataModels.CollectionsWithPagination> {
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
try {
const sdkResponse = await client()
.database(databaseId)
.containers.query(
{ query: "SELECT * FROM c" },
{
continuationToken,
maxItemCount: Queries.containersPerPage,
}
)
.fetchNext();
const collectionsWithPagination: DataModels.CollectionsWithPagination = {
collections: sdkResponse.resources as DataModels.Collection[],
continuationToken: sdkResponse.continuationToken,
};
return collectionsWithPagination;
} catch (error) {
handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`);
throw error;
} finally {
clearMessage();
}
}
async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Collection[]> {
let rpResponse;

View File

@@ -68,6 +68,14 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
? parseInt(resource.minimumThroughput)
: resource.minimumThroughput;
const autoscaleSettings = resource.autoscaleSettings;
const instantMaximumThroughput: number =
typeof resource.instantMaximumThroughput === "string"
? parseInt(resource.instantMaximumThroughput)
: resource.instantMaximumThroughput;
const softAllowedMaximumThroughput: number =
typeof resource.softAllowedMaximumThroughput === "string"
? parseInt(resource.softAllowedMaximumThroughput)
: resource.softAllowedMaximumThroughput;
if (autoscaleSettings) {
return {
@@ -76,6 +84,8 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
manualThroughput: undefined,
minimumThroughput,
offerReplacePending: resource.offerReplacePending === "true",
instantMaximumThroughput,
softAllowedMaximumThroughput,
};
}
@@ -85,6 +95,8 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
manualThroughput: resource.throughput,
minimumThroughput,
offerReplacePending: resource.offerReplacePending === "true",
instantMaximumThroughput,
softAllowedMaximumThroughput,
};
}

View File

@@ -6,6 +6,7 @@ import { HttpHeaders } from "../Constants";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { getPartitionKeyValue } from "./getPartitionKeyValue";
export const readDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<Item> => {
const entityName = getEntityName();
@@ -21,8 +22,7 @@ export const readDocument = async (collection: CollectionBase, documentId: Docum
const response = await client()
.database(collection.databaseId)
.container(collection.id())
// use undefined if the partitionKeyValue is empty
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue)
.item(documentId.id(), getPartitionKeyValue(documentId))
.read(options);
return response?.resource;

View File

@@ -1,8 +1,9 @@
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import { CloudError, SqlStoredProcedureListResult } from "Utils/arm/generatedClients/cosmos/types";
import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
import { listSqlStoredProcedures } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { listSqlStoredProcedures } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
@@ -24,7 +25,13 @@ export async function readStoredProcedures(
databaseId,
collectionId
);
return rpResponse?.value?.map((sproc) => sproc.properties?.resource as StoredProcedureDefinition & Resource);
const listResult = rpResponse as SqlStoredProcedureListResult;
if (listResult) {
return listResult?.value?.map((sproc) => sproc.properties?.resource as StoredProcedureDefinition & Resource);
}
const cloudError = rpResponse as CloudError;
throw new Error(cloudError?.error?.message);
}
const response = await client()

View File

@@ -6,6 +6,7 @@ import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationCons
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { getPartitionKeyValue } from "./getPartitionKeyValue";
export const updateDocument = async (
collection: CollectionBase,
@@ -25,7 +26,7 @@ export const updateDocument = async (
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue)
.item(documentId.id(), getPartitionKeyValue(documentId))
.replace(newDocument, options);
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);

View File

@@ -33,6 +33,8 @@ export interface DatabaseAccountExtendedProperties {
privateEndpointConnections?: unknown[];
capacity?: { totalThroughputLimit: number };
locations?: DatabaseAccountResponseLocation[];
postgresqlEndpoint?: string;
publicNetworkAccess?: string;
}
export interface DatabaseAccountResponseLocation {
@@ -154,6 +156,11 @@ export interface Collection extends Resource {
requestSchema?: () => void;
}
export interface CollectionsWithPagination {
continuationToken?: string;
collections?: Collection[];
}
export interface Database extends Resource {
collections?: Collection[];
}
@@ -235,6 +242,8 @@ export interface Offer {
minimumThroughput: number | undefined;
offerDefinition?: SDKOfferDefinition;
offerReplacePending: boolean;
instantMaximumThroughput?: number;
softAllowedMaximumThroughput?: number;
}
export interface SDKOfferDefinition extends Resource {
@@ -566,6 +575,16 @@ export interface ContainerConnectionInfo {
//need to add ram and rom info
}
export interface PostgresFirewallRule {
id: string;
name: string;
type: string;
properties: {
startIpAddress: string;
endIpAddress: string;
};
}
export enum PhoenixErrorType {
MaxAllocationTimeExceeded = "MaxAllocationTimeExceeded",
MaxDbAccountsPerUserExceeded = "MaxDbAccountsPerUserExceeded",

View File

@@ -35,6 +35,9 @@ export enum MessageTypes {
RefreshDatabaseAccount,
CloseTab,
OpenQuickstartBlade,
OpenPostgreSQLPasswordReset,
OpenPostgresNetworkingBlade,
OpenCosmosDBNetworkingBlade,
}
export { Versions, ActionContracts, Diagnostics };

View File

@@ -87,13 +87,13 @@ export interface Database extends TreeNode {
isDatabaseExpanded: ko.Observable<boolean>;
isDatabaseShared: ko.Computed<boolean>;
isSampleDB?: boolean;
collectionsContinuationToken?: string;
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
expandDatabase(): Promise<void>;
collapseDatabase(): void;
loadCollections(): Promise<void>;
loadCollections(restart?: boolean): Promise<void>;
findCollectionWithId(collectionId: string): Collection;
openAddCollection(database: Database, event: MouseEvent): void;
onSettingsClick: () => void;
@@ -186,7 +186,6 @@ export interface Collection extends CollectionBase {
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
getLabel(): string;
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
}
@@ -372,6 +371,7 @@ export enum TerminalKind {
Default = 0,
Mongo = 1,
Cassandra = 2,
Postgres = 3,
}
export interface DataExplorerInputsFrame {
@@ -395,6 +395,11 @@ export interface DataExplorerInputsFrame {
sharedThroughputDefault?: number;
dataExplorerVersion?: string;
defaultCollectionThroughput?: CollectionCreationDefaults;
isPostgresAccount?: boolean;
isReplica?: boolean;
clientIpAddress?: string;
// TODO: Update this param in the OSS extension to remove isFreeTier, isMarlinServerGroup, and make nodes a flat array instead of an nested array
connectionStringParams?: any;
flights?: readonly string[];
features?: {
[key: string]: string;

View File

@@ -11,9 +11,9 @@ import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
import DeleteUDFIcon from "../../images/DeleteUDF.svg";
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
import * as ViewModels from "../Contracts/ViewModels";
import { useSidePanel } from "../hooks/useSidePanel";
import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { useSidePanel } from "../hooks/useSidePanel";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import Explorer from "./Explorer";
import { useNotebook } from "./Notebook/useNotebook";
@@ -152,7 +152,7 @@ export const createStoreProcedureContextMenuItems = (
{
iconSrc: DeleteSprocIcon,
onClick: () => storedProcedure.delete(),
label: "Delete Store Procedure",
label: "Delete Stored Procedure",
},
];
};

View File

@@ -73,7 +73,7 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
<img
className="expandCollapseIcon"
src={this.state.isExpanded ? TriangleDownIcon : TriangleRightIcon}
alt="Hide"
alt={this.state.isExpanded ? `${this.props.title} hide` : `${this.props.title} expand`}
tabIndex={0}
role="button"
/>

View File

@@ -2,6 +2,7 @@
* Wrapper around Notebook server terminal
*/
import { useTerminal } from "hooks/useTerminal";
import postRobot from "post-robot";
import * as React from "react";
import * as DataModels from "../../../Contracts/DataModels";
@@ -40,6 +41,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
handleFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
this.terminalWindow = (event.target as HTMLIFrameElement).contentWindow;
useTerminal.getState().setTerminal(this.terminalWindow);
this.sendPropsToTerminalFrame();
}
@@ -74,6 +76,8 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
this.props.databaseAccount?.properties.mongoEndpoint || this.props.databaseAccount?.properties.documentEndpoint;
} else if (StringUtils.endsWith(notebookServerEndpoint, "cassandra")) {
terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint;
} else if (StringUtils.endsWith(notebookServerEndpoint, "postgresql")) {
return this.props.databaseAccount?.properties.postgresqlEndpoint;
}
if (terminalEndpoint) {

View File

@@ -1,5 +1,5 @@
import { HoverCard, HoverCardType, Icon, Label, Link, Stack } from "@fluentui/react";
import * as React from "react";
import { Icon, Label, Stack, HoverCard, HoverCardType, Link } from "@fluentui/react";
import { CodeOfConductEndpoints } from "../../../../Common/Constants";
import "./InfoComponent.less";
@@ -41,7 +41,7 @@ export class InfoComponent extends React.Component<InfoComponentProps> {
public render(): JSX.Element {
return (
<HoverCard plainCardProps={{ onRenderPlainCard: this.onHover }} instantOpenOnClick type={HoverCardType.plain}>
<div className="infoPanelMain">
<div className="infoPanelMain" tabIndex={0}>
<Icon className="infoIconMain" iconName="Help" styles={{ root: { verticalAlign: "middle" } }} />
<Label className="infoLabelMain">Help</Label>
</div>

View File

@@ -12,6 +12,7 @@ exports[`InfoComponent renders 1`] = `
>
<div
className="infoPanelMain"
tabIndex={0}
>
<Icon
className="infoIconMain"

View File

@@ -1,46 +1,29 @@
import { shallow } from "enzyme";
import React from "react";
import { IColumn, Text } from "@fluentui/react";
import {
getAutoPilotV3SpendElement,
getEstimatedSpendingElement,
manualToAutoscaleDisclaimerElement,
ttlWarning,
indexingPolicynUnsavedWarningMessage,
updateThroughputBeyondLimitWarningMessage,
updateThroughputDelayedApplyWarningMessage,
getThroughputApplyDelayedMessage,
getThroughputApplyShortDelayMessage,
getThroughputApplyLongDelayMessage,
getToolTipContainer,
conflictResolutionCustomToolTip,
changeFeedPolicyToolTip,
conflictResolutionLwwTooltip,
mongoIndexingPolicyDisclaimer,
mongoIndexingPolicyAADError,
mongoIndexTransformationRefreshingMessage,
renderMongoIndexTransformationRefreshMessage,
ManualEstimatedSpendingDisplayProps,
PriceBreakdown,
changeFeedPolicyToolTip,
conflictResolutionCustomToolTip,
conflictResolutionLwwTooltip,
getEstimatedSpendingElement,
getRuPriceBreakdown,
getThroughputApplyDelayedMessage,
getThroughputApplyLongDelayMessage,
getThroughputApplyShortDelayMessage,
getToolTipContainer,
indexingPolicynUnsavedWarningMessage,
manualToAutoscaleDisclaimerElement,
mongoIndexTransformationRefreshingMessage,
mongoIndexingPolicyAADError,
mongoIndexingPolicyDisclaimer,
renderMongoIndexTransformationRefreshMessage,
ttlWarning,
updateThroughputDelayedApplyWarningMessage,
} from "./SettingsRenderUtils";
class SettingsRenderUtilsTestComponent extends React.Component {
public render(): JSX.Element {
const estimatedSpendingColumns: IColumn[] = [
{ key: "costType", name: "", fieldName: "costType", minWidth: 100, maxWidth: 200, isResizable: true },
{ key: "hourly", name: "Hourly", fieldName: "hourly", minWidth: 100, maxWidth: 200, isResizable: true },
{ key: "daily", name: "Daily", fieldName: "daily", minWidth: 100, maxWidth: 200, isResizable: true },
{ key: "monthly", name: "Monthly", fieldName: "monthly", minWidth: 100, maxWidth: 200, isResizable: true },
];
const estimatedSpendingItems: ManualEstimatedSpendingDisplayProps[] = [
{
costType: <Text>Current Cost</Text>,
hourly: <Text>$ 1.02</Text>,
daily: <Text>$ 24.48</Text>,
monthly: <Text>$ 744.6</Text>,
},
];
const costElement: JSX.Element = <></>;
const priceBreakdown: PriceBreakdown = {
hourlyPrice: 1.02,
dailyPrice: 24.48,
@@ -52,17 +35,11 @@ class SettingsRenderUtilsTestComponent extends React.Component {
return (
<>
{getAutoPilotV3SpendElement(1000, false)}
{getAutoPilotV3SpendElement(undefined, false)}
{getAutoPilotV3SpendElement(1000, true)}
{getAutoPilotV3SpendElement(undefined, true)}
{getEstimatedSpendingElement(estimatedSpendingColumns, estimatedSpendingItems, 1000, 2, priceBreakdown, false)}
{getEstimatedSpendingElement(costElement, 1000, 2, priceBreakdown, false)}
{manualToAutoscaleDisclaimerElement}
{ttlWarning}
{indexingPolicynUnsavedWarningMessage}
{updateThroughputBeyondLimitWarningMessage}
{updateThroughputDelayedApplyWarningMessage}
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}

View File

@@ -1,10 +1,7 @@
import {
DetailsList,
DetailsListLayoutMode,
DetailsRow,
ICheckboxStyles,
IChoiceGroupStyles,
IColumn,
IDetailsColumnStyles,
IDetailsListStyles,
IDetailsRowProps,
@@ -20,7 +17,6 @@ import {
Link,
MessageBar,
MessageBarType,
SelectionMode,
Spinner,
SpinnerSize,
Stack,
@@ -28,8 +24,7 @@ import {
} from "@fluentui/react";
import * as React from "react";
import { StyleConstants, Urls } from "../../../Common/Constants";
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { hoursInAMonth } from "../../../Shared/Constants";
import {
computeRUUsagePriceHourly,
estimatedCostDisclaimer,
@@ -103,6 +98,10 @@ export const checkBoxAndInputStackProps: Partial<IStackProps> = {
tokens: { childrenGap: 10 },
};
export const relaxedSpacingStackProps: Partial<IStackProps> = {
tokens: { childrenGap: 20 },
};
export const toolTipLabelStackTokens: IStackTokens = {
childrenGap: 6,
};
@@ -174,41 +173,6 @@ export function onRenderRow(props: IDetailsRowProps): JSX.Element {
return <DetailsRow {...props} styles={transparentDetailsRowStyles} />;
}
export const getAutoPilotV3SpendElement = (
maxAutoPilotThroughputSet: number,
isDatabaseThroughput: boolean,
requestUnitsUsageCostElement?: JSX.Element
): JSX.Element => {
if (!maxAutoPilotThroughputSet) {
return <></>;
}
const resource: string = isDatabaseThroughput ? "database" : "container";
return (
<>
<Text>
Your {resource} throughput will automatically scale from{" "}
<b>
{AutoPilotUtils.getMinRUsBasedOnUserInput(maxAutoPilotThroughputSet)} RU/s (10% of max RU/s) -{" "}
{maxAutoPilotThroughputSet} RU/s
</b>{" "}
based on usage.
<br />
</Text>
{requestUnitsUsageCostElement}
<Text>
After the first {AutoPilotUtils.getStorageBasedOnUserInput(maxAutoPilotThroughputSet)} GB of data stored, the
max RU/s will be automatically upgraded based on the new storage value.
<Link href={AutopilotDocumentation.Url} target="_blank">
{" "}
Learn more
</Link>
.
</Text>
</>
);
};
export const getRuPriceBreakdown = (
throughput: number,
serverId: string,
@@ -238,8 +202,7 @@ export const getRuPriceBreakdown = (
};
export const getEstimatedSpendingElement = (
estimatedSpendingColumns: IColumn[],
estimatedSpendingItems: EstimatedSpendingDisplayProps[],
costElement: JSX.Element,
throughput: number,
numberOfRegions: number,
priceBreakdown: PriceBreakdown,
@@ -247,22 +210,25 @@ export const getEstimatedSpendingElement = (
): JSX.Element => {
const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : "";
return (
<Stack {...addMongoIndexStackProps} styles={mediumWidthStackStyles}>
<DetailsList
disableSelectionZone
items={estimatedSpendingItems}
columns={estimatedSpendingColumns}
selectionMode={SelectionMode.none}
layoutMode={DetailsListLayoutMode.justified}
onRenderRow={onRenderRow}
/>
<Text id="throughputSpendElement">
({"regions: "} {numberOfRegions}, {ruRange}
{throughput} RU/s, {priceBreakdown.currencySign}
{priceBreakdown.pricePerRu}/RU)
</Text>
<Text>
<em>{estimatedCostDisclaimer}</em>
<Stack>
<Text style={{ fontWeight: 600 }}>Cost estimate*</Text>
{costElement}
<Text style={{ fontWeight: 600, marginTop: 15 }}>How we calculate this</Text>
<Stack id="throughputSpendElement" style={{ marginTop: 5 }}>
<span>
{numberOfRegions} region{numberOfRegions > 1 && <span>s</span>}
</span>
<span>
{ruRange}
{throughput} RU/s
</span>
<span>
{priceBreakdown.currencySign}
{priceBreakdown.pricePerRu}/RU
</span>
</Stack>
<Text style={{ marginTop: 15 }}>
<em>*{estimatedCostDisclaimer}</em>
</Text>
</Stack>
);
@@ -293,14 +259,6 @@ export const indexingPolicynUnsavedWarningMessage: JSX.Element = (
</Text>
);
export const updateThroughputBeyondLimitWarningMessage: JSX.Element = (
<Text styles={infoAndToolTipTextStyle} id="updateThroughputBeyondLimitWarningMessage">
You are about to request an increase in throughput beyond the pre-allocated capacity. The service will scale out and
increase throughput for the selected container. This operation will take 1-3 business days to complete. You can
track the status of this request in Notifications.
</Text>
);
export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
You are about to request an increase in throughput beyond the pre-allocated capacity. This operation will take some
@@ -308,6 +266,61 @@ export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
</Text>
);
export const getUpdateThroughputBeyondInstantLimitMessage = (instantMaximumThroughput: number): JSX.Element => {
return (
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
Scaling up will take 4-6 hours as it exceeds what Azure Cosmos DB can instantly support currently based on your
number of physical partitions. You can increase your throughput to {instantMaximumThroughput} instantly or proceed
with this value and wait until the scale-up is completed.
</Text>
);
};
export const getUpdateThroughputBeyondSupportLimitMessage = (
instantMaximumThroughput: number,
maximumThroughput: number
): JSX.Element => {
return (
<>
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
Your request to increase throughput exceeds the pre-allocated capacity which may take longer than expected.
There are three options you can choose from to proceed:
</Text>
<ol style={{ fontSize: 14, color: "windowtext", marginTop: "5px" }}>
<li>You can instantly scale up to {instantMaximumThroughput} RU/s.</li>
{instantMaximumThroughput < maximumThroughput && (
<li>You can asynchronously scale up to any value under {maximumThroughput} RU/s in 4-6 hours.</li>
)}
<li>
Your current quota max is {maximumThroughput} RU/s. To go over this limit, you must request a quota increase
and the Azure Cosmos DB team will review.
<Link
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase"
target="_blank"
>
Learn more
</Link>
</li>
</ol>
</>
);
};
export const getUpdateThroughputBelowMinimumMessage = (minimum: number): JSX.Element => {
return (
<Text styles={infoAndToolTipTextStyle}>
You are not able to lower throughput below your current minimum of {minimum} RU/s. For more information on this
limit, please refer to our service quote documentation.
<Link
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
target="_blank"
>
Learn more
</Link>
</Text>
);
};
export const saveThroughputWarningMessage: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below
@@ -499,7 +512,11 @@ export const getTextFieldStyles = (current: isDirtyTypes, baseline: isDirtyTypes
},
});
export const getChoiceGroupStyles = (current: isDirtyTypes, baseline: isDirtyTypes): Partial<IChoiceGroupStyles> => ({
export const getChoiceGroupStyles = (
current: isDirtyTypes,
baseline: isDirtyTypes,
isHorizontal?: boolean
): Partial<IChoiceGroupStyles> => ({
flexContainer: [
{
selectors: {
@@ -516,6 +533,8 @@ export const getChoiceGroupStyles = (current: isDirtyTypes, baseline: isDirtyTyp
padding: "2px 5px",
},
},
display: isHorizontal ? "inline-flex" : "default",
columnGap: isHorizontal ? "30px" : "default",
},
],
});

View File

@@ -3,7 +3,6 @@ import ko from "knockout";
import React from "react";
import * as Constants from "../../../../Common/Constants";
import * as DataModels from "../../../../Contracts/DataModels";
import * as SharedConstants from "../../../../Shared/Constants";
import { updateUserContext } from "../../../../UserContext";
import Explorer from "../../../Explorer";
import { throughputUnit } from "../SettingsRenderUtils";
@@ -12,7 +11,6 @@ import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
describe("ScaleComponent", () => {
const nonNationalCloudContainer = new Explorer();
const targetThroughput = 6000;
const baseProps: ScaleComponentProps = {
@@ -125,11 +123,4 @@ describe("ScaleComponent", () => {
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
});
it("getThroughputWarningMessage", () => {
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
const scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
});
});

View File

@@ -1,7 +1,7 @@
import { Label, Link, MessageBar, MessageBarType, Stack, Text, TextField } from "@fluentui/react";
import { Link, MessageBar, MessageBarType, Stack, Text, TextField } from "@fluentui/react";
import * as React from "react";
import * as Constants from "../../../../Common/Constants";
import { configContext, Platform } from "../../../../ConfigContext";
import { Platform, configContext } from "../../../../ConfigContext";
import * as DataModels from "../../../../Contracts/DataModels";
import * as ViewModels from "../../../../Contracts/ViewModels";
import * as SharedConstants from "../../../../Shared/Constants";
@@ -15,7 +15,6 @@ import {
subComponentStackProps,
throughputUnit,
titleAndInputStackProps,
updateThroughputBeyondLimitWarningMessage,
} from "../SettingsRenderUtils";
import { hasDatabaseSharedThroughput } from "../SettingsUtils";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
@@ -68,16 +67,6 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
return !!enableAutoScaleCapability;
};
private getStorageCapacityTitle = (): JSX.Element => {
const capacity: string = this.props.isFixedContainer ? "Fixed" : "Unlimited";
return (
<Stack {...titleAndInputStackProps}>
<Label>Storage capacity</Label>
<Text>{capacity}</Text>
</Stack>
);
};
public getMaxRUs = (): number => {
if (userContext.isTryCosmosDBSubscription) {
return Constants.TryCosmosExperience.maxRU;
@@ -131,18 +120,6 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
return undefined;
};
public getThroughputWarningMessage = (): JSX.Element => {
const throughputExceedsBackendLimits: boolean =
this.canThroughputExceedMaximumValue() &&
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
if (throughputExceedsBackendLimits && !this.props.isFixedContainer) {
return updateThroughputBeyondLimitWarningMessage;
}
return undefined;
};
public getLongDelayMessage = (): JSX.Element => {
const matches: string[] = this.props.initialNotification?.description.match(
`Throughput update for (.*) ${throughputUnit}`
@@ -188,9 +165,10 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
spendAckChecked={false}
onScaleSaveableChange={this.props.onScaleSaveableChange}
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
getThroughputWarningMessage={this.getThroughputWarningMessage}
usageSizeInKB={this.props.collection?.usageSizeInKB()}
throughputError={this.props.throughputError}
instantMaximumThroughput={this.offer?.instantMaximumThroughput}
softAllowedMaximumThroughput={this.offer?.softAllowedMaximumThroughput}
/>
);
@@ -229,12 +207,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
{this.getInitialNotificationElement() && (
<MessageBar messageBarType={MessageBarType.warning}>{this.getInitialNotificationElement()}</MessageBar>
)}
{!this.isAutoScaleEnabled() && (
<Stack {...subComponentStackProps}>
{this.getThroughputInputComponent()}
{!this.props.database && this.getStorageCapacityTitle()}
</Stack>
)}
{!this.isAutoScaleEnabled() && <Stack {...subComponentStackProps}>{this.getThroughputInputComponent()}</Stack>}
{/* TODO: Replace link with call to the Azure Support blade */}
{this.isAutoScaleEnabled() && (

View File

@@ -310,7 +310,16 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
/>
)}
{this.isLargePartitionKeyEnabled() && <Text>Large {this.partitionKeyName.toLowerCase()} has been enabled</Text>}
{userContext.apiType === "SQL" && this.isLargePartitionKeyEnabled() && (
<Text>Large {this.partitionKeyName.toLowerCase()} has been enabled.</Text>
)}
{userContext.apiType === "SQL" &&
(this.isHierarchicalPartitionedContainer() ? (
<Text>Hierarchically partitioned container.</Text>
) : (
<Text>Non-hierarchically partitioned container.</Text>
))}
</Stack>
);
@@ -328,6 +337,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
};
public isLargePartitionKeyEnabled = (): boolean => this.props.collection.partitionKey?.version >= 2;
public isHierarchicalPartitionedContainer = (): boolean => this.props.collection.partitionKey?.kind === "MultiHash";
public render(): JSX.Element {
return (

View File

@@ -42,7 +42,8 @@ describe("ThroughputInputAutoPilotV3Component", () => {
onScaleDiscardableChange: () => {
return;
},
getThroughputWarningMessage: () => undefined,
instantMaximumThroughput: 5000,
softAllowedMaximumThroughput: 1000000,
};
it("throughput input visible", () => {

View File

@@ -3,10 +3,14 @@ import {
ChoiceGroup,
FontIcon,
IChoiceGroupOption,
IColumn,
IProgressIndicatorStyles,
ISeparatorStyles,
Label,
Link,
MessageBar,
MessageBarType,
ProgressIndicator,
Separator,
Stack,
Text,
TextField,
@@ -23,24 +27,24 @@ import { autoPilotThroughput1K } from "../../../../../Utils/AutoPilotUtils";
import { calculateEstimateNumber, usageInGB } from "../../../../../Utils/PricingUtils";
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import {
AutoscaleEstimatedSpendingDisplayProps,
PriceBreakdown,
checkBoxAndInputStackProps,
getAutoPilotV3SpendElement,
getChoiceGroupStyles,
getEstimatedSpendingElement,
getRuPriceBreakdown,
getTextFieldStyles,
getToolTipContainer,
ManualEstimatedSpendingDisplayProps,
getUpdateThroughputBelowMinimumMessage,
getUpdateThroughputBeyondInstantLimitMessage,
getUpdateThroughputBeyondSupportLimitMessage,
manualToAutoscaleDisclaimerElement,
messageBarStyles,
noLeftPaddingCheckBoxStyle,
PriceBreakdown,
relaxedSpacingStackProps,
saveThroughputWarningMessage,
titleAndInputStackProps,
transparentDetailsHeaderStyle,
} from "../../SettingsRenderUtils";
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
import { IsComponentDirtyResult, getSanitizedInputValue, isDirty } from "../../SettingsUtils";
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
export interface ThroughputInputAutoPilotV3Props {
@@ -73,16 +77,16 @@ export interface ThroughputInputAutoPilotV3Props {
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
getThroughputWarningMessage: () => JSX.Element;
usageSizeInKB: number;
throughputError?: string;
instantMaximumThroughput: number;
softAllowedMaximumThroughput: number;
}
interface ThroughputInputAutoPilotV3State {
spendAckChecked: boolean;
exceedFreeTierThroughput: boolean;
}
export class ThroughputInputAutoPilotV3Component extends React.Component<
ThroughputInputAutoPilotV3Props,
ThroughputInputAutoPilotV3State
@@ -128,7 +132,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
} else if (this.props.isAutoPilotSelected) {
if (isDirty(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)) {
isDiscardable = true;
if (AutoPilotUtils.isValidAutoPilotThroughput(this.props.maxAutoPilotThroughput)) {
if (
this.props.maxAutoPilotThroughput <= this.props.softAllowedMaximumThroughput &&
AutoPilotUtils.isValidAutoPilotThroughput(this.props.maxAutoPilotThroughput)
) {
isSaveable = true;
}
}
@@ -187,7 +194,15 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
let estimatedSpend: JSX.Element;
if (!this.props.isAutoPilotSelected) {
if (this.props.isAutoPilotSelected) {
estimatedSpend = this.getEstimatedAutoscaleSpendElement(
this.props.maxAutoPilotThroughputBaseline,
userContext.portalEnv,
regions,
multimaster,
isDirty ? this.props.maxAutoPilotThroughput : undefined
);
} else {
estimatedSpend = this.getEstimatedManualSpendElement(
// if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set...
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : this.props.throughputBaseline,
@@ -196,14 +211,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
multimaster,
isDirty ? this.props.throughput : undefined
);
} else {
estimatedSpend = this.getEstimatedAutoscaleSpendElement(
this.props.maxAutoPilotThroughputBaseline,
userContext.portalEnv,
regions,
multimaster,
isDirty ? this.props.maxAutoPilotThroughput : undefined
);
}
return estimatedSpend;
};
@@ -216,52 +223,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
newThroughput?: number
): JSX.Element => {
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true);
const estimatedSpendingColumns: IColumn[] = [
{
key: "costType",
name: "",
fieldName: "costType",
minWidth: 100,
maxWidth: 200,
isResizable: true,
styles: transparentDetailsHeaderStyle,
},
{
key: "minPerMonth",
name: "Min Per Month",
fieldName: "minPerMonth",
minWidth: 100,
maxWidth: 200,
isResizable: true,
styles: transparentDetailsHeaderStyle,
},
{
key: "maxPerMonth",
name: "Max Per Month",
fieldName: "maxPerMonth",
minWidth: 100,
maxWidth: 200,
isResizable: true,
styles: transparentDetailsHeaderStyle,
},
];
const estimatedSpendingItems: AutoscaleEstimatedSpendingDisplayProps[] = [
{
costType: <Text>Current Cost</Text>,
minPerMonth: (
<Text>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)}
</Text>
),
maxPerMonth: (
<Text>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}
</Text>
),
},
];
if (newThroughput) {
const newThroughputCostElement = (): JSX.Element => {
const newPrices: PriceBreakdown = getRuPriceBreakdown(
newThroughput,
serverId,
@@ -269,37 +232,40 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
isMultimaster,
true
);
estimatedSpendingItems.unshift({
costType: (
<Text>
<b>Updated Cost</b>
</Text>
),
minPerMonth: (
<Text>
<b>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)}
</b>
</Text>
),
maxPerMonth: (
<Text>
<b>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}
</b>
</Text>
),
});
}
return (
<div>
<Text style={{ fontWeight: 600 }}>Updated cost per month</Text>
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
<Text style={{ width: "50%" }}>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)} min
</Text>
<Text style={{ width: "50%" }}>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} max
</Text>
</Stack>
</div>
);
};
return getEstimatedSpendingElement(
estimatedSpendingColumns,
estimatedSpendingItems,
newThroughput ?? throughput,
numberOfRegions,
prices,
true
);
const costElement = (): JSX.Element => {
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true);
return (
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
{newThroughput && newThroughputCostElement()}
<Text style={{ fontWeight: 600 }}>Current cost per month</Text>
<Stack horizontal style={{ marginTop: 5 }}>
<Text style={{ width: "50%" }}>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)} min
</Text>
<Text style={{ width: "50%" }}>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} max
</Text>
</Stack>
</Stack>
);
};
return getEstimatedSpendingElement(costElement(), newThroughput ?? throughput, numberOfRegions, prices, true);
};
private getEstimatedManualSpendElement = (
@@ -310,122 +276,55 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
newThroughput?: number
): JSX.Element => {
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, false);
const estimatedSpendingColumns: IColumn[] = [
{
key: "costType",
name: "",
fieldName: "costType",
minWidth: 100,
maxWidth: 200,
isResizable: true,
styles: transparentDetailsHeaderStyle,
},
{
key: "hourly",
name: "Hourly",
fieldName: "hourly",
minWidth: 100,
maxWidth: 200,
isResizable: true,
styles: transparentDetailsHeaderStyle,
},
{
key: "daily",
name: "Daily",
fieldName: "daily",
minWidth: 100,
maxWidth: 200,
isResizable: true,
styles: transparentDetailsHeaderStyle,
},
{
key: "monthly",
name: "Monthly",
fieldName: "monthly",
minWidth: 100,
maxWidth: 200,
isResizable: true,
styles: transparentDetailsHeaderStyle,
},
];
const estimatedSpendingItems: ManualEstimatedSpendingDisplayProps[] = [
{
costType: <Text>Current Cost</Text>,
hourly: (
<Text>
{prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}
</Text>
),
daily: (
<Text>
{prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}
</Text>
),
monthly: (
<Text>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}
</Text>
),
},
];
if (newThroughput) {
const newThroughputCostElement = (): JSX.Element => {
const newPrices: PriceBreakdown = getRuPriceBreakdown(
newThroughput,
serverId,
numberOfRegions,
isMultimaster,
false
true
);
estimatedSpendingItems.unshift({
costType: (
<Text>
<b>Updated Cost</b>
</Text>
),
hourly: (
<Text>
<b>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}
</b>
</Text>
),
daily: (
<Text>
<b>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}
</b>
</Text>
),
monthly: (
<Text>
<b>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}
</b>
</Text>
),
});
}
return (
<div>
<Text style={{ fontWeight: 600 }}>Updated cost per month</Text>
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
<Text style={{ width: "33%" }}>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}/hr
</Text>
<Text style={{ width: "33%" }}>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}/day
</Text>
<Text style={{ width: "33%" }}>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}/mo
</Text>
</Stack>
</div>
);
};
return getEstimatedSpendingElement(
estimatedSpendingColumns,
estimatedSpendingItems,
newThroughput ?? throughput,
numberOfRegions,
prices,
false
);
};
const costElement = (): JSX.Element => {
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true);
return (
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
{newThroughput && newThroughputCostElement()}
<Text style={{ fontWeight: 600 }}>Current cost per month</Text>
<Stack horizontal style={{ marginTop: 5 }}>
<Text style={{ width: "33%" }}>
{prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}/hr
</Text>
<Text style={{ width: "33%" }}>
{prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}/day
</Text>
<Text style={{ width: "33%" }}>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}/mo
</Text>
</Stack>
</Stack>
);
};
private getAutoPilotUsageCost = (): JSX.Element => {
if (!this.props.maxAutoPilotThroughput) {
return <></>;
}
return getAutoPilotV3SpendElement(
this.props.maxAutoPilotThroughput,
false /* isDatabaseThroughput */,
!this.props.isEmulator ? this.getRequestUnitsUsageCost() : <></>
);
return getEstimatedSpendingElement(costElement(), newThroughput ?? throughput, numberOfRegions, prices, false);
};
private onAutoPilotThroughputChange = (
@@ -512,7 +411,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
onChange={this.onChoiceGroupChange}
required={this.props.showAsMandatory}
ariaLabelledBy={labelId}
styles={getChoiceGroupStyles(this.props.wasAutopilotOriginallySet, this.props.isAutoPilotSelected)}
styles={getChoiceGroupStyles(this.props.wasAutopilotOriginallySet, this.props.isAutoPilotSelected, true)}
/>
</Stack>
);
@@ -521,97 +420,266 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
private onSpendAckChecked = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean): void =>
this.setState({ spendAckChecked: checked });
private renderAutoPilotInput = (): JSX.Element => (
<>
<Text>
Provision maximum RU/s required by this resource. Estimate your required RU/s with
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
{` capacity calculator`}
</Link>
</Text>
<TextField
label="Max RU/s"
required
type="number"
id="autopilotInput"
key="auto pilot throughput input"
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
disabled={this.overrideWithProvisionedThroughputSettings()}
step={AutoPilotUtils.autoPilotIncrementStep}
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
onChange={this.onAutoPilotThroughputChange}
min={autoPilotThroughput1K}
errorMessage={this.props.throughputError}
private getStorageCapacityTitle = (): JSX.Element => {
const capacity: string = this.props.isFixed ? "Fixed" : "Unlimited";
return (
<Stack {...titleAndInputStackProps}>
<Label>Storage capacity</Label>
<Text>{capacity}</Text>
</Stack>
);
};
private thoughputRangeSeparatorStyles: Partial<ISeparatorStyles> = {
root: [
{
selectors: {
"::before": {
backgroundColor: "rgb(200, 200, 200)",
height: "3px",
marginTop: "-1px",
},
},
},
],
};
private currentThroughputValue = (): number => {
return this.props.isAutoPilotSelected
? this.props.maxAutoPilotThroughput
: this.overrideWithAutoPilotSettings()
? this.props.maxAutoPilotThroughputBaseline
: this.props.throughput;
};
private getCurrentRuRange = (): "below" | "instant" | "delayed" | "requireSupport" => {
if (this.currentThroughputValue() < this.props.minimum) {
return "below";
}
if (
this.currentThroughputValue() >= this.props.minimum &&
this.currentThroughputValue() <= this.props.instantMaximumThroughput
) {
return "instant";
}
if (this.currentThroughputValue() > this.props.softAllowedMaximumThroughput) {
return "requireSupport";
}
return "delayed";
};
private getRuThermometerStyles = (): Partial<IProgressIndicatorStyles> => ({
progressBar: [
{
backgroundColor:
this.getCurrentRuRange() === "instant"
? "rgb(0, 120, 212)"
: this.getCurrentRuRange() === "delayed"
? "rgb(255 216 109)"
: "rgb(251, 217, 203)",
},
],
});
private getRuThermometerPercentValue = (): number => {
let percentValue: number;
const currentRus = this.currentThroughputValue();
switch (this.getCurrentRuRange()) {
case "below":
percentValue = 0;
break;
case "instant": {
const percentOfInstantRange: number = currentRus / this.props.instantMaximumThroughput;
percentValue = percentOfInstantRange * 0.34;
break;
}
case "delayed": {
const adjustedMax = this.props.softAllowedMaximumThroughput - this.props.instantMaximumThroughput;
const adjustedRus = currentRus - this.props.instantMaximumThroughput;
const percentOfDelayedRange = adjustedRus / adjustedMax;
const adjustedPercent = percentOfDelayedRange * 0.66;
percentValue = adjustedPercent + 0.34;
break;
}
default:
// over maximum
percentValue = 1;
}
return percentValue;
};
private getRUThermometer = (): JSX.Element => (
<Stack>
<Stack horizontal>
<Stack.Item style={{ width: "34%" }}>
<span>{this.props.minimum.toLocaleString()}</span>
</Stack.Item>
<Stack.Item style={{ width: "66%" }}>
<span style={{ float: "left", transform: "translateX(-50%)" }}>
{this.props.instantMaximumThroughput.toLocaleString()}
</span>
<span style={{ float: "right" }}>{this.props.softAllowedMaximumThroughput.toLocaleString()}</span>
</Stack.Item>
</Stack>
<ProgressIndicator
barHeight={20}
percentComplete={this.getRuThermometerPercentValue()}
styles={this.getRuThermometerStyles()}
/>
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
{this.minRUperGBSurvey()}
{this.props.spendAckVisible && (
<Checkbox
id="spendAckCheckBox"
styles={noLeftPaddingCheckBoxStyle}
label={this.props.spendAckText}
checked={this.state.spendAckChecked}
onChange={this.onSpendAckChecked}
<Stack horizontal>
<Stack.Item style={{ width: "34%", paddingRight: "5px" }}>
<Separator styles={this.thoughputRangeSeparatorStyles}>Instant</Separator>
</Stack.Item>
<Stack.Item style={{ width: "66%", paddingLeft: "5px" }}>
<Separator styles={this.thoughputRangeSeparatorStyles}>4-6 hrs</Separator>
</Stack.Item>
</Stack>
</Stack>
);
private showThroughputWarning = (): boolean => {
return (
this.currentThroughputValue() > this.props.instantMaximumThroughput ||
this.currentThroughputValue() < this.props.minimum
);
};
private getThroughputWarningMessageText = (): JSX.Element => {
switch (this.getCurrentRuRange()) {
case "below":
return getUpdateThroughputBelowMinimumMessage(this.props.minimum);
case "delayed":
return getUpdateThroughputBeyondInstantLimitMessage(this.props.instantMaximumThroughput);
case "requireSupport":
return getUpdateThroughputBeyondSupportLimitMessage(
this.props.instantMaximumThroughput,
this.props.softAllowedMaximumThroughput
);
default:
return <></>;
}
};
private getThroughputWarningMessageBar = (): JSX.Element => {
const isSevereWarning: boolean =
this.currentThroughputValue() > this.props.softAllowedMaximumThroughput ||
this.currentThroughputValue() < this.props.minimum;
return (
<MessageBar messageBarType={isSevereWarning ? MessageBarType.severeWarning : MessageBarType.warning}>
{this.getThroughputWarningMessageText()}
</MessageBar>
);
};
private getThroughputTextField = (): JSX.Element => (
<>
{this.props.isAutoPilotSelected ? (
<TextField
label="Maximum RU/s required by this resource"
required
type="number"
id="autopilotInput"
key="auto pilot throughput input"
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
disabled={this.overrideWithProvisionedThroughputSettings()}
step={AutoPilotUtils.autoPilotIncrementStep}
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
onChange={this.onAutoPilotThroughputChange}
min={autoPilotThroughput1K}
onGetErrorMessage={(value: string) => {
const sanitizedValue = getSanitizedInputValue(value);
return sanitizedValue % 1000
? "Throughput value must be in increments of 1000"
: this.props.throughputError;
}}
validateOnLoad={false}
/>
) : (
<TextField
required
type="number"
id="throughputInput"
key="provisioned throughput input"
styles={getTextFieldStyles(this.props.throughput, this.props.throughputBaseline)}
disabled={this.overrideWithAutoPilotSettings()}
step={this.step}
value={
this.overrideWithAutoPilotSettings()
? this.props.maxAutoPilotThroughputBaseline?.toString()
: this.props.throughput?.toString()
}
onChange={this.onThroughputChange}
min={this.props.minimum}
errorMessage={this.props.throughputError}
/>
)}
{this.props.isFixed && <p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>}
</>
);
private renderThroughputInput = (): JSX.Element => (
<Stack {...titleAndInputStackProps}>
<Text>
Estimate your required throughput with
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
{` capacity calculator`} <FontIcon iconName="NavigateExternalInline" />
</Link>
</Text>
<TextField
required
type="number"
id="throughputInput"
key="provisioned throughput input"
styles={getTextFieldStyles(this.props.throughput, this.props.throughputBaseline)}
disabled={this.overrideWithAutoPilotSettings()}
step={this.step}
value={
this.overrideWithAutoPilotSettings()
? this.props.maxAutoPilotThroughputBaseline?.toString()
: this.props.throughput?.toString()
}
onChange={this.onThroughputChange}
min={this.props.minimum}
errorMessage={this.props.throughputError}
/>
{this.state.exceedFreeTierThroughput && (
<MessageBar
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
styles={messageBarStyles}
>
{`Billing will apply if you provision more than ${SharedConstants.FreeTierLimits.RU} RU/s of manual throughput, or if the resource scales beyond ${SharedConstants.FreeTierLimits.RU} RU/s with autoscale.`}
</MessageBar>
)}
{this.props.getThroughputWarningMessage() && (
<MessageBar
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
styles={messageBarStyles}
>
{this.props.getThroughputWarningMessage()}
</MessageBar>
)}
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
{this.minRUperGBSurvey()}
{this.props.spendAckVisible && (
<Checkbox
id="spendAckCheckBox"
styles={noLeftPaddingCheckBoxStyle}
label={this.props.spendAckText}
checked={this.state.spendAckChecked}
onChange={this.onSpendAckChecked}
/>
)}
<br />
{this.props.isFixed && <p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>}
private renderThroughputComponent = (): JSX.Element => (
<Stack horizontal>
<Stack.Item style={{ width: "70%", maxWidth: "700px" }}>
<Stack {...relaxedSpacingStackProps} style={{ paddingRight: "50px" }}>
{this.getThroughputTextField()}
{this.props.instantMaximumThroughput && (
<Stack>
{this.getRUThermometer()}
{this.showThroughputWarning() && this.getThroughputWarningMessageBar()}
</Stack>
)}
{this.props.isAutoPilotSelected ? (
<Text style={{ marginTop: "40px" }}>
Based on usage, your {this.props.collectionName ? "container" : "database"} throughput will scale from{" "}
<b>
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)} RU/s (10% of max RU/s) -{" "}
{this.props.maxAutoPilotThroughput} RU/s
</b>
<br />
</Text>
) : (
<>
{this.state.exceedFreeTierThroughput && (
<MessageBar
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
styles={messageBarStyles}
style={{ marginTop: "40px" }}
>
{`Billing will apply if you provision more than ${SharedConstants.FreeTierLimits.RU} RU/s of manual throughput, or if the resource scales beyond ${SharedConstants.FreeTierLimits.RU} RU/s with autoscale.`}
</MessageBar>
)}
</>
)}
{!this.overrideWithProvisionedThroughputSettings() && (
<Text>
Estimate your required RU/s with
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
{` capacity calculator`} <FontIcon iconName="NavigateExternalInline" />
</Link>
</Text>
)}
{this.minRUperGBSurvey()}
{this.props.spendAckVisible && (
<Checkbox
id="spendAckCheckBox"
styles={noLeftPaddingCheckBoxStyle}
label={this.props.spendAckText}
checked={this.state.spendAckChecked}
onChange={this.onSpendAckChecked}
/>
)}
{this.props.isFixed && (
<p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>
)}
{this.props.collectionName && (
<Stack.Item style={{ marginTop: "40px" }}>{this.getStorageCapacityTitle()}</Stack.Item>
)}
</Stack>
</Stack.Item>
<Stack.Item style={{ width: "30%", maxWidth: "300px" }}>
{!this.props.isEmulator ? this.getRequestUnitsUsageCost() : <></>}
</Stack.Item>
</Stack>
);
@@ -624,7 +692,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
return (
<>
{warningMessage && (
<MessageBar messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}>
<MessageBar
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
role="alert"
>
{warningMessage}
</MessageBar>
)}
@@ -638,7 +709,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
{this.renderWarningMessage()}
{this.renderThroughputModeChoices()}
{this.props.isAutoPilotSelected ? this.renderAutoPilotInput() : this.renderThroughputInput()}
{this.renderThroughputComponent()}
</Stack>
);
}

View File

@@ -28,6 +28,8 @@ exports[`ConflictResolutionComponent Path text field displayed 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": undefined,
@@ -100,6 +102,8 @@ exports[`ConflictResolutionComponent Sproc text field displayed 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",

View File

@@ -39,7 +39,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
canExceedMaximumValue={true}
collectionName="test"
databaseName="test"
getThroughputWarningMessage={[Function]}
isAutoPilotSelected={false}
isEmulator={false}
isEnabled={true}
@@ -60,20 +59,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
usageSizeInKB={100}
wasAutopilotOriginallySet={true}
/>
<Stack
tokens={
Object {
"childrenGap": 5,
}
}
>
<StyledLabelBase>
Storage capacity
</StyledLabelBase>
<Text>
Unlimited
</Text>
</Stack>
</Stack>
</Stack>
`;

View File

@@ -40,6 +40,8 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
@@ -106,6 +108,8 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
@@ -168,6 +172,8 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
@@ -218,7 +224,10 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
<Text>
Large
partition key
has been enabled
has been enabled.
</Text>
<Text>
Non-hierarchically partitioned container.
</Text>
</Stack>
</Stack>
@@ -264,6 +273,8 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
@@ -330,6 +341,8 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
@@ -382,6 +395,8 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": undefined,
@@ -445,6 +460,8 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
@@ -495,7 +512,10 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
<Text>
Large
partition key
has been enabled
has been enabled.
</Text>
<Text>
Non-hierarchically partitioned container.
</Text>
</Stack>
</Stack>
@@ -541,6 +561,8 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
@@ -607,6 +629,8 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
@@ -659,6 +683,8 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
@@ -734,7 +760,10 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
<Text>
Large
partition key
has been enabled
has been enabled.
</Text>
<Text>
Non-hierarchically partitioned container.
</Text>
</Stack>
</Stack>
@@ -780,6 +809,8 @@ exports[`SubSettingsComponent renders 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
@@ -846,6 +877,8 @@ exports[`SubSettingsComponent renders 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
@@ -898,6 +931,8 @@ exports[`SubSettingsComponent renders 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
@@ -986,6 +1021,8 @@ exports[`SubSettingsComponent renders 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
@@ -1036,7 +1073,10 @@ exports[`SubSettingsComponent renders 1`] = `
<Text>
Large
partition key
has been enabled
has been enabled.
</Text>
<Text>
Non-hierarchically partitioned container.
</Text>
</Stack>
</Stack>
@@ -1082,6 +1122,8 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": undefined,
@@ -1123,6 +1165,8 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
@@ -1175,6 +1219,8 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
@@ -1263,6 +1309,8 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
Object {
"flexContainer": Array [
Object {
"columnGap": "default",
"display": "default",
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
@@ -1313,7 +1361,10 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
<Text>
Large
partition key
has been enabled
has been enabled.
</Text>
<Text>
Non-hierarchically partitioned container.
</Text>
</Stack>
</Stack>

View File

@@ -35,6 +35,7 @@ exports[`SettingsComponent renders 1`] = `
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": Object {
"maxTimeout": 5000,
"minTimeout": 5000,
@@ -111,6 +112,7 @@ exports[`SettingsComponent renders 1`] = `
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": Object {
"maxTimeout": 5000,
"minTimeout": 5000,

View File

@@ -2,154 +2,60 @@
exports[`SettingsUtils functions render 1`] = `
<Fragment>
<Text>
Your
container
throughput will automatically scale from
<b>
100
RU/s (10% of max RU/s) -
1000
RU/s
</b>
based on usage.
<br />
</Text>
<Text>
After the first
10
GB of data stored, the max RU/s will be automatically upgraded based on the new storage value.
<StyledLinkBase
href="https://aka.ms/cosmos-autoscale-info"
target="_blank"
>
Learn more
</StyledLinkBase>
.
</Text>
<Text>
Your
database
throughput will automatically scale from
<b>
100
RU/s (10% of max RU/s) -
1000
RU/s
</b>
based on usage.
<br />
</Text>
<Text>
After the first
10
GB of data stored, the max RU/s will be automatically upgraded based on the new storage value.
<StyledLinkBase
href="https://aka.ms/cosmos-autoscale-info"
target="_blank"
>
Learn more
</StyledLinkBase>
.
</Text>
<Stack
styles={
Object {
"root": Object {
"width": 600,
},
}
}
tokens={
Object {
"childrenGap": 10,
}
}
>
<StyledWithViewportComponent
columns={
Array [
Object {
"fieldName": "costType",
"isResizable": true,
"key": "costType",
"maxWidth": 200,
"minWidth": 100,
"name": "",
},
Object {
"fieldName": "hourly",
"isResizable": true,
"key": "hourly",
"maxWidth": 200,
"minWidth": 100,
"name": "Hourly",
},
Object {
"fieldName": "daily",
"isResizable": true,
"key": "daily",
"maxWidth": 200,
"minWidth": 100,
"name": "Daily",
},
Object {
"fieldName": "monthly",
"isResizable": true,
"key": "monthly",
"maxWidth": 200,
"minWidth": 100,
"name": "Monthly",
},
]
}
disableSelectionZone={true}
items={
Array [
Object {
"costType": <Text>
Current Cost
</Text>,
"daily": <Text>
$ 24.48
</Text>,
"hourly": <Text>
$ 1.02
</Text>,
"monthly": <Text>
$ 744.6
</Text>,
},
]
}
layoutMode={1}
onRenderRow={[Function]}
selectionMode={0}
/>
<Stack>
<Text
id="throughputSpendElement"
style={
Object {
"fontWeight": 600,
}
}
>
(
regions:
2
,
1000
RU/s,
¥
0.00051
/RU)
Cost estimate*
</Text>
<Text>
<Text
style={
Object {
"fontWeight": 600,
"marginTop": 15,
}
}
>
How we calculate this
</Text>
<Stack
id="throughputSpendElement"
style={
Object {
"marginTop": 5,
}
}
>
<span>
2
region
<span>
s
</span>
</span>
<span>
1000
RU/s
</span>
<span>
¥
0.00051
/RU
</span>
</Stack>
<Text
style={
Object {
"marginTop": 15,
}
}
>
<em>
*
This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account
</em>
</Text>
@@ -205,19 +111,6 @@ exports[`SettingsUtils functions render 1`] = `
>
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
</Text>
<Text
id="updateThroughputBeyondLimitWarningMessage"
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
}
>
You are about to request an increase in throughput beyond the pre-allocated capacity. The service will scale out and increase throughput for the selected container. This operation will take 1-3 business days to complete. You can track the status of this request in Notifications.
</Text>
<Text
id="updateThroughputDelayedApplyWarningMessage"
styles={

View File

@@ -46,10 +46,13 @@ export class TabComponent extends React.Component<TabComponentProps> {
}
let className = "toggleSwitch";
let ariaselected;
if (index === this.props.currentTabIndex) {
className += " selectedToggle";
ariaselected = true;
} else {
className += " unselectedToggle";
ariaselected = false;
}
return (
@@ -57,9 +60,10 @@ export class TabComponent extends React.Component<TabComponentProps> {
<AccessibleElement
as="span"
className={className}
role="presentation"
role="tab"
onActivated={() => this.setActiveTab(index)}
aria-label={`Select tab: ${tab.title}`}
aria-selected={ariaselected}
>
{tab.title}
</AccessibleElement>
@@ -77,7 +81,11 @@ export class TabComponent extends React.Component<TabComponentProps> {
return (
<div className="tabComponentContainer">
{!this.props.hideHeader && <div className="tabs tabSwitch">{this.renderTabTitles()}</div>}
{!this.props.hideHeader && (
<div className="tabs tabSwitch" role="tablist">
{this.renderTabTitles()}
</div>
)}
<div className={className}>{currentTabContent.render()}</div>
</div>
);

View File

@@ -23,12 +23,12 @@ describe("ThroughputInput Pane", () => {
});
it("should switch mode properly", () => {
wrapper.find('[aria-label="Manual mode"]').simulate("change");
wrapper.find('[aria-label="Manual database throughput"]').simulate("change");
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe(
"Container throughput (400 - unlimited RU/s)"
);
wrapper.find('[aria-label="Autoscale mode"]').simulate("change");
wrapper.find('[aria-label="Autoscale database throughput"]').simulate("change");
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe("Container throughput (autoscale)");
});
});

View File

@@ -185,36 +185,48 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
</Stack>
<Stack horizontal verticalAlign="center">
<input
className="throughputInputRadioBtn"
aria-label="Autoscale mode"
aria-required={true}
checked={isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Autoscale")}
/>
<span className="throughputInputRadioBtnLabel">Autoscale</span>
<div role="radiogroup">
<input
id="Autoscale-input"
className="throughputInputRadioBtn"
aria-label="Autoscale database throughput"
aria-required={true}
checked={isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Autoscale")}
/>
<label htmlFor="Autoscale-input" className="throughputInputRadioBtnLabel">
Autoscale
</label>
<input
className="throughputInputRadioBtn"
aria-label="Manual mode"
checked={!isAutoscaleSelected}
type="radio"
aria-required={true}
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Manual")}
/>
<span className="throughputInputRadioBtnLabel">Manual</span>
<input
id="Manual-input"
className="throughputInputRadioBtn"
aria-label="Manual database throughput"
checked={!isAutoscaleSelected}
type="radio"
aria-required={true}
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Manual")}
/>
<label className="throughputInputRadioBtnLabel" htmlFor="Manual-input">
Manual
</label>
</div>
</Stack>
{isAutoscaleSelected && (
<Stack className="throughputInputSpacing">
<Text variant="small" aria-label="ruDescription">
<Text variant="small" aria-label="capacity calculator of azure cosmos db">
Estimate your required RU/s with{" "}
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/" aria-label="ruDescription">
<Link
target="_blank"
href="https://cosmos.azure.com/capacitycalculator/"
aria-label="capacity calculator of azure cosmos db"
>
capacity calculator
</Link>
.
@@ -238,7 +250,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
step={AutoPilotUtils.autoPilotIncrementStep}
min={AutoPilotUtils.autoPilotThroughput1K}
value={throughput.toString()}
aria-label="Max request units per second"
ariaLabel={`${isDatabase ? "Database" : getCollectionName()} max RU/s`}
required={true}
errorMessage={throughputError}
/>

View File

@@ -344,13 +344,13 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
onMouseLeave={[Function]}
>
<StyledIconBase
ariaLabel="Info"
ariaLabel="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
className="panelInfoIcon"
iconName="Info"
tabIndex={0}
>
<IconBase
ariaLabel="Info"
ariaLabel="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
className="panelInfoIcon"
iconName="Info"
styles={[Function]}
@@ -630,7 +630,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
}
>
<i
aria-label="Info"
aria-label="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
className="panelInfoIcon root-57"
data-icon-name="Info"
role="img"
@@ -654,40 +654,45 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
<div
className="ms-Stack css-58"
>
<input
aria-label="Autoscale mode"
aria-required={true}
checked={true}
className="throughputInputRadioBtn"
<div
key=".0:$.0"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="throughputInputRadioBtnLabel"
key=".0:$.1"
role="radiogroup"
>
Autoscale
</span>
<input
aria-label="Manual mode"
aria-required={true}
checked={false}
className="throughputInputRadioBtn"
key=".0:$.2"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="throughputInputRadioBtnLabel"
key=".0:$.3"
>
Manual
</span>
<input
aria-label="Autoscale database throughput"
aria-required={true}
checked={true}
className="throughputInputRadioBtn"
id="Autoscale-input"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<label
className="throughputInputRadioBtnLabel"
htmlFor="Autoscale-input"
>
Autoscale
</label>
<input
aria-label="Manual database throughput"
aria-required={true}
checked={false}
className="throughputInputRadioBtn"
id="Manual-input"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<label
className="throughputInputRadioBtnLabel"
htmlFor="Manual-input"
>
Manual
</label>
</div>
</div>
</Stack>
<Stack
@@ -697,23 +702,23 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
className="ms-Stack throughputInputSpacing css-59"
>
<Text
aria-label="ruDescription"
aria-label="capacity calculator of azure cosmos db"
key=".0:$.0"
variant="small"
>
<span
aria-label="ruDescription"
aria-label="capacity calculator of azure cosmos db"
className="css-54"
>
Estimate your required RU/s with
<StyledLinkBase
aria-label="ruDescription"
aria-label="capacity calculator of azure cosmos db"
href="https://cosmos.azure.com/capacitycalculator/"
target="_blank"
>
<LinkBase
aria-label="ruDescription"
aria-label="capacity calculator of azure cosmos db"
href="https://cosmos.azure.com/capacitycalculator/"
styles={[Function]}
target="_blank"
@@ -992,7 +997,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
}
>
<a
aria-label="ruDescription"
aria-label="capacity calculator of azure cosmos db"
className="ms-Link root-60"
href="https://cosmos.azure.com/capacitycalculator/"
onClick={[Function]}
@@ -1331,13 +1336,13 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
onMouseLeave={[Function]}
>
<StyledIconBase
ariaLabel="Info"
ariaLabel="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
className="panelInfoIcon"
iconName="Info"
tabIndex={0}
>
<IconBase
ariaLabel="Info"
ariaLabel="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
className="panelInfoIcon"
iconName="Info"
styles={[Function]}
@@ -1617,7 +1622,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
}
>
<i
aria-label="Info"
aria-label="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
className="panelInfoIcon root-57"
data-icon-name="Info"
role="img"
@@ -1635,7 +1640,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
</div>
</Stack>
<StyledTextFieldBase
aria-label="Max request units per second"
ariaLabel="Container max RU/s"
errorMessage=""
id="autoscaleRUValueField"
key=".0:$.2"
@@ -1658,7 +1663,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
value="4000"
>
<TextFieldBase
aria-label="Max request units per second"
ariaLabel="Container max RU/s"
deferredValidationTime={200}
errorMessage=""
id="autoscaleRUValueField"
@@ -1956,6 +1961,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
>
<input
aria-invalid={false}
aria-label="Container max RU/s"
className="ms-TextField-field field-64"
id="autoscaleRUValueField"
min={1000}

View File

@@ -195,7 +195,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
</div>
{node.children && (
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
<div className="nodeChildren" data-test={node.label}>
<div className="nodeChildren" data-test={node.label} role="group">
{TreeNodeComponent.getSortedChildren(node).map((childNode: TreeNode) => (
<TreeNodeComponent
key={`${childNode.label}-${generation + 1}-${childNode.timestamp}`}

View File

@@ -100,6 +100,7 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
<div
className="nodeChildren"
data-test="label"
role="group"
>
<TreeNodeComponent
generation={3}
@@ -251,6 +252,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
<div
className="nodeChildren"
data-test="label"
role="group"
>
<TreeNodeComponent
generation={13}
@@ -352,6 +354,7 @@ exports[`TreeNodeComponent renders loading icon 1`] = `
<div
className="nodeChildren"
data-test="label"
role="group"
/>
</AnimateHeight>
</div>
@@ -465,6 +468,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
<div
className="nodeChildren"
data-test="label"
role="group"
>
<TreeNodeComponent
generation={13}
@@ -594,6 +598,7 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
<div
className="nodeChildren"
data-test="label"
role="group"
>
<TreeNodeComponent
generation={3}

View File

@@ -3,7 +3,7 @@
.treeComponent {
.nodeItem {
&:focus {
outline: 1px dashed @AccentMedium;
outline: 2px @AccentMedium;
}
.treeNodeHeader {

View File

@@ -133,7 +133,6 @@ describe("ContainerSampleGenerator", () => {
} as DatabaseAccount,
});
// Rejects with error that contains experience
expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
});

View File

@@ -22,10 +22,17 @@ export class ContainerSampleGenerator {
/**
* Factory function to load the json data file
*/
public static async createSampleGeneratorAsync(container: Explorer): Promise<ContainerSampleGenerator> {
public static async createSampleGeneratorAsync(
container: Explorer,
isCopilot?: boolean
): Promise<ContainerSampleGenerator> {
const generator = new ContainerSampleGenerator(container);
let dataFileContent: any;
if (userContext.apiType === "Gremlin") {
if (isCopilot) {
dataFileContent = await import(
/* webpackChunkName: "queryCopilotSampleData" */ "../../../sampleData/queryCopilotSampleData.json"
);
} else if (userContext.apiType === "Gremlin") {
dataFileContent = await import(
/* webpackChunkName: "gremlinSampleJsonData" */ "../../../sampleData/gremlinSampleData.json"
);

View File

@@ -93,7 +93,8 @@ export default class Explorer {
dataExplorerArea: Constants.Areas.ResourceTree,
});
this._isInitializingNotebooks = false;
this.phoenixClient = new PhoenixClient();
this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
useNotebook.subscribe(
() => this.refreshCommandBarButtons(),
(state) => state.isNotebooksEnabledForAccount
@@ -185,9 +186,7 @@ export default class Explorer {
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
}
if (userContext.apiType !== "Postgres") {
this.refreshExplorer();
}
this.refreshExplorer();
}
public async initiateAndRefreshNotebookList(): Promise<void> {
@@ -353,7 +352,7 @@ export default class Explorer {
(notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined))
) {
const provisionData: IProvisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint,
poolId: PoolIdType.DefaultPoolId,
};
const connectionStatus: ContainerConnectionInfo = {
@@ -578,7 +577,7 @@ export default class Explorer {
try {
await Promise.all(
databasesToLoad.map(async (database: ViewModels.Database) => {
await database.loadCollections();
await database.loadCollections(true);
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id());
if (isNewDatabase) {
database.expandDatabase();
@@ -1058,6 +1057,10 @@ export default class Explorer {
title = "Cassandra Shell";
break;
case ViewModels.TerminalKind.Postgres:
title = "PSQL Shell";
break;
default:
throw new Error("Terminal kind: ${kind} not supported");
}
@@ -1244,9 +1247,11 @@ export default class Explorer {
}
public async refreshExplorer(): Promise<void> {
userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases();
if (userContext.apiType !== "Postgres") {
userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases();
}
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount

View File

@@ -729,6 +729,7 @@ export class D3ForceGraph implements GraphRenderer {
var iconGroup = newNodes
.append("g")
.attr("class", "iconContainer")
.attr("role", "group")
.attr("tabindex", 0)
.attr("aria-label", (d: D3Node) => {
return this.retrieveNodeCaption(d);

View File

@@ -6,9 +6,9 @@ import * as React from "react";
import LoadGraphIcon from "../../../../images/LoadGraph.png";
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
import * as Constants from "../../../Common/Constants";
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { InputProperty } from "../../../Contracts/ViewModels";
@@ -182,7 +182,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
public static readonly WITHOUT_STEP_ARGS_MAX_CHARS = 10000; // maximums char size of the without() step parameter
public static readonly ROOT_LIST_PAGE_SIZE = 100;
private static readonly MAX_LATEST_QUERIES = 10;
private static readonly MAX_RESULT_SIZE = 10000;
// TODO This prevents loading too much data in GraphExplorer and d3. A better is to cap the size of the result, rather than the number of items in the array.
private static readonly MAX_RESULT_SIZE = 100_000;
private static readonly TAB_INDEX_JSON = 0;
private static readonly TAB_INDEX_GRAPH = 1;

View File

@@ -40,6 +40,7 @@ export class GraphVizComponent extends React.Component<GraphVizComponentProps> {
{/* svg load more icon inlined as-is here: remove the style="fill:#374649;" so we can override it */}
<svg
role="img"
aria-label="graph"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
@@ -135,6 +136,7 @@ export class GraphVizComponent extends React.Component<GraphVizComponentProps> {
<g id="triangleRight">
<svg
role="img"
aria-label="graph"
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -31,7 +31,7 @@ export class GremlinClient {
public client: GremlinSimpleClient;
public pendingResults: Map<string, PendingResultData>; // public for testing purposes
private maxResultSize: number;
private static readonly PENDING_REQUEST_TIMEOUT_MS = 6 /* minutes */ * 60 /* seconds */ * 1000 /* ms */;
private static readonly PENDING_REQUEST_TIMEOUT_MS = 1 /* hour */ * 3_600 /* seconds */ * 1_000 /* ms */;
private static readonly TIMEOUT_ERROR_MSG = `Pending request timed out (${GremlinClient.PENDING_REQUEST_TIMEOUT_MS} ms)`;
private static readonly LOG_AREA = "GremlinClient";

View File

@@ -17,19 +17,21 @@ export class MiddlePaneComponent extends React.Component<MiddlePaneComponentProp
<div className="middlePane">
<div className="graphTitle">
<span className="paneTitle">Graph</span>
<span
<button
style={{ border: "none", background: "none" }}
className="graphExpandCollapseBtn pull-right"
onClick={this.props.toggleExpandGraph}
role="button"
aria-expanded={this.props.isTabsContentExpanded}
aria-name="View graph in full screen"
tabIndex={0}
aria-label={
this.props.isTabsContentExpanded ? "Collapse graph to minimized view" : "View graph in full screen"
}
>
<img
src={this.props.isTabsContentExpanded ? CollapseArrowIcon : ExpandIcon}
alt={this.props.isTabsContentExpanded ? "collapse graph content" : "expand graph content"}
/>
</span>
</button>
</div>
<div className="maingraphContainer">
<GraphVizComponent forceGraphParams={this.props.graphVizProps.forceGraphParams} />

View File

@@ -107,11 +107,11 @@ describe("New Vertex Component", () => {
it("should call onTypeChange method on type dropdown change", () => {
const DOWN_ARROW = { keyCode: 40 };
const onTypeChange = jest.fn();
const dropdown = screen.getByRole("listbox");
const dropdown = screen.getByRole("combobox");
dropdown.onclick = onTypeChange();
dropdown.onkeydown = onTypeChange();
fireEvent.keyDown(screen.getByRole("listbox"), DOWN_ARROW);
fireEvent.keyDown(screen.getByRole("combobox"), DOWN_ARROW);
fireEvent.click(screen.getByText(/number/));
expect(onTypeChange).toHaveBeenCalled();
});

View File

@@ -145,6 +145,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
type="text"
id="propertyKeyNewVertexPane"
componentRef={input}
aria-required="true"
placeholder="Key"
autoComplete="off"
aria-label={`Enter value for propery ${index + 1}`}
@@ -152,6 +153,8 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)}
/>
</div>
<span className="mandatoryStar">*&nbsp;</span>
<div className="valueCol">
<TextField
className="edgeInput"
@@ -165,7 +168,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
</div>
<div>
<Dropdown
role="listbox"
role="combobox"
placeholder="Select an option"
defaultSelectedKey={data.values[0].type}
style={{ width: 100 }}

View File

@@ -25,7 +25,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
capabilities: [{ name: "EnableMongo" }],
},
} as DatabaseAccount,
});
@@ -38,6 +38,38 @@ describe("CommandBarComponentButtonFactory tests", () => {
);
expect(enableAzureSynapseLinkBtn).toBeDefined();
});
it("Button should not be visible for Tables API", () => {
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
expect(enableAzureSynapseLinkBtn).toBeUndefined();
});
it("Button should not be visible for Cassandra API", () => {
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
expect(enableAzureSynapseLinkBtn).toBeUndefined();
});
});
describe("Enable notebook button", () => {

View File

@@ -51,11 +51,13 @@ export function createStaticCommandBarButtons(
const buttons: CommandButtonComponentProps[] = [];
buttons.push(newCollectionBtn);
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
if (addSynapseLink) {
buttons.push(createDivider());
buttons.push(addSynapseLink);
if (addSynapseLink) {
buttons.push(createDivider());
buttons.push(addSynapseLink);
}
}
if (userContext.apiType !== "Tables") {
@@ -202,6 +204,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
if (showOpenFullScreen) {
const label = "Open Full Screen";
const fullScreenButton: CommandButtonComponentProps = {
id: "openFullScreenBtn",
iconSrc: OpenInTabIcon,
iconAlt: label,
onCommandClick: () => {
@@ -523,6 +526,28 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
};
}
function createOpenPsqlTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open PSQL Shell";
const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Postgres);
}
},
commandButtonLabel: label,
hasPopup: false,
disabled: disableButton,
ariaLabel: label,
tooltipText: !disableButton
? ""
: "This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.",
};
}
function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps {
const label = "Reset Workspace";
return {
@@ -587,16 +612,7 @@ function createStaticCommandBarButtonsForResourceToken(
}
export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] {
const postgreShellLabel = "Open PostgreSQL Shell";
const openPostgreShellBtn = {
iconSrc: HostedTerminalIcon,
iconAlt: postgreShellLabel,
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Mongo),
commandButtonLabel: postgreShellLabel,
hasPopup: false,
disabled: false,
ariaLabel: postgreShellLabel,
};
const openPostgreShellBtn = createOpenPsqlTerminalButton(container);
return [openPostgreShellBtn];
}

View File

@@ -147,12 +147,9 @@ export class NotificationConsoleComponent extends React.Component<
<div className="notificationConsoleControls">
<Dropdown
label="Filter:"
role="combobox"
selectedKey={this.state.selectedFilter}
options={NotificationConsoleComponent.FilterOptions}
onChange={this.onFilterSelected.bind(this)}
aria-labelledby="consoleFilterLabel"
aria-label={this.state.selectedFilter}
/>
<span className="consoleSplitter" />
<span

View File

@@ -112,8 +112,6 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
className="notificationConsoleControls"
>
<Dropdown
aria-label="All"
aria-labelledby="consoleFilterLabel"
label="Filter:"
onChange={[Function]}
options={
@@ -136,7 +134,6 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
},
]
}
role="combobox"
selectedKey="All"
/>
<span
@@ -278,8 +275,6 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
className="notificationConsoleControls"
>
<Dropdown
aria-label="All"
aria-labelledby="consoleFilterLabel"
label="Filter:"
onChange={[Function]}
options={
@@ -302,7 +297,6 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
},
]
}
role="combobox"
selectedKey="All"
/>
<span

View File

@@ -0,0 +1,23 @@
import * as InMemoryContentProviderUtils from "./ContentProviders/InMemoryContentProviderUtils";
describe("fromContentUri", () => {
it("fromContentUri should return valid result", () => {
const contentUri = "memory://resource/path";
const result = "resource";
expect(InMemoryContentProviderUtils.fromContentUri(contentUri)).toEqual(result);
});
it("fromContentUri should return undefined on invalid input", () => {
const contentUri = "invalid";
expect(InMemoryContentProviderUtils.fromContentUri(contentUri)).toEqual(undefined);
});
it("toContentUri should return valid result", () => {
const path = "resource/path";
const result = "memory://resource/path";
expect(InMemoryContentProviderUtils.toContentUri(path)).toEqual(result);
});
});

View File

@@ -23,7 +23,7 @@ export class NotebookContainerClient {
private scheduleTimerId: NodeJS.Timeout;
constructor(private onConnectionLost: () => void) {
this.phoenixClient = new PhoenixClient();
this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
this.retryOptions = {
retries: Notebook.retryAttempts,
maxTimeout: Notebook.retryAttemptDelayMs,

View File

@@ -124,8 +124,9 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
}
const firstWriteLocation =
databaseAccount?.properties?.writeLocations &&
databaseAccount?.properties?.writeLocations[0]?.locationName.toLowerCase();
userContext.apiType === "Postgres"
? databaseAccount?.location
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
const authorizationHeader = getAuthorizationHeader();
try {
@@ -307,13 +308,16 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
let isPhoenixFeatures = false;
const isPublicInternetAllowed = isPublicInternetAccessAllowed();
const phoenixClient = new PhoenixClient();
const phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
const dbAccountAllowedInfo = await phoenixClient.getDbAccountAllowedStatus();
if (dbAccountAllowedInfo.status === HttpStatusCodes.OK) {
if (dbAccountAllowedInfo?.type === PhoenixErrorType.PhoenixFlightFallback) {
isPhoenixNotebooks = isPublicInternetAllowed && userContext.features.phoenixNotebooks === true;
isPhoenixFeatures = isPublicInternetAllowed && userContext.features.phoenixFeatures === true;
isPhoenixFeatures =
isPublicInternetAllowed &&
// phoenix needs to be enabled for Postgres accounts since the PSQL shell requires phoenix containers
(userContext.features.phoenixFeatures === true || userContext.apiType === "Postgres");
} else {
isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed;
}

View File

@@ -1,17 +1,9 @@
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");
expect(screen.getByText("Open")).toBeDefined();
});

View File

@@ -1,66 +1,26 @@
import { DefaultButton, PrimaryButton, Spinner, Stack, Text, TextField } from "@fluentui/react";
import copyToClipboard from "clipboard-copy";
import { PrimaryButton, Stack, Text } from "@fluentui/react";
import * as React from "react";
import { useFullScreenURLs } from "../hooks/useFullScreenURLs";
export const OpenFullScreen: React.FunctionComponent = () => {
const [isReadUrlCopy, setIsReadUrlCopy] = React.useState<boolean>(false);
const [isReadWriteUrlCopy, setIsReadWriteUrlCopy] = React.useState<boolean>(false);
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
ariaLabel={isReadWriteUrlCopy ? "Copied url" : "Copy"}
onClick={() => {
copyToClipboard(readWriteUrl);
setIsReadWriteUrlCopy(true);
}}
text={isReadWriteUrlCopy ? "Copied" : "Copy"}
iconProps={{ iconName: "Copy" }}
/>
<PrimaryButton
onClick={() => {
window.open(readWriteUrl, "_blank");
}}
text="Open"
iconProps={{ iconName: "OpenInNewWindow" }}
/>
<div style={{ padding: "34px" }}>
<Stack tokens={{ childrenGap: 10 }}>
<Text>
Open this database account in a new browser tab with Cosmos DB Explorer. You can connect using your
Microsoft account or a connection string.
</Text>
<Stack horizontal tokens={{ childrenGap: 10 }}>
<PrimaryButton
onClick={() => {
window.open("https://cosmos.azure.com/", "_blank");
}}
text="Open"
iconProps={{ iconName: "OpenInNewWindow" }}
/>
</Stack>
</Stack>
<TextField label="Read Only" readOnly defaultValue={readUrl} />
<Stack horizontal tokens={{ childrenGap: 10 }}>
<DefaultButton
ariaLabel={isReadUrlCopy ? "Copied url" : "Copy"}
onClick={() => {
setIsReadUrlCopy(true);
copyToClipboard(readUrl);
}}
text={isReadUrlCopy ? "Copied" : "Copy"}
iconProps={{ iconName: "Copy" }}
/>
<PrimaryButton
onClick={() => {
window.open(readUrl, "_blank");
}}
text="Open"
iconProps={{ iconName: "OpenInNewWindow" }}
/>
</Stack>
</Stack>
</div>
</>
);
};

View File

@@ -57,7 +57,7 @@ const SharedDatabaseDefault: DataModels.IndexingPolicy = {
],
};
const AllPropertiesIndexed: DataModels.IndexingPolicy = {
export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
indexingMode: "consistent",
automatic: true,
includedPaths: [
@@ -89,9 +89,10 @@ export interface AddCollectionPanelState {
enableIndexing: boolean;
isSharded: boolean;
partitionKey: string;
subPartitionKeys: string[];
enableDedicatedThroughput: boolean;
createMongoWildCardIndex: boolean;
useHashV2: boolean;
useHashV1: boolean;
enableAnalyticalStore: boolean;
uniqueKeys: string[];
errorMessage: string;
@@ -121,9 +122,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
enableIndexing: true,
isSharded: userContext.apiType !== "Tables",
partitionKey: this.getPartitionKey(),
subPartitionKeys: [],
enableDedicatedThroughput: false,
createMongoWildCardIndex: isCapabilityEnabled("EnableMongo"),
useHashV2: false,
createMongoWildCardIndex:
isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport"),
useHashV1: false,
enableAnalyticalStore: false,
uniqueKeys: [],
errorMessage: "",
@@ -259,37 +262,46 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
true
).toLocaleLowerCase()}.`}
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true
).toLocaleLowerCase()}.`}
/>
</TooltipHost>
</Stack>
<Stack horizontal verticalAlign="center">
<input
className="panelRadioBtn"
checked={this.state.createNewDatabase}
aria-label="Create new database"
aria-checked={this.state.createNewDatabase}
name="databaseType"
type="radio"
role="radio"
id="databaseCreateNew"
tabIndex={0}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Create new</span>
<div role="radiogroup">
<input
className="panelRadioBtn"
checked={this.state.createNewDatabase}
aria-label="Create new database"
aria-checked={this.state.createNewDatabase}
name="databaseType"
type="radio"
role="radio"
id="databaseCreateNew"
tabIndex={0}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Create new</span>
<input
className="panelRadioBtn"
checked={!this.state.createNewDatabase}
aria-label="Use existing database"
aria-checked={!this.state.createNewDatabase}
name="databaseType"
type="radio"
role="radio"
tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Use existing</span>
<input
className="panelRadioBtn"
checked={!this.state.createNewDatabase}
aria-label="Use existing database"
aria-checked={!this.state.createNewDatabase}
name="databaseType"
type="radio"
role="radio"
tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Use existing</span>
</div>
</Stack>
{this.state.createNewDatabase && (
@@ -335,7 +347,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
true
).toLocaleLowerCase()} within the database.`}
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
true
).toLocaleLowerCase()} within the database.`}
/>
</TooltipHost>
</Stack>
)}
@@ -383,7 +402,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
directionalHint={DirectionalHint.bottomLeftEdge}
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
<Icon
role="button"
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
/>
</TooltipHost>
</Stack>
@@ -466,7 +491,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
}
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
}
/>
</TooltipHost>
</Stack>
@@ -513,7 +545,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
directionalHint={DirectionalHint.bottomLeftEdge}
content={this.getPartitionKeyTooltipText()}
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={this.getPartitionKeyTooltipText()}
/>
</TooltipHost>
</Stack>
@@ -545,6 +582,77 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
}}
/>
{userContext.apiType === "SQL" &&
this.state.subPartitionKeys.map((subPartitionKey: string, index: number) => {
return (
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${index}`} horizontal>
<div
style={{
width: "20px",
border: "solid",
borderWidth: "0px 0px 1px 1px",
marginRight: "5px",
}}
></div>
<input
type="text"
id="addCollection-partitionKeyValue"
key={`addCollection-partitionKeyValue_${index}`}
aria-required
required
size={40}
tabIndex={index > 0 ? 1 : 0}
className="panelTextField"
autoComplete="off"
placeholder={this.getPartitionKeyPlaceHolder(index)}
aria-label={this.getPartitionKeyName()}
pattern={".*"}
title={""}
value={subPartitionKey}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const subPartitionKeys = [...this.state.subPartitionKeys];
if (!this.state.subPartitionKeys[index] && !event.target.value.startsWith("/")) {
subPartitionKeys[index] = "/" + event.target.value.trim();
this.setState({ subPartitionKeys });
} else {
subPartitionKeys[index] = event.target.value.trim();
this.setState({ subPartitionKeys });
}
}}
/>
<IconButton
iconProps={{ iconName: "Delete" }}
style={{ height: 27 }}
onClick={() => {
const subPartitionKeys = this.state.subPartitionKeys.filter((uniqueKey, j) => index !== j);
this.setState({ subPartitionKeys });
}}
/>
</Stack>
);
})}
{userContext.apiType === "SQL" && (
<Stack className="panelGroupSpacing">
<DefaultButton
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
hidden={this.state.useHashV1}
disabled={this.state.subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
onClick={() => this.setState({ subPartitionKeys: [...this.state.subPartitionKeys, ""] })}
>
Add hierarchical partition key
</DefaultButton>
{this.state.subPartitionKeys.length > 0 && (
<Text variant="small">
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to
partition your data with up to three levels of keys for better data distribution. Requires .NET
V3, Java V4 SDK, or preview JavaScript V3 SDK.{" "}
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
Learn more
</Link>
</Text>
)}
</Stack>
)}
</Stack>
)}
@@ -571,7 +679,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
does not count towards the throughput you provisioned for the database. This throughput amount will be
billed in addition to the throughput amount you provisioned at the database level.`}
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName(
true
).toLocaleLowerCase()} in the database and
does not count towards the throughput you provisioned for the database. This throughput amount will be
billed in addition to the throughput amount you provisioned at the database level.`}
/>
</TooltipHost>
</Stack>
)}
@@ -602,11 +720,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content="Unique keys provide developers with the ability to add a layer of data integrity to their database. By
creating a unique key policy when a container is created, you ensure the uniqueness of one or more values
per partition key."
content={
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
}
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
}
/>
</TooltipHost>
</Stack>
@@ -669,40 +794,48 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
directionalHint={DirectionalHint.bottomLeftEdge}
content={this.getAnalyticalStorageTooltipContent()}
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without
impacting the performance of transactional workloads."
/>
</TooltipHost>
</Stack>
<Stack horizontal verticalAlign="center">
<input
className="panelRadioBtn"
checked={this.state.enableAnalyticalStore}
disabled={!this.isSynapseLinkEnabled()}
aria-label="Enable analytical store"
aria-checked={this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="enableAnalyticalStoreBtn"
tabIndex={0}
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">On</span>
<div role="radiogroup">
<input
className="panelRadioBtn"
checked={this.state.enableAnalyticalStore}
disabled={!this.isSynapseLinkEnabled()}
aria-label="Enable analytical store"
aria-checked={this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="enableAnalyticalStoreBtn"
tabIndex={0}
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">On</span>
<input
className="panelRadioBtn"
checked={!this.state.enableAnalyticalStore}
disabled={!this.isSynapseLinkEnabled()}
aria-label="Disable analytical store"
aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="disableAnalyticalStoreBtn"
tabIndex={0}
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Off</span>
<input
className="panelRadioBtn"
checked={!this.state.enableAnalyticalStore}
disabled={!this.isSynapseLinkEnabled()}
aria-label="Disable analytical store"
aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="disableAnalyticalStoreBtn"
tabIndex={0}
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Off</span>
</div>
</Stack>
{!this.isSynapseLinkEnabled() && (
@@ -735,7 +868,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}}
>
<Stack className="panelGroupSpacing" id="collapsibleSectionContent">
{isCapabilityEnabled("EnableMongo") && (
{isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport") && (
<Stack className="panelGroupSpacing">
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
@@ -746,7 +879,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
directionalHint={DirectionalHint.bottomLeftEdge}
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
/>
</TooltipHost>
</Stack>
@@ -766,18 +904,29 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)}
{userContext.apiType === "SQL" && (
<Checkbox
label="My partition key is larger than 101 bytes"
checked={this.state.useHashV2}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
this.setState({ useHashV2: isChecked })
}
/>
<Stack className="panelGroupSpacing">
<Checkbox
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
checked={this.state.useHashV1}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center", wordWrap: "break-word", whiteSpace: "break-spaces" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
this.setState({ useHashV1: isChecked, subPartitionKeys: [] })
}
/>
<Text variant="small">
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> To ensure compatibility with
older SDKs, the created container will use a legacy partitioning scheme that supports partition
key values of size only up to 101 bytes. If this is enabled, you will not be able to use
hierarchical partition keys.{" "}
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
Learn more
</Link>
</Text>
</Stack>
)}
</Stack>
</CollapsibleSectionComponent>
@@ -832,12 +981,20 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
}
private getPartitionKeyPlaceHolder(): string {
private getPartitionKeyPlaceHolder(index?: number): string {
switch (userContext.apiType) {
case "Mongo":
return "e.g., address.zipCode";
return "e.g., categoryId";
case "Gremlin":
return "e.g., /address";
case "SQL":
return `${
index === undefined
? "Required - first partition key e.g., /TenantId"
: index === 0
? "second partition key e.g., /UserId"
: "third partition key e.g., /SessionId"
}`;
default:
return "e.g., /address/zipCode";
}
@@ -959,7 +1116,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return userContext.apiType === "SQL" ? "/pk" : "pk";
}
if (this.props.isQuickstart) {
return userContext.apiType === "SQL" ? "/address" : "address";
return userContext.apiType === "SQL" ? "/categoryId" : "categoryId";
}
return "";
}
@@ -1163,11 +1320,16 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = this.parseUniqueKeys();
const partitionKeyVersion = this.state.useHashV2 ? 2 : undefined;
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
const partitionKey: DataModels.PartitionKey = partitionKeyString
? {
paths: [partitionKeyString],
kind: "Hash",
paths: [
partitionKeyString,
...(userContext.apiType === "SQL" && this.state.subPartitionKeys.length > 0
? this.state.subPartitionKeys
: []),
],
kind: userContext.apiType === "SQL" && this.state.subPartitionKeys.length > 0 ? "MultiHash" : "Hash",
version: partitionKeyVersion,
}
: undefined;

View File

@@ -89,8 +89,8 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
}
} catch (error) {
setLoadingFalse();
setFormError(error);
const errorMessage = getErrorMessage(error);
setFormError(errorMessage);
TelemetryProcessor.traceFailure(
Action.DeleteDatabase,
{

View File

@@ -24,6 +24,7 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": Object {
"maxTimeout": 5000,
"minTimeout": 5000,

View File

@@ -48,7 +48,7 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
)}
</Text>
{showErrorDetails && (
<a className="paneErrorLink" role="link" onClick={expandConsole}>
<a className="paneErrorLink" role="button" onClick={expandConsole} tabIndex={0} onKeyPress={expandConsole}>
More details
</a>
)}

View File

@@ -4,11 +4,11 @@ import React, { FunctionComponent, useState } from "react";
import { Areas, SavedQueries } from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { Query } from "../../../Contracts/DataModels";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { useTabs } from "../../../hooks/useTabs";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { useTabs } from "../../../hooks/useTabs";
import Explorer from "../../Explorer";
import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab";
import { useDatabases } from "../../useDatabases";
@@ -16,9 +16,13 @@ import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneFor
interface SaveQueryPaneProps {
explorer: Explorer;
queryToSave?: string;
}
export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer }: SaveQueryPaneProps): JSX.Element => {
export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
explorer,
queryToSave,
}: SaveQueryPaneProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [formError, setFormError] = useState<string>("");
@@ -36,7 +40,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer
}
const queryTab = useTabs.getState().activeTab as NewQueryTab;
const query: string = queryTab && queryTab.iTabAccessor.onSaveClickEvent();
const query: string = queryToSave || queryTab?.iTabAccessor.onSaveClickEvent();
if (!queryName || queryName.length === 0) {
setFormError("No query name specified");
@@ -62,8 +66,8 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer
try {
await explorer.queriesClient.saveQuery(queryParam);
setLoadingFalse();
queryTab.tabTitle(queryParam.queryName);
queryTab.tabPath(`${queryTab.collection.databaseId}>${queryTab.collection.id()}>${queryParam.queryName}`);
queryTab?.tabTitle(queryParam.queryName);
queryTab?.tabPath(`${queryTab.collection.databaseId}>${queryTab.collection.id()}>${queryParam.queryName}`);
traceSuccess(
Action.SaveQuery,
{

View File

@@ -21,6 +21,11 @@ export const SettingsPane: FunctionComponent = () => {
const [customItemPerPage, setCustomItemPerPage] = useState<number>(
LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage) || 0
);
const [containerPaginationEnabled, setContainerPaginationEnabled] = useState<boolean>(
LocalStorageUtility.hasItem(StorageKey.ContainerPaginationEnabled)
? LocalStorageUtility.getEntryString(StorageKey.ContainerPaginationEnabled) === "true"
: false
);
const [crossPartitionQueryEnabled, setCrossPartitionQueryEnabled] = useState<boolean>(
LocalStorageUtility.hasItem(StorageKey.IsCrossPartitionQueryEnabled)
? LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
@@ -50,6 +55,7 @@ export const SettingsPane: FunctionComponent = () => {
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage
);
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString());
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
@@ -185,6 +191,25 @@ export const SettingsPane: FunctionComponent = () => {
</div>
</div>
)}
<div className="settingsSection">
<div className="settingsSectionPart">
<div className="settingsSectionLabel">
Enable container pagination
<InfoTooltip>
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
</InfoTooltip>
</div>
<Checkbox
styles={{
label: { padding: 0 },
}}
className="padding"
ariaLabel="Enable container pagination"
checked={containerPaginationEnabled}
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
/>
</div>
</div>
{shouldShowCrossPartitionOption && (
<div className="settingsSection">
<div className="settingsSectionPart">

View File

@@ -97,6 +97,35 @@ exports[`Settings Pane should render Default properly 1`] = `
</div>
</div>
</div>
<div
className="settingsSection"
>
<div
className="settingsSectionPart"
>
<div
className="settingsSectionLabel"
>
Enable container pagination
<InfoTooltip>
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
</InfoTooltip>
</div>
<StyledCheckboxBase
ariaLabel="Enable container pagination"
checked={false}
className="padding"
onChange={[Function]}
styles={
Object {
"label": Object {
"padding": 0,
},
}
}
/>
</div>
</div>
<div
className="settingsSection"
>
@@ -182,6 +211,35 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
<div
className="paneMainContent"
>
<div
className="settingsSection"
>
<div
className="settingsSectionPart"
>
<div
className="settingsSectionLabel"
>
Enable container pagination
<InfoTooltip>
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
</InfoTooltip>
</div>
<StyledCheckboxBase
ariaLabel="Enable container pagination"
checked={false}
className="padding"
onChange={[Function]}
styles={
Object {
"label": Object {
"padding": 0,
},
}
}
/>
</div>
</div>
<div
className="settingsSection"
>

View File

@@ -14,6 +14,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": Object {
"maxTimeout": 5000,
"minTimeout": 5000,

View File

@@ -242,6 +242,11 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
submitButtonText: getButtonLabel(userContext.apiType),
onSubmit,
};
const handlekeypressaddentity = (event: React.KeyboardEvent<HTMLElement>) => {
if (event.key === "Enter" || event.key === "Space") {
addNewEntity();
}
};
return (
<RightPaneForm {...props}>
@@ -284,7 +289,13 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
);
})}
{userContext.apiType !== "Cassandra" && (
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
<Stack
horizontal
onClick={addNewEntity}
className="addButtonEntiy"
tabIndex={0}
onKeyPress={handlekeypressaddentity}
>
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
</Stack>

View File

@@ -29,10 +29,14 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
className="addButtonEntiy"
horizontal={true}
onClick={[Function]}
onKeyPress={[Function]}
tabIndex={0}
>
<div
className="ms-Stack addButtonEntiy css-53"
onClick={[Function]}
onKeyPress={[Function]}
tabIndex={0}
>
<StyledImageBase
alt="Add Entity"

View File

@@ -31,6 +31,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
directionalHint={4}
>
<Icon
ariaLabel="A database is analogous to a namespace. It is the unit of management for a set of containers."
className="panelInfoIcon"
iconName="Info"
tabIndex={0}
@@ -41,39 +42,43 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
horizontal={true}
verticalAlign="center"
>
<input
aria-checked={true}
aria-label="Create new database"
checked={true}
className="panelRadioBtn"
id="databaseCreateNew"
name="databaseType"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="panelRadioBtnLabel"
<div
role="radiogroup"
>
Create new
</span>
<input
aria-checked={false}
aria-label="Use existing database"
checked={false}
className="panelRadioBtn"
name="databaseType"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="panelRadioBtnLabel"
>
Use existing
</span>
<input
aria-checked={true}
aria-label="Create new database"
checked={true}
className="panelRadioBtn"
id="databaseCreateNew"
name="databaseType"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="panelRadioBtnLabel"
>
Create new
</span>
<input
aria-checked={false}
aria-label="Use existing database"
checked={false}
className="panelRadioBtn"
name="databaseType"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="panelRadioBtnLabel"
>
Use existing
</span>
</div>
</Stack>
<Stack
className="panelGroupSpacing"
@@ -124,6 +129,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
directionalHint={4}
>
<Icon
ariaLabel="Throughput configured at the database level will be shared across all containers within the database."
className="panelInfoIcon"
iconName="Info"
tabIndex={0}
@@ -163,8 +169,10 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
directionalHint={4}
>
<Icon
ariaLabel="Unique identifier for the container and used for id-based routing through REST and all SDKs."
className="panelInfoIcon"
iconName="Info"
role="button"
tabIndex={0}
/>
</StyledTooltipHostBase>
@@ -206,6 +214,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
directionalHint={4}
>
<Icon
ariaLabel="The partition key is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume. For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice."
className="panelInfoIcon"
iconName="Info"
tabIndex={0}
@@ -223,13 +232,36 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
id="addCollection-partitionKeyValue"
onChange={[Function]}
pattern=".*"
placeholder="e.g., /address/zipCode"
placeholder="Required - first partition key e.g., /TenantId"
required={true}
size={40}
title=""
type="text"
value=""
/>
<Stack
className="panelGroupSpacing"
>
<CustomizedDefaultButton
disabled={false}
hidden={false}
onClick={[Function]}
styles={
Object {
"label": Object {
"fontSize": 12,
},
"root": Object {
"height": 30,
"padding": 0,
"width": 200,
},
}
}
>
Add hierarchical partition key
</CustomizedDefaultButton>
</Stack>
</Stack>
<Stack>
<Stack
@@ -246,6 +278,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
directionalHint={4}
>
<Icon
ariaLabel="Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
className="panelInfoIcon"
iconName="Info"
tabIndex={0}
@@ -303,6 +336,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
directionalHint={4}
>
<Icon
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
className="panelInfoIcon"
iconName="Info"
tabIndex={0}
@@ -313,42 +347,46 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
horizontal={true}
verticalAlign="center"
>
<input
aria-checked={false}
aria-label="Enable analytical store"
checked={false}
className="panelRadioBtn"
disabled={true}
id="enableAnalyticalStoreBtn"
name="analyticalStore"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="panelRadioBtnLabel"
<div
role="radiogroup"
>
On
</span>
<input
aria-checked={true}
aria-label="Disable analytical store"
checked={true}
className="panelRadioBtn"
disabled={true}
id="disableAnalyticalStoreBtn"
name="analyticalStore"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="panelRadioBtnLabel"
>
Off
</span>
<input
aria-checked={false}
aria-label="Enable analytical store"
checked={false}
className="panelRadioBtn"
disabled={true}
id="enableAnalyticalStoreBtn"
name="analyticalStore"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="panelRadioBtnLabel"
>
On
</span>
<input
aria-checked={true}
aria-label="Disable analytical store"
checked={true}
className="panelRadioBtn"
disabled={true}
id="disableAnalyticalStoreBtn"
name="analyticalStore"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="panelRadioBtnLabel"
>
Off
</span>
</div>
</Stack>
<Stack
className="panelGroupSpacing"
@@ -396,26 +434,49 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
className="panelGroupSpacing"
id="collapsibleSectionContent"
>
<StyledCheckboxBase
checked={false}
label="My partition key is larger than 101 bytes"
onChange={[Function]}
styles={
Object {
"checkbox": Object {
"height": 12,
"width": 12,
},
"label": Object {
"alignItems": "center",
"padding": 0,
},
"text": Object {
"fontSize": 12,
},
<Stack
className="panelGroupSpacing"
>
<StyledCheckboxBase
checked={false}
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
onChange={[Function]}
styles={
Object {
"checkbox": Object {
"height": 12,
"width": 12,
},
"label": Object {
"alignItems": "center",
"padding": 0,
"whiteSpace": "break-spaces",
"wordWrap": "break-word",
},
"text": Object {
"fontSize": 12,
},
}
}
}
/>
/>
<Text
variant="small"
>
<Icon
className="removeIcon"
iconName="InfoSolid"
tabIndex={0}
/>
To ensure compatibility with older SDKs, the created container will use a legacy partitioning scheme that supports partition key values of size only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.
<StyledLinkBase
href="https://aka.ms/cosmos-large-pk"
target="_blank"
>
Learn more
</StyledLinkBase>
</Text>
</Stack>
</Stack>
</CollapsibleSectionComponent>
</div>

View File

@@ -0,0 +1,11 @@
import { shallow } from "enzyme";
import React from "react";
import Explorer from "../Explorer";
import { QueryCopilotCarousel } from "./CopilotCarousel";
describe("Query Copilot Carousel snapshot test", () => {
it("should render when isOpen is true", () => {
const wrapper = shallow(<QueryCopilotCarousel isOpen={true} explorer={new Explorer()} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,277 @@
import {
DefaultButton,
ISeparatorStyles,
IconButton,
Image,
Link,
Modal,
PrimaryButton,
Separator,
Spinner,
Stack,
Text,
} from "@fluentui/react";
import { QueryCopilotSampleDatabaseId, StyleConstants } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils";
import { createCollection } from "Common/dataAccess/createCollection";
import * as DataModels from "Contracts/DataModels";
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
import Explorer from "Explorer/Explorer";
import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel";
import { PromptCard } from "Explorer/QueryCopilot/PromptCard";
import { useDatabases } from "Explorer/useDatabases";
import { useCarousel } from "hooks/useCarousel";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import React, { useState } from "react";
import YoutubePlaceholder from "../../../images/YoutubePlaceholder.svg";
interface QueryCopilotCarouselProps {
isOpen: boolean;
explorer: Explorer;
}
const separatorStyles: Partial<ISeparatorStyles> = {
root: {
selectors: {
"::before": {
background: StyleConstants.BaseMedium,
},
},
padding: "16px 0",
height: 1,
},
};
export const QueryCopilotCarousel: React.FC<QueryCopilotCarouselProps> = ({
isOpen,
explorer,
}: QueryCopilotCarouselProps): JSX.Element => {
const [page, setPage] = useState<number>(1);
const [isCreatingDatabase, setIsCreatingDatabase] = useState<boolean>(false);
const [spinnerText, setSpinnerText] = useState<string>("");
const [selectedPrompt, setSelectedPrompt] = useState<number>(1);
const getHeaderText = (): string => {
switch (page) {
case 1:
return "What exactly is copilot?";
case 2:
return "Setting up your Sample database";
case 3:
return "Sample prompts to help you";
default:
return "";
}
};
const getQueryCopilotInitialInput = (): string => {
switch (selectedPrompt) {
case 1:
return "Write a query to return all records in this table";
case 2:
return "Write a query to return all records in this table created in the last thirty days";
case 3:
return 'Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"';
default:
return "";
}
};
const createSampleDatabase = async (): Promise<void> => {
const database = useDatabases.getState().findDatabaseWithId(QueryCopilotSampleDatabaseId);
if (database) {
return;
}
try {
setIsCreatingDatabase(true);
setSpinnerText("Setting up your database...");
const params: DataModels.CreateCollectionParams = {
createNewDatabase: true,
collectionId: "SampleContainer",
databaseId: QueryCopilotSampleDatabaseId,
databaseLevelThroughput: true,
autoPilotMaxThroughput: 1000,
offerThroughput: undefined,
indexingPolicy: AllPropertiesIndexed,
partitionKey: {
paths: ["/categoryId"],
kind: "Hash",
version: 2,
},
};
await createCollection(params);
await explorer.refreshAllDatabases();
const database = useDatabases.getState().findDatabaseWithId(QueryCopilotSampleDatabaseId);
await database.loadCollections();
const collection = database.findCollectionWithId("SampleContainer");
setSpinnerText("Adding sample data set...");
const sampleGenerator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorer, true);
await sampleGenerator.populateContainerAsync(collection);
await database.expandDatabase();
collection.expandCollection();
useDatabases.getState().updateDatabase(database);
} catch (error) {
//TODO: show error in UI
handleError(error, "QueryCopilotCreateSampleDB");
throw error;
} finally {
setIsCreatingDatabase(false);
setSpinnerText("");
}
};
const getContent = (): JSX.Element => {
switch (page) {
case 1:
return (
<Stack style={{ marginTop: 8 }}>
<Text style={{ fontSize: 13 }}>
A couple of lines about copilot and the background about it. The idea is to have some text to give context
to the user.
</Text>
<Text style={{ fontSize: 14, fontWeight: 600, marginTop: 16 }}>How do you use copilot</Text>
<Text style={{ fontSize: 13, marginTop: 8 }}>
To generate queries , just describe the query you want and copilot will generate the query for you.Watch
this video to learn more about how to use copilot.
</Text>
<Image src={YoutubePlaceholder} style={{ margin: "16px auto" }} />
<Text style={{ fontSize: 14, fontWeight: 600 }}>What is copilot good at</Text>
<Text style={{ fontSize: 13, marginTop: 8 }}>
A couple of lines about what copilot can do and its capablites with a link to{" "}
<Link href="" target="_blank">
documentation
</Link>{" "}
if possible.
</Text>
<Text style={{ fontSize: 14, fontWeight: 600, marginTop: 16 }}>What are its limitations</Text>
<Text style={{ fontSize: 13, marginTop: 8 }}>
A couple of lines about what copilot cant do and its limitations.{" "}
<Link href="" target="_blank">
Link to documentation
</Link>
</Text>
<Text style={{ fontSize: 14, fontWeight: 600, marginTop: 16 }}>Disclaimer</Text>
<Text style={{ fontSize: 13, marginTop: 8 }}>
AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "}
<Link href="" target="_blank">
Read preview terms
</Link>
</Text>
</Stack>
);
case 2:
return (
<Stack style={{ marginTop: 8 }}>
<Text style={{ fontSize: 13 }}>
Before you get started, we need to configure your sample database for you. Here is a summary of the
database being created for your reference. Configuration values can be updated using the settings icon in
the query builder.
</Text>
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 24 }}>Database Id</Text>
<Text style={{ fontSize: 13 }}>CopilotSampleDb</Text>
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database throughput (autoscale)</Text>
<Text style={{ fontSize: 13 }}>Autoscale</Text>
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database Max RU/s</Text>
<Text>1000</Text>
<Text style={{ fontSize: 10, marginTop: 8 }}>
Your database throughput will automatically scale from{" "}
<strong>100 RU/s (10% of max RU/s) - 1000 RU/s</strong> based on usage.
</Text>
<Text style={{ fontSize: 10, marginTop: 8 }}>
Estimated monthly cost (USD): <strong>$8.76 - $87.60</strong> (1 region, 100 - 1000 RU/s, $0.00012/RU)
</Text>
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Container Id</Text>
<Text style={{ fontSize: 13 }}>SampleContainer</Text>
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Partition key</Text>
<Text style={{ fontSize: 13 }}>categoryId</Text>
</Stack>
);
case 3:
return (
<Stack>
<Text>To help you get started, here are some sample prompts to get you started</Text>
<Stack tokens={{ childrenGap: 12 }} style={{ marginTop: 16 }}>
<PromptCard
header="Write a query to return all records in this table"
description="This is a basic query which returns all records in the table "
onSelect={() => setSelectedPrompt(1)}
isSelected={selectedPrompt === 1}
/>
<PromptCard
header="Write a query to return all records in this table created in the last thirty days"
description="This builds on the previous query which returns all records in the table which were inserted in the last thirty days. You can also modify this query to return records based upon creation date"
onSelect={() => setSelectedPrompt(2)}
isSelected={selectedPrompt === 2}
/>
<PromptCard
header='Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"'
description='This builds on the previous query which returns all records in the table which were inserted in the last thirty days but which has the record owner as "contoso"'
onSelect={() => setSelectedPrompt(3)}
isSelected={selectedPrompt === 3}
/>
</Stack>
<Text style={{ fontSize: 13, marginTop: 32 }}>
Interested in learning more about how to write effective prompts. Please read this article for more
information.
</Text>
<Text style={{ fontSize: 13, marginTop: 16 }}>
You can also access these prompts by selecting the Samples prompts button in the query builder page.
</Text>
<Text style={{ fontSize: 13, marginTop: 16 }}>
Don&apos;t like any of the prompts? Just click Get Started and write your own prompt.
</Text>
</Stack>
);
default:
return <></>;
}
};
return (
<Modal styles={{ main: { width: 880 } }} isOpen={isOpen && page < 4}>
<Stack style={{ padding: 16 }}>
<Stack horizontal horizontalAlign="space-between">
<Text variant="xLarge">{getHeaderText()}</Text>
<IconButton
iconProps={{ iconName: "Cancel" }}
onClick={() => useCarousel.getState().setShowCopilotCarousel(false)}
/>
</Stack>
{getContent()}
<Separator styles={separatorStyles} />
<Stack horizontal horizontalAlign="start" verticalAlign="center">
{page !== 1 && (
<DefaultButton
text="Previous"
style={{ marginRight: 8 }}
onClick={() => setPage(page - 1)}
disabled={isCreatingDatabase}
/>
)}
<PrimaryButton
text={page === 3 ? "Get started" : "Next"}
onClick={async () => {
if (page === 3) {
useCarousel.getState().setShowCopilotCarousel(false);
useTabs.getState().setQueryCopilotTabInitialInput(getQueryCopilotInitialInput());
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
return;
}
if (page === 2) {
await createSampleDatabase();
}
setPage(page + 1);
}}
disabled={isCreatingDatabase}
/>
{isCreatingDatabase && <Spinner style={{ marginLeft: 8 }} />}
{isCreatingDatabase && <Text style={{ marginLeft: 8, color: "#0078D4" }}>{spinnerText}</Text>}
</Stack>
</Stack>
</Modal>
);
};

View File

@@ -0,0 +1,19 @@
import { shallow } from "enzyme";
import React from "react";
import { PromptCard } from "./PromptCard";
describe("Prompt card snapshot test", () => {
it("should render properly if isSelected is true", () => {
const wrapper = shallow(
<PromptCard header="TestHeader" description="TestDescription" isSelected={true} onSelect={() => undefined} />
);
expect(wrapper).toMatchSnapshot();
});
it("should render properly if isSelected is false", () => {
const wrapper = shallow(
<PromptCard header="TestHeader" description="TestDescription" isSelected={false} onSelect={() => undefined} />
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,48 @@
import { ChoiceGroup, Stack, Text } from "@fluentui/react";
import React from "react";
interface PromptCardProps {
header: string;
description: string;
isSelected: boolean;
onSelect: () => void;
}
export const PromptCard: React.FC<PromptCardProps> = ({
header,
description,
isSelected,
onSelect,
}: PromptCardProps): JSX.Element => {
return (
<Stack
horizontal
style={{
padding: "16px 0 16px 16px ",
boxSizing: "border-box",
width: 650,
height: 100,
border: "1px solid #F3F2F1",
boxShadow: "0px 1.6px 3.6px rgba(0, 0, 0, 0.132), 0px 0.3px 0.9px rgba(0, 0, 0, 0.108)",
}}
>
<Stack.Item grow={1}>
<Stack horizontal>
<div>
<Text style={{ fontSize: 13, color: "#00A2AD", background: "#F8FFF0" }}>Prompt</Text>
</div>
<Text style={{ fontSize: 13, marginLeft: 16 }}>{header}</Text>
</Stack>
<Text style={{ fontSize: 10, marginTop: 16 }}>{description}</Text>
</Stack.Item>
<Stack.Item style={{ marginLeft: 16 }}>
<ChoiceGroup
styles={{ flexContainer: { width: 36 } }}
options={[{ key: "selected", text: "" }]}
selectedKey={isSelected ? "selected" : ""}
onChange={onSelect}
/>
</Stack.Item>
</Stack>
);
};

View File

@@ -0,0 +1,105 @@
import {
Checkbox,
ChoiceGroup,
DefaultButton,
IconButton,
Link,
Modal,
PrimaryButton,
Stack,
Text,
TextField,
} from "@fluentui/react";
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
const {
generatedQuery,
userPrompt,
likeQuery,
showFeedbackModal,
closeFeedbackModal,
setHideFeedbackModalForLikedQueries,
} = useQueryCopilot();
const [isContactAllowed, setIsContactAllowed] = React.useState<boolean>(true);
const [description, setDescription] = React.useState<string>("");
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
return (
<Modal isOpen={showFeedbackModal}>
<Stack style={{ padding: 24 }}>
<Stack horizontal horizontalAlign="space-between">
<Text style={{ fontSize: 20, fontWeight: 600, marginBottom: 20 }}>Send feedback to Microsoft</Text>
<IconButton iconProps={{ iconName: "Cancel" }} onClick={() => closeFeedbackModal()} />
</Stack>
<Text style={{ fontSize: 14, marginBottom: 14 }}>Your feedback will help improve the experience.</Text>
<TextField
styles={{ root: { marginBottom: 14 } }}
label="Description"
required
placeholder="Provide more details"
value={description}
onChange={(_, newValue) => setDescription(newValue)}
multiline
rows={3}
/>
<TextField
styles={{ root: { marginBottom: 14 } }}
label="Query generated"
defaultValue={generatedQuery}
readOnly
/>
<ChoiceGroup
styles={{
root: {
marginBottom: 14,
},
flexContainer: {
selectors: {
".ms-ChoiceField-field::before": { marginTop: 4 },
".ms-ChoiceField-field::after": { marginTop: 4 },
".ms-ChoiceFieldLabel": { paddingLeft: 6 },
},
},
}}
label="May we contact you about your feedback?"
options={[
{ key: "yes", text: "Yes, you may contact me." },
{ key: "no", text: "No, do not contact me." },
]}
selectedKey={isContactAllowed ? "yes" : "no"}
onChange={(_, option) => setIsContactAllowed(option.key === "yes")}
></ChoiceGroup>
<Text style={{ fontSize: 12, marginBottom: 14 }}>
By pressing submit, your feedback will be used to improve Microsoft products and services. IT admins for your
organization will be able to view and manage your feedback data.{" "}
<Link href="" target="_blank">
Privacy statement
</Link>
</Text>
{likeQuery && (
<Checkbox
styles={{ label: { paddingLeft: 0 }, root: { marginBottom: 14 } }}
label="Don't show me this next time"
checked={doNotShowAgainChecked}
onChange={(_, checked) => setDoNotShowAgainChecked(checked)}
/>
)}
<Stack horizontal horizontalAlign="end">
<PrimaryButton
styles={{ root: { marginRight: 8 } }}
onClick={() => {
closeFeedbackModal();
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
submitFeedback({ generatedQuery, likeQuery, description, userPrompt });
}}
>
Submit
</PrimaryButton>
<DefaultButton onClick={() => closeFeedbackModal()}>Cancel</DefaultButton>
</Stack>
</Stack>
</Modal>
);
};

View File

@@ -0,0 +1,13 @@
import { shallow } from "enzyme";
import React from "react";
import Explorer from "../Explorer";
import { QueryCopilotTab } from "./QueryCopilotTab";
describe("Query copilot tab snapshot test", () => {
it("should render with initial input", () => {
const wrapper = shallow(
<QueryCopilotTab initialInput="Write a query to return all records in this table" explorer={new Explorer()} />
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,275 @@
/* eslint-disable no-console */
import { FeedOptions } from "@azure/cosmos";
import {
Callout,
CommandBarButton,
DirectionalHint,
IconButton,
Image,
Link,
Separator,
Spinner,
Stack,
Text,
TextField,
} from "@fluentui/react";
import {
QueryCopilotSampleContainerId,
QueryCopilotSampleContainerSchema,
QueryCopilotSampleDatabaseId,
} from "Common/Constants";
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
import { MinimalQueryIterator } from "Common/IteratorUtilities";
import { queryDocuments } from "Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
import { QueryResults } from "Contracts/ViewModels";
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel";
import React, { useState } from "react";
import SplitterLayout from "react-splitter-layout";
import CopilotIcon from "../../../images/Copilot.svg";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import SaveQueryIcon from "../../../images/save-cosmos.svg";
interface QueryCopilotTabProps {
initialInput: string;
explorer: Explorer;
}
interface GenerateSQLQueryResponse {
apiVersion: string;
sql: string;
explanation: string;
generateStart: string;
generateEnd: string;
}
export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
initialInput,
explorer,
}: QueryCopilotTabProps): JSX.Element => {
const hideFeedbackModalForLikedQueries = useQueryCopilot((state) => state.hideFeedbackModalForLikedQueries);
const [userInput, setUserInput] = useState<string>(initialInput || "");
const [generatedQuery, setGeneratedQuery] = useState<string>("");
const [query, setQuery] = useState<string>("");
const [selectedQuery, setSelectedQuery] = useState<string>("");
const [isGeneratingQuery, setIsGeneratingQuery] = useState<boolean>(false);
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const [likeQuery, setLikeQuery] = useState<boolean>();
const [showCallout, setShowCallout] = useState<boolean>(false);
const [queryIterator, setQueryIterator] = useState<MinimalQueryIterator>();
const [queryResults, setQueryResults] = useState<QueryResults>();
const [errorMessage, setErrorMessage] = useState<string>("");
const generateSQLQuery = async (): Promise<void> => {
try {
setIsGeneratingQuery(true);
const payload = {
containerSchema: QueryCopilotSampleContainerSchema,
userPrompt: userInput,
};
const response = await fetch("https://copilotorchestrater.azurewebsites.net/generateSQLQuery", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify(payload),
});
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
if (generateSQLQueryResponse?.sql) {
let query = `-- **Prompt:** ${userInput}\r\n`;
if (generateSQLQueryResponse.explanation) {
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
}
query += generateSQLQueryResponse.sql;
setQuery(query);
setGeneratedQuery(generateSQLQueryResponse.sql);
}
} catch (error) {
handleError(error, "executeNaturalLanguageQuery");
throw error;
} finally {
setIsGeneratingQuery(false);
}
};
const onExecuteQueryClick = async (): Promise<void> => {
const queryToExecute = selectedQuery || query;
const queryIterator = queryDocuments(QueryCopilotSampleDatabaseId, QueryCopilotSampleContainerId, queryToExecute, {
enableCrossPartitionQuery: shouldEnableCrossPartitionKey(),
} as FeedOptions);
setQueryIterator(queryIterator);
setTimeout(async () => {
await queryDocumentsPerPage(0, queryIterator);
}, 100);
};
const queryDocumentsPerPage = async (firstItemIndex: number, queryIterator: MinimalQueryIterator): Promise<void> => {
try {
setIsExecuting(true);
const queryResults: QueryResults = await queryPagesUntilContentPresent(
firstItemIndex,
async (firstItemIndex: number) =>
queryDocumentsPage(QueryCopilotSampleContainerId, queryIterator, firstItemIndex)
);
setQueryResults(queryResults);
setErrorMessage("");
} catch (error) {
const errorMessage = getErrorMessage(error);
setErrorMessage(errorMessage);
handleError(errorMessage, "executeQueryCopilotTab");
} finally {
setIsExecuting(false);
}
};
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
const executeQueryBtn = {
iconSrc: ExecuteQueryIcon,
iconAlt: executeQueryBtnLabel,
onCommandClick: () => onExecuteQueryClick(),
commandButtonLabel: executeQueryBtnLabel,
ariaLabel: executeQueryBtnLabel,
hasPopup: false,
};
const saveQueryBtn = {
iconSrc: SaveQueryIcon,
iconAlt: "Save Query",
onCommandClick: () =>
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={explorer} queryToSave={query} />),
commandButtonLabel: "Save Query",
ariaLabel: "Save Query",
hasPopup: false,
};
return [executeQueryBtn, saveQueryBtn];
};
React.useEffect(() => {
useCommandBar.getState().setContextButtons(getCommandbarButtons());
}, [query, selectedQuery]);
React.useEffect(() => {
if (initialInput) {
generateSQLQuery();
}
}, []);
return (
<Stack className="tab-pane" style={{ padding: 24, width: "100%", height: "100%" }}>
<Stack horizontal verticalAlign="center">
<Image src={CopilotIcon} />
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
</Stack>
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%" }}>
<TextField
value={userInput}
onChange={(_, newValue) => setUserInput(newValue)}
style={{ lineHeight: 30 }}
styles={{ root: { width: "90%" } }}
disabled={isGeneratingQuery}
/>
<IconButton
iconProps={{ iconName: "Send" }}
disabled={isGeneratingQuery}
style={{ marginLeft: 8 }}
onClick={() => generateSQLQuery()}
/>
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
</Stack>
<Text style={{ marginTop: 8, marginBottom: 24, fontSize: 12 }}>
AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "}
<Link href="" target="_blank">
Read preview terms
</Link>
</Text>
<Stack style={{ backgroundColor: "#FFF8F0", padding: "2px 8px" }} horizontal verticalAlign="center">
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
{showCallout && !hideFeedbackModalForLikedQueries && (
<Callout
style={{ padding: 8 }}
target="#likeBtn"
onDismiss={() => {
setShowCallout(false);
submitFeedback({ generatedQuery, likeQuery, description: "", userPrompt: userInput });
}}
directionalHint={DirectionalHint.topCenter}
>
<Text>
Thank you. Need to give{" "}
<Link
onClick={() => {
setShowCallout(false);
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userInput);
}}
>
more feedback?
</Link>
</Text>
</Callout>
)}
<IconButton
id="likeBtn"
style={{ marginLeft: 20 }}
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
onClick={() => {
setLikeQuery(true);
setShowCallout(true);
}}
/>
<IconButton
style={{ margin: "0 10px" }}
iconProps={{ iconName: likeQuery === false ? "DislikeSolid" : "Dislike" }}
onClick={() => {
setLikeQuery(false);
setShowCallout(false);
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userInput);
}}
/>
<Separator vertical style={{ color: "#EDEBE9" }} />
<CommandBarButton iconProps={{ iconName: "Copy" }} style={{ margin: "0 10px", backgroundColor: "#FFF8F0" }}>
Copy code
</CommandBarButton>
<CommandBarButton iconProps={{ iconName: "Delete" }} style={{ backgroundColor: "#FFF8F0" }}>
Delete code
</CommandBarButton>
</Stack>
<Stack className="tabPaneContentContainer">
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
<EditorReact
language={"sql"}
content={query}
isReadOnly={false}
ariaLabel={"Editing Query"}
lineNumbers={"on"}
onContentChanged={(newQuery: string) => setQuery(newQuery)}
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
/>
<QueryResultSection
isMongoDB={false}
queryEditorContent={selectedQuery || query}
error={errorMessage}
queryResults={queryResults}
isExecuting={isExecuting}
executeQueryDocumentsPage={(firstItemIndex: number) => queryDocumentsPerPage(firstItemIndex, queryIterator)}
/>
</SplitterLayout>
</Stack>
</Stack>
);
};

View File

@@ -0,0 +1,36 @@
import { QueryCopilotSampleContainerSchema } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils";
interface FeedbackParams {
likeQuery: boolean;
generatedQuery: string;
userPrompt: string;
description?: string;
contact?: string;
}
export const submitFeedback = async (params: FeedbackParams): Promise<void> => {
try {
const { likeQuery, generatedQuery, userPrompt, description, contact } = params;
const payload = {
containerSchema: QueryCopilotSampleContainerSchema,
like: likeQuery ? "like" : "dislike",
generatedSql: generatedQuery,
userPrompt,
description: description || "",
contact: contact || "",
};
const response = await fetch("https://copilotorchestrater.azurewebsites.net/feedback", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify(payload),
});
// eslint-disable-next-line no-console
console.log(response);
} catch (error) {
handleError(error, "copilotSubmitFeedback");
}
};

View File

@@ -0,0 +1,198 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Query Copilot Carousel snapshot test should render when isOpen is true 1`] = `
<Modal
isOpen={true}
styles={
Object {
"main": Object {
"width": 880,
},
}
}
>
<Stack
style={
Object {
"padding": 16,
}
}
>
<Stack
horizontal={true}
horizontalAlign="space-between"
>
<Text
variant="xLarge"
>
What exactly is copilot?
</Text>
<CustomizedIconButton
iconProps={
Object {
"iconName": "Cancel",
}
}
onClick={[Function]}
/>
</Stack>
<Stack
style={
Object {
"marginTop": 8,
}
}
>
<Text
style={
Object {
"fontSize": 13,
}
}
>
A couple of lines about copilot and the background about it. The idea is to have some text to give context to the user.
</Text>
<Text
style={
Object {
"fontSize": 14,
"fontWeight": 600,
"marginTop": 16,
}
}
>
How do you use copilot
</Text>
<Text
style={
Object {
"fontSize": 13,
"marginTop": 8,
}
}
>
To generate queries , just describe the query you want and copilot will generate the query for you.Watch this video to learn more about how to use copilot.
</Text>
<Image
src=""
style={
Object {
"margin": "16px auto",
}
}
/>
<Text
style={
Object {
"fontSize": 14,
"fontWeight": 600,
}
}
>
What is copilot good at
</Text>
<Text
style={
Object {
"fontSize": 13,
"marginTop": 8,
}
}
>
A couple of lines about what copilot can do and its capablites with a link to
<StyledLinkBase
href=""
target="_blank"
>
documentation
</StyledLinkBase>
if possible.
</Text>
<Text
style={
Object {
"fontSize": 14,
"fontWeight": 600,
"marginTop": 16,
}
}
>
What are its limitations
</Text>
<Text
style={
Object {
"fontSize": 13,
"marginTop": 8,
}
}
>
A couple of lines about what copilot cant do and its limitations.
<StyledLinkBase
href=""
target="_blank"
>
Link to documentation
</StyledLinkBase>
</Text>
<Text
style={
Object {
"fontSize": 14,
"fontWeight": 600,
"marginTop": 16,
}
}
>
Disclaimer
</Text>
<Text
style={
Object {
"fontSize": 13,
"marginTop": 8,
}
}
>
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
<StyledLinkBase
href=""
target="_blank"
>
Read preview terms
</StyledLinkBase>
</Text>
</Stack>
<Separator
styles={
Object {
"root": Object {
"height": 1,
"padding": "16px 0",
"selectors": Object {
"::before": Object {
"background": undefined,
},
},
},
}
}
/>
<Stack
horizontal={true}
horizontalAlign="start"
verticalAlign="center"
>
<CustomizedPrimaryButton
disabled={false}
onClick={[Function]}
text="Next"
/>
</Stack>
</Stack>
</Modal>
`;

View File

@@ -0,0 +1,171 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Prompt card snapshot test should render properly if isSelected is false 1`] = `
<Stack
horizontal={true}
style={
Object {
"border": "1px solid #F3F2F1",
"boxShadow": "0px 1.6px 3.6px rgba(0, 0, 0, 0.132), 0px 0.3px 0.9px rgba(0, 0, 0, 0.108)",
"boxSizing": "border-box",
"height": 100,
"padding": "16px 0 16px 16px ",
"width": 650,
}
}
>
<StackItem
grow={1}
>
<Stack
horizontal={true}
>
<div>
<Text
style={
Object {
"background": "#F8FFF0",
"color": "#00A2AD",
"fontSize": 13,
}
}
>
Prompt
</Text>
</div>
<Text
style={
Object {
"fontSize": 13,
"marginLeft": 16,
}
}
>
TestHeader
</Text>
</Stack>
<Text
style={
Object {
"fontSize": 10,
"marginTop": 16,
}
}
>
TestDescription
</Text>
</StackItem>
<StackItem
style={
Object {
"marginLeft": 16,
}
}
>
<StyledChoiceGroup
onChange={[Function]}
options={
Array [
Object {
"key": "selected",
"text": "",
},
]
}
selectedKey=""
styles={
Object {
"flexContainer": Object {
"width": 36,
},
}
}
/>
</StackItem>
</Stack>
`;
exports[`Prompt card snapshot test should render properly if isSelected is true 1`] = `
<Stack
horizontal={true}
style={
Object {
"border": "1px solid #F3F2F1",
"boxShadow": "0px 1.6px 3.6px rgba(0, 0, 0, 0.132), 0px 0.3px 0.9px rgba(0, 0, 0, 0.108)",
"boxSizing": "border-box",
"height": 100,
"padding": "16px 0 16px 16px ",
"width": 650,
}
}
>
<StackItem
grow={1}
>
<Stack
horizontal={true}
>
<div>
<Text
style={
Object {
"background": "#F8FFF0",
"color": "#00A2AD",
"fontSize": 13,
}
}
>
Prompt
</Text>
</div>
<Text
style={
Object {
"fontSize": 13,
"marginLeft": 16,
}
}
>
TestHeader
</Text>
</Stack>
<Text
style={
Object {
"fontSize": 10,
"marginTop": 16,
}
}
>
TestDescription
</Text>
</StackItem>
<StackItem
style={
Object {
"marginLeft": 16,
}
}
>
<StyledChoiceGroup
onChange={[Function]}
options={
Array [
Object {
"key": "selected",
"text": "",
},
]
}
selectedKey="selected"
styles={
Object {
"flexContainer": Object {
"width": 36,
},
}
}
/>
</StackItem>
</Stack>
`;

View File

@@ -0,0 +1,211 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Query copilot tab snapshot test should render with initial input 1`] = `
<Stack
className="tab-pane"
style={
Object {
"height": "100%",
"padding": 24,
"width": "100%",
}
}
>
<Stack
horizontal={true}
verticalAlign="center"
>
<Image
src=""
/>
<Text
style={
Object {
"fontSize": 16,
"fontWeight": 600,
"marginLeft": 8,
}
}
>
Copilot
</Text>
</Stack>
<Stack
horizontal={true}
style={
Object {
"marginTop": 16,
"width": "100%",
}
}
verticalAlign="center"
>
<StyledTextFieldBase
disabled={false}
onChange={[Function]}
style={
Object {
"lineHeight": 30,
}
}
styles={
Object {
"root": Object {
"width": "90%",
},
}
}
value="Write a query to return all records in this table"
/>
<CustomizedIconButton
disabled={false}
iconProps={
Object {
"iconName": "Send",
}
}
onClick={[Function]}
style={
Object {
"marginLeft": 8,
}
}
/>
</Stack>
<Text
style={
Object {
"fontSize": 12,
"marginBottom": 24,
"marginTop": 8,
}
}
>
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
<StyledLinkBase
href=""
target="_blank"
>
Read preview terms
</StyledLinkBase>
</Text>
<Stack
horizontal={true}
style={
Object {
"backgroundColor": "#FFF8F0",
"padding": "2px 8px",
}
}
verticalAlign="center"
>
<Text
style={
Object {
"fontSize": 12,
"fontWeight": 600,
}
}
>
Provide feedback on the query generated
</Text>
<CustomizedIconButton
iconProps={
Object {
"iconName": "Like",
}
}
id="likeBtn"
onClick={[Function]}
style={
Object {
"marginLeft": 20,
}
}
/>
<CustomizedIconButton
iconProps={
Object {
"iconName": "Dislike",
}
}
onClick={[Function]}
style={
Object {
"margin": "0 10px",
}
}
/>
<Separator
style={
Object {
"color": "#EDEBE9",
}
}
vertical={true}
/>
<CustomizedCommandBarButton
iconProps={
Object {
"iconName": "Copy",
}
}
style={
Object {
"backgroundColor": "#FFF8F0",
"margin": "0 10px",
}
}
>
Copy code
</CustomizedCommandBarButton>
<CustomizedCommandBarButton
iconProps={
Object {
"iconName": "Delete",
}
}
style={
Object {
"backgroundColor": "#FFF8F0",
}
}
>
Delete code
</CustomizedCommandBarButton>
</Stack>
<Stack
className="tabPaneContentContainer"
>
<t
customClassName=""
onDragEnd={null}
onDragStart={null}
onSecondaryPaneSizeChange={null}
percentage={false}
primaryIndex={0}
primaryMinSize={100}
secondaryMinSize={200}
vertical={true}
>
<EditorReact
ariaLabel="Editing Query"
content=""
isReadOnly={false}
language="sql"
lineNumbers="on"
onContentChanged={[Function]}
onContentSelected={[Function]}
/>
<QueryResultSection
error=""
executeQueryDocumentsPage={[Function]}
isExecuting={false}
isMongoDB={false}
queryEditorContent=""
/>
</t>
</Stack>
</Stack>
`;

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