Compare commits

...

70 Commits

Author SHA1 Message Date
Archie Agarwal
a96d679087 clean up the code 2025-06-16 15:16:47 +05:30
Archie Agarwal
dc8bb69359 Initial changes for index 2025-06-11 18:54:36 +05:30
JustinKol
64533b445f Autoscale min/max UX changes (#2162)
* master pull

* Better VSCode detection

* Prettier run

* Update .npmrc

* Update settings.json

* Fixed ESLint error

* Changed the VSCode detection to a test url that will not open if successful

* Initial UX changes to Add Collection Panel

* Removing changes from other branch

* Reverting explorer changes

* Snapshot updates and Lint fixes

* Formatting fixes

* Setting separator spacing the same

* Update test snapshot

* Reverting Manual to old UI
2025-06-04 10:40:57 -04:00
Dmitry Shilov
d7bdd0032e fix: Activate the last opened React tab after closing active tab (#2156) 2025-06-04 11:10:44 +02:00
SATYA SB
372ac6921f fix: Enhance splash screen layout with responsive design for stack elements (#2159)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2025-06-04 10:29:45 +05:30
SATYA SB
c6eda097fc [Supporting the Platform - Azure Cosmos DB- Data Explorer - Graphs]: When page viewport is set to 320x256px, Content under 'Notification' section is not properly visible. (#2161)
* [accessibility-3739643]: [Supporting the Platform - Azure Cosmos DB- Data Explorer - Graphs]: When page viewport is set to 320x256px, Content under 'Notification' section is not properly visible.

* fix: Adjust notification console styles for better responsiveness.

---------

Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2025-06-04 10:27:12 +05:30
Dmitry Shilov
05d02f08fa feat: Integrate Fabric Native support for throughput management (#2166) 2025-06-03 08:47:00 +02:00
jawelton74
ab4f02f74a Remove frame-src from CSP. (#2169) 2025-05-29 10:54:37 -07:00
asier-isayas
0fc6647627 Upgrade Cosmos SDK to v4.3 (#2137)
* Upgrade Cosmos SDK to v4.3

* push pkg lock

* fix tests

* fix package-lock.json

* fix package-lock

* fix package-lock.json

* fix package-lock.json

* log console warning when RU limit is reached

* fix tests

* fix format

* fix tests

* added description to RU limit message

* show warning icon on tab header

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-05-29 11:35:06 -04:00
jawelton74
c5ed537109 Fix invalid reference in frame-src and add delimiter for (#2168)
frame-ancestors.
2025-05-29 07:27:15 -07:00
jawelton74
db322ccb59 Add login.microsoftonline.com to CSP. (#2167) 2025-05-28 12:25:14 -07:00
sunghyunkang1111
2d7631c358 Added throughput buckets in the scale update (#2164) 2025-05-27 11:54:20 -05:00
JustinKol
e401c88df6 Add query results, export to json/csv button (#2143)
* master pull

* Added export to json button

* Update .npmrc

* Update settings.json

* Update .npmrc

* Update .npmrc

* revert .npmrc file

* Added export to csv

* Prettier run

* Disable react/prop-types ESLint check

* Changed to download icon

* Added titles

* Switched to download icon already present

* Fixed download title

* Added check for all unique headers and added seperator header for excel only

* Moved to inline dropdown under download button

* Capitalized CSV and JSON

* Fixed where format wasn't updating before exporting

* removed testing console log

* Removed unnecessary async

* Added csv escaping

* Removing unnecessary escape character

* Separated into different functions for better organization and readability

* Fixed any value
2025-05-21 07:54:34 -04:00
asier-isayas
f14b574527 Enable Full Text Search on all NoSQL accounts (#2157)
* Enable Full Text Search on all NoSQL accounts

* format

* fix tests

* run tests

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-05-20 10:49:09 -04:00
Dmitry Shilov
45513e5e1b fix: Update grid after uploading documents (#2131)
- Refactor UploadItemsPane to support onUpload callback
- Enhance bulk insert result type
- Insert uploaded documents to the grid
2025-05-19 11:35:04 +02:00
Dmitry Shilov
15154dfd6a fix: Implement abort functionality for bulk document deletion (#2139) 2025-05-19 11:34:44 +02:00
Nishtha Ahuja
7aeb682bea Connect with VScode on Quickstart Page (#2151)
Co-authored-by: nishthaAhujaa <nishtha17354@iiittd.ac.in>
2025-05-15 17:55:51 +05:30
SATYA SB
35051bace5 [Screen reader - Azure Cosmos DB-New Container]: Screen reader does not announce the associated label information for the 'Estimated monthly cost' info icon under 'New Container' blade. (#2091)
* [accessibility-3690553]:[Screen reader - Azure Cosmos DB-New Container]: Screen reader does not announce the associated label information for the 'Estimated monthly cost' info icon under 'New Container' blade.

* snap updated.

---------

Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2025-05-14 22:45:44 +05:30
JustinKol
5fc53a7f89 [BUG] Prevent dialog from opening if VS Code is open (#2145)
* master pull

* Reverting .npmrc file

* Removed logging userContext

* Prettier run

* Added support for opening CosmosDB Account without clicking database tab

* Reverting change in settings.json

* Prettier run

* Added check if the link closed

* Added check if the link didn't closed

* Check if VS Code was opened, if not popup with download button link

* Prettier run

* Redirect to Download VS Code if not opened

* Added error message to VS Code timeout and redirect

* Fixing baseUrl from testing

* Increased timeout for when user is asked to open VS Code

* switched to iframe for redirects

* Fixed VS Code url

* Removed insider url

* Added log messages

* Added link to vCore data explorer dashboard

* Increased timeout to 2.5 secs to see if that helps with VS Code open popup

* Changed to dialog box

* Changed param name

* Increase startTime for extra popup

* Changed to dialog box only when no VS Code detected

* Fixed vscode url

* Changed title back to Open CosmosDB in VS Code

* Added text on required extensions

* Removed text on required extensions as it will prompt by default

* Fixed wording and Primary Button timeout

* Spelled out VS Code

* Removed console log of timeout

* Updated snapshots and lowered timeout

* Remove VS Code button from Gremlin

* Prettier run on CommandBarComponentButtonFactory

* Changed from referencing location to a link

* Prettier run

* Reverting back to popup for opening

* Updated unit test snapshots

* Added vscode: to Content Security Policy

* Reverting back to popup only if opening times out

* Corrected misspelled url

* Corrected url

* Added event listener to check if DE is in focus or not, to prevent showing dialog when VS Code is opened

* Prettier and url fix

* Moved closeDialog before removing event listener

* Changed handleFocus to a const rather than function

* Changed listener to document

* Decreased timeout time

* Reverting back to popup by default as too many factors are present using a timeout
2025-05-14 10:01:39 -04:00
SATYA SB
ed83bf47e4 [accessibility-3556762]:[Screen Reader- Azure Cosmos DB- Data Explorer]: Two H1 heading are defined on the page. (#2061)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2025-05-14 15:29:08 +05:30
SATYA SB
d657c4919e [Supporting the platform - Azure Cosmos DB - Data Explorer]: All the controls present under 'Data Explorer' page are truncated after setting the viewport to 320*256 pixel. (#2092)
* [accessibility-2278267]:[Supporting the platform - Azure Cosmos DB - Data Explorer]: All the controls present under 'Data Explorer' page are truncated after setting the viewport to 320*256 pixel.

* feat: implement zoom level hook and update components for responsive design.

* Format fixed.

* feat: add conditionalClass utility and refactor className assignments for improved readability.

---------

Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2025-05-14 10:16:19 +05:30
sunghyunkang1111
95d33356c3 Hide throughput bucket settings for nonshared, nondedicated throughput container (#2146) 2025-05-13 18:58:42 -05:00
sunghyunkang1111
1081432bbd Added AFEC check in userContext (#2140)
* Added AFEC check in userContext

* Update unit tests and fix Group to Bucket
2025-05-13 11:26:47 -05:00
Nishtha Ahuja
44d815454c for manual isAutoscale is supposed to be false (#2141)
Co-authored-by: nishthaAhujaa <nishtha17354@iiittd.ac.in>
2025-05-13 20:04:12 +05:30
JustinKol
6d604490d3 URL Correction in VS Code link (#2142)
* master pull

* Reverting .npmrc file

* Removed logging userContext

* Prettier run

* Added support for opening CosmosDB Account without clicking database tab

* Reverting change in settings.json

* Prettier run

* Added check if the link closed

* Added check if the link didn't closed

* Check if VS Code was opened, if not popup with download button link

* Prettier run

* Redirect to Download VS Code if not opened

* Added error message to VS Code timeout and redirect

* Fixing baseUrl from testing

* Increased timeout for when user is asked to open VS Code

* switched to iframe for redirects

* Fixed VS Code url

* Removed insider url

* Added log messages

* Added link to vCore data explorer dashboard

* Increased timeout to 2.5 secs to see if that helps with VS Code open popup

* Changed to dialog box

* Changed param name

* Increase startTime for extra popup

* Changed to dialog box only when no VS Code detected

* Fixed vscode url

* Changed title back to Open CosmosDB in VS Code

* Added text on required extensions

* Removed text on required extensions as it will prompt by default

* Fixed wording and Primary Button timeout

* Spelled out VS Code

* Removed console log of timeout

* Updated snapshots and lowered timeout

* Remove VS Code button from Gremlin

* Prettier run on CommandBarComponentButtonFactory

* Changed from referencing location to a link

* Prettier run

* Reverting back to popup for opening

* Updated unit test snapshots

* Added vscode: to Content Security Policy

* Reverting back to popup only if opening times out

* Corrected misspelled url

* Corrected url
2025-05-13 10:26:54 -04:00
asier-isayas
34edd96c76 Default New Global Secondary Index Panel to be sharded (#2138)
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-05-12 13:12:20 -04:00
JustinKol
7c0aae6ffa Open VS Code via Data Explorer button (#2116)
* master pull

* Reverting .npmrc file

* Removed logging userContext

* Prettier run

* Added support for opening CosmosDB Account without clicking database tab

* Reverting change in settings.json

* Prettier run

* Added check if the link closed

* Added check if the link didn't closed

* Check if VS Code was opened, if not popup with download button link

* Prettier run

* Redirect to Download VS Code if not opened

* Added error message to VS Code timeout and redirect

* Fixing baseUrl from testing

* Increased timeout for when user is asked to open VS Code

* switched to iframe for redirects

* Fixed VS Code url

* Removed insider url

* Added log messages

* Added link to vCore data explorer dashboard

* Increased timeout to 2.5 secs to see if that helps with VS Code open popup

* Changed to dialog box

* Changed param name

* Increase startTime for extra popup

* Changed to dialog box only when no VS Code detected

* Fixed vscode url

* Changed title back to Open CosmosDB in VS Code

* Added text on required extensions

* Removed text on required extensions as it will prompt by default

* Fixed wording and Primary Button timeout

* Spelled out VS Code

* Removed console log of timeout

* Updated snapshots and lowered timeout

* Remove VS Code button from Gremlin

* Prettier run on CommandBarComponentButtonFactory

* Changed from referencing location to a link

* Prettier run

* Reverting back to popup for opening

* Updated unit test snapshots

* Added vscode: to Content Security Policy

* Reverting back to popup only if opening times out
2025-05-12 10:55:06 -04:00
JustinKol
86e8bf3c80 Show unique keys in Settings for SQL api (#2136)
* master pull

* Added unique keys in Settings for SQL api

* Revert settings.json

* Reverting other PR changes that haven't merged

* Adding space back in

* Added unit tests
2025-05-09 12:37:56 -04:00
Dmitry Shilov
e98c9a83b8 fix: Add collapsible feature to Accordion in SettingsPane (#2125) 2025-05-09 12:13:17 +02:00
Dmitry Shilov
7d57a90d50 fix: Correct documentId method call in error logging for deletion failures (#2132) 2025-05-09 12:12:32 +02:00
Dmitry Shilov
0f896f556b feat: Enhance UploadItemsPane with error handling and status icons for file uploads (#2133) 2025-05-09 12:11:26 +02:00
sunghyunkang1111
985c744198 get the user defined system key value for updating (#2134)
* get the user defined system key value for updating

* Added the systemkey check for non-defined system key
2025-05-08 09:14:09 -05:00
Sourabh Jain
2dbec019af CloudShell: Changed User Consent Message and Add appName in commands (#2130)
* message change

* updated commands
2025-05-08 06:41:35 +05:30
Laurent Nguyen
2fa95a281e Disable "Learn more" link for now in Fabric Home (#2129) 2025-05-07 11:52:41 +02:00
Sourabh Jain
ea6f3d1579 Cloudshell: Few Enhancement (#2128)
* few enhancement

* fix time
2025-05-05 21:17:36 +05:30
Dmitry Shilov
f9b0abdd14 fix: Add overflow property and set minimum heights for flex and sidebar containers (#2124)
* fix: Add overflow property and set minimum heights for flex and sidebar containers

* fix: Update overflow and minimum height properties for tab panes and containers
2025-05-05 15:50:43 +02:00
asier-isayas
10cda21401 Add Vector Index Shard Key option on container creation (#2097)
* Add vector index shard key

* npm run format

* rename shard key to vector index shard key

* add tooltip for quantization byte size

* change text for GSI and container in VectorEmbedding Policy

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-05-02 11:05:40 -04:00
Sourabh Jain
205355bf55 Shell: Integrate Cloudshell to existing shells (#2098)
* first draft

* refactored code

* ux fix

* add custom header support and fix ui

* minor changes

* hide last command also

* remove logger

* bug fixes

* updated loick file

* fix tests

* moved files

* update readme

* documentation update

* fix compilationerror

* undefined check handle

* format fix

* format fix

* fix lints

* format fix

* fix unrelatred test

* code refator

* fix format

* ut fix

* cgmanifest

* Revert "cgmanifest"

This reverts commit 2e76a6926ee0d3d4e0510f2e04e03446c2ca8c47.

* fix snap

* test fix

* formatting code

* updated xterm

* include username in command

* cloudshell add exit

* fix test

* format fix

* tets fix

* fix multiple open cloudshell calls

* socket time out after 20 min

* remove unused code

* 120 min

* Addressed comments
2025-04-30 13:19:01 -07:00
sunghyunkang1111
bb66deb3a4 Added more test cases and fix system partition key load issue (#2126)
* Added more test cases and fix system partition key load issue

* Fix unit tests and fix ci

* Updated test snapsho
2025-04-30 15:18:11 -05:00
Laurent Nguyen
fe73d0a1c6 Fix Fabric Native ReadOnly mode (#2123)
* Add FabricNativeReadOnly mode

* Hide Settings for Fabric native readonly

* Fix strict compil
2025-04-30 17:37:54 +02:00
sakshigupta12feb
e90e1fc581 Updated the Migrate data link (#2122)
* updated the Migrate data link

* updated the Migrate data link (removed en-us)

---------

Co-authored-by: Sakshi Gupta <sakshig+microsoft@microsoft.com>
2025-04-30 17:48:15 +05:30
Nishtha Ahuja
8bcad6e0e0 Emulator Quickstart Tutorials (#2121)
* updated all outdated sample apps
Co-authored-by: nishthaAhujaa <nishtha17354@iiittd.ac.in>
2025-04-30 13:32:53 +05:30
SATYA SB
9f3236c29c [accessibility-3560183]:[Screen reader - Cosmos DB Query Copilot - Query Faster with Copilot>Enable Query Advisor]: Screen reader does not announce the dialog information on invoking 'Clear editor' button. (#2068)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2025-04-30 11:35:58 +05:30
Laurent Nguyen
2f858ecf9b Fabric native improvements: Settings pane, Partition Key settings tab, sample data and message contract (#2119)
* Hide entire Accordion of options in Settings Pane

* In PartitionKeyComponent hide "Change partition key" label when read-only.

* Create sample data container with correct pkey

* Add unit tests to PartitionKeyComponent

* Fix format

* fix unit test snapshot

* Add Fabric message to open Settings to given tab id

* Improve syntax on message contract

* Remove "(preview)" in partition key tab title in Settings Tab
2025-04-29 17:50:20 +02:00
sunghyunkang1111
274c85d2de Added document test skips (#2120) 2025-04-28 13:42:18 -05:00
asier-isayas
d9436be61b Remove references to old Portal Backend (#2109)
* remove old portal backend endpoints

* format

* fix tests

* remove Materialized Views from createResourceTokenTreeNodes

* add portal FE back to defaultAllowedBackendEndpoints

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-04-28 13:29:27 -04:00
Nishtha Ahuja
6db2536a61 fixed quickstart tab in emulator (#2115)
Co-authored-by: nishthaAhujaa <nishtha17354@iiittd.ac.in>
2025-04-28 21:11:27 +05:30
Sourabh Jain
714f38a1be Mongo RU Schema Analyzer Deprecation (#2117)
* remove menu item

* remove unused import
2025-04-27 20:43:24 -05:00
asier-isayas
af4e1d10b4 GSI: Remove Unique Key Policy and Manual Throughput (#2114)
* Remove Unique Key Policy and Manual Throughput

* fix tests

* remove manual throughput option from scale & settings

* fix test

* cleanup

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-04-18 14:39:31 -04:00
Laurent Nguyen
6dbc412fa6 Implement Sample data import for Fabric Home (#2101)
* Implement dialog to import sample data

* Fix format

* Cosmetic fixes

* fix: update help link to point to the new documentation URL

---------

Co-authored-by: Sevo Kukol <sevoku@microsoft.com>
2025-04-16 14:27:22 -07:00
sunghyunkang1111
3470f56535 Pk missing fix (#2107)
* fix partition key missing not being able to load the document

* Implement E2E tests for documents with different partitionkeys

* Implement E2E tests for documents with different partitionkeys

* Implement E2E tests for documents with different partitionkeys

* Updated snapshot

* Updated tests for MongoRU and add create/delete tests

* Fixing system partition key showing up in Data Explorer
2025-04-16 13:12:53 -05:00
Vsevolod Kukol
00ec678569 fix: handle optional activeTab in tab activation logic (#2106) 2025-04-16 10:34:15 -07:00
Laurent Nguyen
27c9ea7ab6 Add tracing events for tracking query execution and upload documents (#2100)
* Add tracing for Execute (submit) query and Upload documents

* Trace query closer to iterator operation

* Add warnings to values used in Fabric

* Fix format

* Don't trace execute queries for filtering calls

* Remove tracing call for documents tab filtering

* Fix failing unit test.

---------

Co-authored-by: Jade Welton <jawelton@microsoft.com>
Co-authored-by: Sevo Kukol <sevoku@microsoft.com>
2025-04-16 10:31:43 -07:00
asier-isayas
d5fe2d9e9f Fix Unit and E2E tests (#2112)
* fix tests

* remove Materialized Views from createResourceTokenTreeNodes

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-04-16 10:05:36 -04:00
Laurent Nguyen
94b1e729d1 Enable original azure resource tree style for Fabric native and turn on Settings tab (#2103)
* Enable original azure resource tree for Fabric native and turn on Settings page

* Fix unit tests
2025-04-15 08:49:16 -07:00
Laurent Nguyen
afdbefe36c Implement refreshResourceTree message and hide refresh button above resource tree for Fabric native (#2102) 2025-04-15 08:48:44 -07:00
asier-isayas
2cff0fc3ff Global Secondary Index (#2071)
* add Materialized Views feature flag

* fetch MV properties from RP API and capture them in our data models

* AddMaterializedViewPanel

* undefined check

* subpartition keys

* Partition Key, Throughput, Unique Keys

* All views associated with a container (#2063) and Materialized View Target Container (#2065)

Identified Source container and Target container
Created tabs in Scale and Settings respectively
Changed the Icon of target container

* Add MV Panel

* format

* format

* styling

* add tests

* tests

* test files (#2074)

Co-authored-by: nishthaAhujaa

* fix type error

* fix tests

* merge conflict

* Panel Integration (#2075)

* integrated panel

* edited header text

---------

Co-authored-by: nishthaAhujaa <nishtha17354@iiittd.ac.in>
Co-authored-by: Asier Isayas <aisayas@microsoft.com>

* updated tests (#2077)

Co-authored-by: nishthaAhujaa

* fix tests

* update treeNodeUtil test snap

* update settings component test snap

* fixed source container in global "New Materialized View"

* source container check (#2079)

Co-authored-by: nishthaAhujaa

* renamed Materialized Views to Global Secondary Index

* more renaming

* fix import

* fix typo

* disable materialized views for Fabric

* updated input validation

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
Co-authored-by: Nishtha Ahuja <45535788+nishthaAhujaa@users.noreply.github.com>
Co-authored-by: nishthaAhujaa <nishtha17354@iiittd.ac.in>
2025-04-11 10:39:32 -04:00
sunghyunkang1111
a3bfc89318 Revert "Pk missing fix (#2094)" (#2099)
This reverts commit af0a890516.
2025-04-09 13:04:44 -05:00
Ajay Parulekar
0666e11d89 Renaming Materialized views builder blade text to Global secondary indexes for NoSql API (#1991)
* GSI changes

* GSI changes

* GSI changes

* updating GlobalSecondaryIndexesBuilder.json

* Changes

* Update cost text keys based on user context API type

* Refactor Materialized Views Builder code for improved readability and consistency in API type checks

* Update links in Materialized Views Builder for consistency and accuracy

* Update Global Secondary Indexes links and descriptions for clarity and accuracy based on API type

* Update portal notification message keys based on user context API type for Materialized Views Builder

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Fix capitalization and wording inconsistencies in Materialized Views Builder localization strings

* Fix capitalization and wording inconsistencies in localization strings for Materialized Views Builder

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

* Update src/Localization/en/MaterializedViewsBuilder.json

Co-authored-by: Justine Cocchi <justine@cocchi.org>

---------

Co-authored-by: Justine Cocchi <justine@cocchi.org>
2025-04-09 13:32:35 -04:00
bogercraig
9bb1d0bace Manual Region Selection (#2037)
* Add standin region selection to settings menu.

* Retrieve read and write regions from user context and populate dropdown menu.  Update local storage value.
Need to now connect with updating read region of primary cosmos client.

* Change to only selecting region for cosmos client.  Not setting up separate read and write clients.

* Add read and write endpoint logging to cosmos client.

* Pass changing endpoint from settings menu to client.  Encountered token issues using new endpoint in client.

* Rough implementation of region selection of endpoint for cosmos client.  Still need to:
1 - Use separate context var to track selected region.  Directly updating database account context throws off token generation by acquireMSALTokenForAccount
2 - Remove href overrides in acquireMSALTokenForAccount.

* Update region selection to include global endpoint and generate a unique list of read and write endpoints.
Need to continue with clearing out selected endpoint when global is selected again.
Write operations stall when read region is selected even though 403 returned when region rejects operation.
Need to limit feature availablility to nosql, table, gremlin (maybe).

* Update cosmos client to fix bug.
Clients continuously generate after changing RBAC setting.

* Swapping back to default endpoint value.

* Rebase on client refresh bug fix.

* Enable region selection for NoSql, Table, Gremlin

* Add logic to reset regional endpoint when global is selected.

* Fix state changing when selecting region or resetting to global.

* Rough implementation of configuring regional endpoint when DE is loaded in portal or hosted with AAD/Entra auth.

* Ininitial attempt at adding error handling, but still having issues with errors caught at proxy plugin.

* Added rough error handling in local requestPlugin used in local environments.  Passes new error to calling code.
Might need to add specific error handling for request plugin to the handleError class.

* Change how request plugin returns error so existing error handling utility can process and present error.

* Only enable region selection for nosql accounts.

* Limit region selection to portal and hosted AAD auth.  SQL accounts only.  Could possibly enable on table and gremlin later.

* Update error handling to account for generic error code.

* Refactor error code extraction.

* Update test snapshots and remove unneeded logging.

* Change error handling to use only the message rather than casting to any.

* Clean up debug logging in cosmos client.

* Remove unused storage keys.

* Use endpoint instead of region name to track selected region.  Prevents having to do endpoint lookups.

* Add initial button state update depending on region selection.
Need to update with the API and react to user context changes.

* Disable CRUD buttons when read region selected.

* Default to write enabled in react.

* Disable query saving when read region is selected.

* Patch clientWidth error on conflicts tab.

* Resolve merge conflicts from rebase.

* Make sure proxy endpoints return in all cases.

* Remove excess client logging and match main for ConflictsTab.

* Cleaning up logging and fixing endpoint discovery bug.

* Fix formatting.

* Reformatting if statements with preferred formatting.

* Migrate region selection to local persistence.  Fixes account swapping bug.
TODO: Inspect better way to reset interface elements when deleteAllStates is called.  Need to react to regional endpoint being reset.

* Relocate resetting interface context to helper function.

* Remove legacy state storage for regional endpoint selection.

* Laurent suggestion updates.
2025-04-07 09:29:11 -07:00
sunghyunkang1111
af0a890516 Pk missing fix (#2094)
* fix partition key missing not being able to load the document

* Implement E2E tests for documents with different partitionkeys

* Implement E2E tests for documents with different partitionkeys

* Implement E2E tests for documents with different partitionkeys

* Updated snapshot
2025-04-07 10:45:29 -05:00
bogercraig
e3c3a8b1b7 Hide Keys and Connection Strings in Connect Tab (#2095)
* Hide connection strings and keys by default.  Move URI above pivot since common across tabs.  Matches frontend.  Need to add scrolling of keys when window is small.  Possibly reduce URI width.

* Add vertical scrolling when window size reduces.

* Adding missing semicolon at end of connection strings.
2025-04-03 17:19:28 -07:00
sunghyunkang1111
0f6c979268 Update cleanupDBs.js (#2093) 2025-04-03 11:10:40 -05:00
asier-isayas
32576f50d3 Self Serve text render fix (#2088)
* debug

* added comment

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-03-27 14:17:06 -04:00
sunghyunkang1111
10f5a5fbfe Revert "fix partition key missing not being able to load the document (#2085)" (#2090)
This reverts commit 257256f915.
2025-03-27 12:47:14 -05:00
JustinKol
8eb53674dc Add refresh button to Mongo DB RU and adjust ellipsis so refresh button on single column container doesn't hide it (#2089)
* Moved ellipsis to the left for single column containers

* Added refresh to MongoDB RU

* prettier run
2025-03-27 13:44:22 -04:00
sunghyunkang1111
257256f915 fix partition key missing not being able to load the document (#2085) 2025-03-26 11:26:47 -05:00
jawelton74
41f5401016 Fix input validation patterns for resource ids (#2086)
* Fix input element pattern matching and add validation reporting for
cases where the element is not within a form element.

* Update test snapshots.

* Remove old code and fix trigger error message.

* Move id validation to a util class.

* Add unit tests, fix standalone function, rename constants.
2025-03-26 07:10:47 -07:00
Laurent Nguyen
a4c9a47d4e Add comments for expired token used in test (#2084) 2025-03-26 09:00:55 +01:00
JustinKol
c43132d5c0 Adding container item refresh button back to upper right corner of page (#2083)
* Moved button to upper right

* Reverted background color

* Updated test snapshot

* Added hidding refresh button on overflow

* Ran prettier and updated snapshot
2025-03-25 08:16:39 -04:00
186 changed files with 12217 additions and 3247 deletions

View File

@@ -164,24 +164,24 @@ jobs:
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
shardTotal: [16]
steps:
- uses: actions/checkout@v4
- name: "Az CLI login"
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Use Node.js 18.x
uses: actions/setup-node@v4
with:
node-version: 18.x
- run: npm ci
- run: npx playwright install --with-deps
- name: "Az CLI login"
uses: Azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
- name: Upload blob report to GitHub Actions Artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4

View File

@@ -24,5 +24,8 @@
"source.organizeImports": "explicit"
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[typescriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
}
}

8
images/VisualStudio.svg Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

8
images/golang.svg Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none">
<path fill="#8CC5E7" d="M21.4679537,3.20617761 C22.1814672,4.67953668 20.0131274,4.83706564 20.1243243,5.49498069 C20.3281853,6.68108108 20.1891892,8.44169884 20.0316602,10.1745174 C19.7629344,13.1119691 21.9590734,20.1451737 17.3814672,22.9714286 C16.5196911,23.5088803 14.4718147,23.8054054 12.4517375,23.8517375 C12.4517375,23.8517375 12.442471,23.8517375 12.442471,23.8517375 C12.442471,23.8517375 12.4332046,23.8517375 12.4332046,23.8517375 C10.4131274,23.8054054 8.08725869,23.5088803 7.22548263,22.9714286 C2.65714286,20.1451737 4.85328185,13.1119691 4.59382239,10.1745174 C4.42702703,8.44169884 4.28803089,6.68108108 4.5011583,5.49498069 C4.61235521,4.83706564 2.44401544,4.68880309 3.15752896,3.20617761 C3.76911197,1.93667954 5.27953668,3.05791506 5.65945946,2.65945946 C7.596139,0.648648649 9.94980695,0.111196911 11.8030888,0.0648648649 C11.988417,0.0648648649 12.8223938,0.0648648649 12.8223938,0.0648648649 C14.6664093,0.157528958 17.0200772,0.657915058 18.9660232,2.65945946 C19.3459459,3.05791506 20.8471042,1.93667954 21.4679537,3.20617761 Z M11.4324324,10.9065637 C11.3490347,10.9436293 11.2100386,11.8517375 11.6362934,11.8980695 C11.9235521,11.9258687 12.7111969,12.0185328 12.8965251,11.8980695 C13.2579151,11.6664093 13.2208494,11.1104247 13.0169884,10.9714286 C12.6741313,10.7490347 11.5250965,10.8602317 11.4324324,10.9065637 Z M9.07876448,4.10501931 C8.12432432,3.99382239 6.52123552,4.88339768 6.28030888,6.77374517 C6.02084942,8.73822394 8.33745174,10.6841699 10.56139,8.73822394 C11.7567568,7.69111969 12.1737452,4.46640927 9.07876448,4.10501931 Z M15.5281853,4.10501931 C12.4332046,4.46640927 12.8501931,7.69111969 14.0455598,8.73822394 C16.2694981,10.6841699 18.5861004,8.73822394 18.3266409,6.77374517 C18.0949807,4.88339768 16.4918919,3.99382239 15.5281853,4.10501931 Z"/>
<path fill="#B8937F" d="M12.3127413,8.98841699 C12.8965251,8.90501931 14.2957529,9.57220077 14.2030888,10.3598456 C14.0918919,11.2772201 10.5984556,11.3976834 10.4131274,10.3042471 C10.3019305,9.63706564 10.8301158,9.21081081 12.3127413,8.98841699 Z M20.1984556,16.3737452 C19.9111969,16.3644788 19.7258687,15.984556 19.7258687,15.7528958 C19.7258687,15.3359073 19.7814672,14.8447876 20.0872587,14.6316602 C20.7173745,14.196139 21.2177606,16.3830116 20.1984556,16.3737452 Z M4.41776062,16.3737452 C3.3984556,16.3830116 3.8988417,14.196139 4.52895753,14.6316602 C4.83474903,14.8447876 4.89034749,15.3359073 4.89034749,15.7528958 C4.89034749,15.984556 4.70501931,16.3644788 4.41776062,16.3737452 Z M18.2617761,23.0918919 C18.4471042,23.3606178 18.4563707,23.5459459 18.1598456,23.6849421 C17.0293436,24.203861 16.019305,23.5088803 16.3992278,23.3142857 C17.2054054,22.9065637 17.7057915,22.2671815 18.2617761,23.0918919 Z M6.35444015,23.184556 C6.91042471,22.3598456 7.41081081,22.9992278 8.21698842,23.4069498 C8.5969112,23.6015444 7.58687259,24.2965251 6.45637066,23.7776062 C6.15984556,23.63861 6.16911197,23.4532819 6.35444015,23.184556 Z"/>
<path fill="#000000" d="M19.7351351,3.42857143 C19.7814672,3.23397683 20.2633205,3.14131274 20.5320463,3.47490347 C20.8563707,3.87335907 20.0594595,4.42007722 20.0223938,4.1976834 C19.9297297,3.5953668 19.6795367,3.62316602 19.7351351,3.42857143 Z M4.88108108,3.42857143 C4.93667954,3.62316602 4.68648649,3.5953668 4.59382239,4.1976834 C4.55675676,4.42007722 3.75984556,3.87335907 4.08416988,3.47490347 C4.34362934,3.14131274 4.82548263,3.23397683 4.88108108,3.42857143 Z M15.7413127,7.94131274 C15.1578953,7.94131274 14.6849421,7.46835949 14.6849421,6.88494208 C14.6849421,6.30152468 15.1578953,5.82857143 15.7413127,5.82857143 C16.3247301,5.82857143 16.7976834,6.30152468 16.7976834,6.88494208 C16.7976834,7.46835949 16.3247301,7.94131274 15.7413127,7.94131274 Z M15.4633205,6.76447876 C15.6475575,6.76447876 15.7969112,6.61512511 15.7969112,6.43088803 C15.7969112,6.24665096 15.6475575,6.0972973 15.4633205,6.0972973 C15.2790834,6.0972973 15.1297297,6.24665096 15.1297297,6.43088803 C15.1297297,6.61512511 15.2790834,6.76447876 15.4633205,6.76447876 Z M11.3583012,9.43320463 C11.4694981,9.00694981 11.8586873,8.86795367 12.1737452,8.85868726 C12.9799228,8.84015444 13.2857143,9.27567568 13.3135135,9.61853282 C13.369112,10.2023166 11.1081081,10.3413127 11.3583012,9.43320463 Z M8.87490347,7.94131274 C8.29148607,7.94131274 7.81853282,7.46835949 7.81853282,6.88494208 C7.81853282,6.30152468 8.29148607,5.82857143 8.87490347,5.82857143 C9.45832088,5.82857143 9.93127413,6.30152468 9.93127413,6.88494208 C9.93127413,7.46835949 9.45832088,7.94131274 8.87490347,7.94131274 Z M9.15289575,6.76447876 C9.33713283,6.76447876 9.48648649,6.61512511 9.48648649,6.43088803 C9.48648649,6.24665096 9.33713283,6.0972973 9.15289575,6.0972973 C8.96865868,6.0972973 8.81930502,6.24665096 8.81930502,6.43088803 C8.81930502,6.61512511 8.96865868,6.76447876 9.15289575,6.76447876 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

10
images/springboot.svg Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M38.9437824,35.879008 C89.5234256,-13.1200214 170.398168,-11.8028432 219.397197,39.0402357 C224.929346,31.6640377 229.671187,23.4975328 233.095851,15.0675923 C249.165425,64.0666217 258.912543,105.162582 255.224444,137.038295 C253.380395,163.90873 242.842969,189.725423 225.456217,210.273403 C180.145286,264.014274 99.53398,270.863601 45.7931091,225.55267 L45.7931091,225.55267 L44.765,224.638 L44.7103323,224.601984 C44.5420247,224.484832 44.376007,224.362668 44.2124952,224.235492 C43.7219599,223.853965 43.2765312,223.438607 42.8762093,222.995252 L42.732,222.831 L41.0512675,221.3377 C39.4121124,219.93271 37.7729573,218.52772 36.3188215,216.93771 L35.7825547,216.332423 C-13.2164747,165.752779 -11.6358609,84.8780374 38.9437824,35.879008 Z M57.9111486,207.375611 C53.169307,203.687512 46.3199803,204.214383 42.6318814,208.956225 C39.3888978,213.125775 39.4048731,218.924805 42.6798072,222.771269 L42.732,222.831 L44.765,224.638 L44.9644841,224.773953 C49.5691585,227.80174 55.7644273,227.175885 59.2982065,222.896387 L59.4917624,222.654878 C63.1798614,217.913037 62.3895545,211.06371 57.9111486,207.375611 Z M231.778672,28.2393744 C218.60689,55.9001168 185.940871,76.9749681 157.753257,83.5608592 C131.146257,89.8833146 107.963921,84.6146018 83.4644059,94.0982849 C27.6160498,115.436572 28.6697923,181.822354 59.2283268,196.838185 L59.2283268,196.838185 L61.0723763,197.891928 C61.0723763,197.891928 83.1456487,193.50309 104.973663,187.707242 L106.843514,187.207079 C115.561826,184.857554 124.138869,182.296538 131.146257,179.714869 C167.500376,166.279651 207.542593,133.08676 220.714375,94.6251562 C213.865049,134.667374 179.35498,173.392413 144.84491,191.042601 C126.404416,200.526284 112.178891,202.633769 81.883792,213.171195 C78.195693,214.488373 75.297901,215.805551 75.297901,215.805551 C75.6675607,215.754564 76.0372203,215.70481 76.4060145,215.65629 L77.1421925,215.560893 L77.1421925,215.560893 L77.8745239,215.468787 C84.5652297,214.639554 90.5771682,214.224938 90.5771682,214.224938 C133.517178,212.117452 200.956702,226.342977 232.305544,184.45671 C264.444692,141.780136 246.531068,72.7599979 231.778672,28.2393744 Z" fill="#6DB33F">
</path>
<path d="M57.9111486,207.375611 C62.3895545,211.06371 63.1798614,217.913037 59.4917624,222.654878 C55.8036635,227.39672 48.9543368,227.923591 44.2124952,224.235492 C39.4706537,220.547393 38.9437824,213.698066 42.6318814,208.956225 C46.3199803,204.214383 53.169307,203.687512 57.9111486,207.375611 Z M231.778672,28.2393744 C246.531068,72.7599979 264.444692,141.780136 232.305544,184.45671 C200.956702,226.342977 133.517178,212.117452 90.5771682,214.224938 C90.5771682,214.224938 84.5652297,214.639554 77.8745239,215.468787 L77.1421925,215.560893 C76.5300999,215.63902 75.9140004,215.720572 75.297901,215.805551 C75.297901,215.805551 78.195693,214.488373 81.883792,213.171195 C112.178891,202.633769 126.404416,200.526284 144.84491,191.042601 C179.35498,173.392413 213.865049,134.667374 220.714375,94.6251562 C207.542593,133.08676 167.500376,166.279651 131.146257,179.714869 C106.119871,188.935116 61.0723763,197.891928 61.0723763,197.891928 L59.2283268,196.838185 C28.6697923,181.822354 27.6160498,115.436572 83.4644059,94.0982849 C107.963921,84.6146018 131.146257,89.8833146 157.753257,83.5608592 C185.940871,76.9749681 218.60689,55.9001168 231.778672,28.2393744 Z" fill="#FFFFFF">
</path>

After

Width:  |  Height:  |  Size: 3.6 KiB

1
images/vscode.svg Normal file
View File

@@ -0,0 +1 @@
<svg width="15" height="15" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" overflow="hidden"><defs><clipPath id="clip0"><rect x="479" y="279" width="15" height="15"/></clipPath><clipPath id="clip1"><rect x="-0.287396" y="-0.171573" width="152381" height="152381"/></clipPath><image width="35" height="35" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAMAAAApB0NrAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAH4UExURQAAAASExCGs7CC//wB9vQB5uxSU1yaz8ySy9AB8uwB8vAB5uxOR1Say8yax8iaw8gB6vAB6uwB7uwB4uROQ1Cax8ySy8QCAvwB3ugB5ugB2uROP1CWw8yav8wCT2QCQ1QCP1wB2uQB1uBOO0yWv8yat8wCP1QCP1QCP1QCO1QCQ1wB1tQB2uAB3uAB1tx+k6SSt8ySt8QCN0wCO1ACM0wBzuQBztAB1twB0tgB0tiSr8SSs8gCM1ACM1ACEywBwtQBxtAB0tgBztQB0sySp8SSr8gCL0wCK0gCL0wCHzwByuABusgBztQBttiOq8gB6wQCI0QCJ0gB7wySn8SOo8gBxtABtsABwtgCFzwCI0gCG0QCJ0SKn8SKn8iKl8QBvsABvsgBvsQBtsABssgCCzACG0QCF0ACDzyKm8QBtrwBusABsrgCAzQCE0ACF0ACF0ACE0CKj8SKl8QBssQBtrwBtrwBsrgBsrwCK1QCBzwCD0ACA0B2d7CGj8QBsrABrrQBwrwCA3wCC0ACCzwCBzwB+zhGM3iGi8SKh8QCAzQCAzgCAzwB9zRCK3iCh8SGh8SCf8QB+zAB/zgB8zRCJ3SCg8SCf7wB+zQB6zBCI3CCe8CCf8CCe8CCf8SCf/wB7zAB4yxGJ3h6c8CCd7yCf7wmA0RqX60C//5CaUeMAAACodFJOUwA8XAho+//ncID/////53iM//////9wCKv/////gCiYILf///+AMPP/70wYw/+3lP+AXP+MKNv/+3uA/3z/+////+NAgP9c9/////+7HP+A///MgP9Y+////7scgP+AeP/////7/+NA/2D/iyTb//t4gP808//vUBjD/7eU/yifIAi3//////+Ar////////4CI/////3B//////+9/CGj7/+dwEDhYBCm1XqwAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAFpSURBVDhPY2AYBaiAkYkZXQgdsLCysXPgV8XJxc3Dy8vHj0eVgKCQsIioqKioGLoMDIhLSEpKSknLyMjIyKLLyckrgJUoCgsLCyspq6ioqKiiKVFT19DUYmDQ1tEFAT19AwMDA0M0NUbGxsbGJqZm5ubm5haWDFbW1tbWVmhqGGxsbW1t7ewdHB2dnBkYXFxdXV1d0NUwuLl7eHh4enn7+DIwMLj4+fn5Yaph8A8IDAwMDBIHsYNDQkJCgtFVMISGhUdERkZGRkUzMDDExMbGxsahK4lPSExKTklNTU1NS2dgiMvIyMhAV5OZlZWVlZ2Tm5eXl5dfwFBYVFRUVIimpriktKycgaGisgoEqmtqa2tr0dUw1NU3gKjGpuaWlpbWtvb29vYOdDUw0NjZ1dXd09vX39c3AV0OASZOmjR5ytSpU6dOQ5dBAtN7ZsycNXvO3HnoEshg/oKFixYvQRdFA0uXLUcXGqEAAH4FV0z+qQbjAAAAAElFTkSuQmCC" preserveAspectRatio="none" id="img2"></image><clipPath id="clip3"><path d="M44291.4 46947.4 187148 46947.4 187148 188823 44291.4 188823Z" fill-rule="evenodd" clip-rule="evenodd"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-479 -279)"><g clip-path="url(#clip1)" transform="matrix(0.000105 0 0 0.000105 479 279)"><g clip-path="url(#clip3)" transform="matrix(1 0 0 1.00692 -44291.4 -47272.4)"><use width="100%" height="100%" xlink:href="#img2" transform="scale(6709.45 6709.45)"></use></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -2869,6 +2869,7 @@ a:link {
z-index: 1000;
overflow-y: auto;
overflow-x: clip;
min-height: fit-content;
}
.uniqueIndexesContainer {

View File

@@ -211,3 +211,12 @@ a:focus {
.fileImportImg img {
filter: brightness(0) saturate(100%);
}
.tabPanesContainer {
overflow: auto !important;
}
.tabs-container {
min-height: 500px;
min-width: 500px;
}

View File

@@ -130,19 +130,19 @@ menuQuickStart {
.content {
display: inline-block;
width: 100%;
transition: all .4s ease-in-out;
-ms-transition: all .4s ease-in-out;
-webkit-transition: all .4s ease-in-out;
-moz-transition: all .4s ease-in-out;
transition: all 0.4s ease-in-out;
-ms-transition: all 0.4s ease-in-out;
-webkit-transition: all 0.4s ease-in-out;
-moz-transition: all 0.4s ease-in-out;
height: 100vh;
}
.mini {
width: 0%;
float: left;
transition: all .4s ease-in-out;
-webkit-transition: all .4s ease-in-out;
-moz-transition: all .4s ease-in-out;
transition: all 0.4s ease-in-out;
-webkit-transition: all 0.4s ease-in-out;
-moz-transition: all 0.4s ease-in-out;
height: 100vh;
background-color: white;
}
@@ -235,29 +235,29 @@ menuQuickStart {
}
.gridRowSelected {
background-color: #DEF;
background-color: #def;
}
.gridRowSelected:hover {
background-color: #DEF!important;
background-color: #def !important;
cursor: initial;
}
.collectionNodeSelected {
background-color: #DEF;
background-color: #def;
}
.collectionNodeSelected:hover {
background-color: #DEF!important;
background-color: #def !important;
cursor: default !important;
}
.databaseNodeSelected {
background-color: #DEF;
background-color: #def;
}
.databaseNodeSelected:hover {
background-color: #DEF!important;
background-color: #def !important;
cursor: default !important;
}
@@ -266,7 +266,7 @@ menuQuickStart {
border-top: 1px solid #eee;
margin-left: -17px;
width: 100%;
color: 1px solid #53575B;
color: 1px solid #53575b;
}
.partitioning-btn {
@@ -328,7 +328,7 @@ menuQuickStart {
.collid-white {
width: 100%;
border: solid 1px #DDD;
border: solid 1px #ddd;
}
.plusimg-but {
@@ -515,11 +515,11 @@ label {
.datalist-arrow:focus:after,
.datalist-arrow:active:after {
background: #1EBBEE;
background: #1ebbee;
}
input::-webkit-calendar-picker-indicator::after {
content: '\276F';
content: "\276F";
right: 0;
top: -8%;
display: block;
@@ -533,7 +533,7 @@ input::-webkit-calendar-picker-indicator::after {
}
.datalist-arrow:after:hover {
content: '\276F';
content: "\276F";
position: absolute;
right: 1px;
top: 6%;
@@ -545,7 +545,7 @@ input::-webkit-calendar-picker-indicator::after {
color: #fff;
text-align: center;
pointer-events: none;
background-color: #1EBBEE;
background-color: #1ebbee;
}
.Introline3 {
@@ -567,7 +567,7 @@ input::-webkit-calendar-picker-indicator::after {
}
.collectionCollapsed:hover {
background: #EEEEEE;
background: #eeeeee;
}
.collectionCollapsed:active {
@@ -590,12 +590,12 @@ input::-webkit-calendar-picker-indicator::after {
.leftarrowCollapsed {
padding: 2px 4px 4px 5px;
border: solid 1px #FFF;
border: solid 1px #fff;
margin: 6px 4px 0px -11px;
}
.leftarrowCollapsed:hover {
background-color: #EEEEEE;
background-color: #eeeeee;
}
.leftarrowCollapsed:active {
@@ -655,11 +655,7 @@ input::-webkit-calendar-picker-indicator::after {
color: #0058ad;
}
.netApp {
padding-bottom: 80px;
}
.pythonApp {
.sampleApp {
padding-bottom: 45px;
}
@@ -691,7 +687,7 @@ input::-webkit-calendar-picker-indicator::after {
.atags {
color: @AccentMediumHigh;
font-weight: 400;
cursor: pointer
cursor: pointer;
}
.qsmenuicons {
@@ -754,13 +750,13 @@ a:link {
}
.command:hover {
background-color: #E6E6E6;
background-color: #e6e6e6;
cursor: pointer;
padding-bottom: 12px;
}
.command:active {
background-color: #CCCCCC;
background-color: #cccccc;
border: solid 1px @AccentMediumHigh;
}
@@ -966,7 +962,7 @@ a:link {
color: #393939;
}
.tab [type=radio] {
.tab [type="radio"] {
display: none;
}
@@ -979,40 +975,40 @@ a:link {
padding: 15px 0px 20px 0;
}
.tab [type=radio]:checked~label {
.tab [type="radio"]:checked ~ label {
border: 1px solid #0072c6;
background-color: @AccentMediumHigh;
color: white;
z-index: 2;
}
.tab [type=radio]:checked~label:hover {
.tab [type="radio"]:checked ~ label:hover {
border: 1px solid @AccentMediumHigh;
background-color: @AccentMediumHigh;
color: white;
z-index: 2;
}
.tab [type=radio]:checked~label:active {
.tab [type="radio"]:checked ~ label:active {
border: 1px solid #0072c6;
background-color: #0072c6;
color: white;
z-index: 2;
}
.tab [type=radio]:checked~label~.tabcontent {
.tab [type="radio"]:checked ~ label ~ .tabcontent {
z-index: 1;
display: initial;
}
.tab [type=radio]:not(:checked)~label:hover {
.tab [type="radio"]:not(:checked) ~ label:hover {
border: 1px solid #969696;
background-color: #969696;
color: white;
cursor: pointer;
}
.tab [type=radio]:not(:checked)~label~.tabcontent {
.tab [type="radio"]:not(:checked) ~ label ~ .tabcontent {
display: none;
}
@@ -1193,7 +1189,7 @@ a:link {
.nav-tabs > li.active > a,
.nav-tabs > li.active > a:focus,
.nav-tabs > li.active > a:hover {
border-bottom-color: #FFF;
border-bottom-color: #fff;
}
.tabList {
@@ -1262,7 +1258,7 @@ a:link {
.clickableLink {
color: @AccentMediumHigh;
font-family: 'Segoe UI';
font-family: "Segoe UI";
font-size: 12px;
cursor: pointer;
}

214
package-lock.json generated
View File

@@ -10,7 +10,7 @@
"hasInstallScript": true,
"dependencies": {
"@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.2.0-beta.1",
"@azure/cosmos": "4.3.0",
"@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "4.5.0",
"@azure/msal-browser": "2.14.2",
@@ -51,6 +51,8 @@
"@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7",
"@xmldom/xmldom": "0.7.13",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "5.5.0",
"allotment": "1.20.2",
"applicationinsights": "1.8.0",
"bootstrap": "3.4.1",
@@ -288,57 +290,69 @@
"version": "2.6.2",
"license": "0BSD"
},
"node_modules/@azure/core-rest-pipeline": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.18.0.tgz",
"integrity": "sha512-QSoGUp4Eq/gohEFNJaUOwTN7BCc2nHTjjbm75JT0aD7W65PWM1H/tItz0GsABn22uaKyGxiMhWQLt2r+FGU89Q==",
"node_modules/@azure/core-http-compat": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.0.tgz",
"integrity": "sha512-qLQujmUypBBG0gxHd0j6/Jdmul6ttl24c8WGiLXIk7IHXdBlfoBqW27hyz3Xn6xbfdyVSarl1Ttbk0AwnZBYCw==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.8.0",
"@azure/core-tracing": "^1.0.1",
"@azure/core-util": "^1.11.0",
"@azure/core-client": "^1.3.0",
"@azure/core-rest-pipeline": "^1.20.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-lro": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz",
"integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-util": "^1.2.0",
"@azure/logger": "^1.0.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-rest-pipeline/node_modules/agent-base": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
"node_modules/@azure/core-lro/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/@azure/core-paging": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz",
"integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==",
"dependencies": {
"debug": "^4.3.4"
"tslib": "^2.6.2"
},
"engines": {
"node": ">= 14"
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-rest-pipeline/node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"node_modules/@azure/core-paging/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/@azure/core-rest-pipeline": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.20.0.tgz",
"integrity": "sha512-ASoP8uqZBS3H/8N8at/XwFr6vYrRP3syTK0EUjDXQy0Y1/AUS+QeIRThKmTNJO2RggvBBxaXDPM7YoIwDGeA0g==",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.8.0",
"@azure/core-tracing": "^1.0.1",
"@azure/core-util": "^1.11.0",
"@azure/logger": "^1.0.0",
"@typespec/ts-http-runtime": "^0.2.2",
"tslib": "^2.6.2"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@azure/core-rest-pipeline/node_modules/https-proxy-agent": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
"integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
"dependencies": {
"agent-base": "^7.0.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-rest-pipeline/node_modules/tslib": {
@@ -377,15 +391,16 @@
"license": "0BSD"
},
"node_modules/@azure/cosmos": {
"version": "4.2.0-beta.1",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.2.0-beta.1.tgz",
"integrity": "sha512-mREONehm1DxjEKXGaNU6Wmpf9Ckb9IrhKFXhDFVs45pxmoEb3y2s/Ub0owuFmqlphpcS1zgtYQn5exn+lwnJuQ==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.3.0.tgz",
"integrity": "sha512-0Ls3l1uWBBSphx6YRhnM+w7rSvq8qVugBCdO6kSiNuRYXEf6+YWLjbzz4e7L2kkz/6ScFdZIOJYP+XtkiRYOhA==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.7.1",
"@azure/core-rest-pipeline": "^1.15.1",
"@azure/core-tracing": "^1.1.1",
"@azure/core-util": "^1.8.1",
"@azure/keyvault-keys": "^4.8.0",
"fast-json-stable-stringify": "^2.1.0",
"jsbi": "^4.3.0",
"priorityqueuejs": "^2.0.0",
@@ -490,14 +505,66 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/@azure/logger": {
"version": "1.0.4",
"license": "MIT",
"node_modules/@azure/keyvault-common": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz",
"integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.3.0",
"@azure/core-client": "^1.5.0",
"@azure/core-rest-pipeline": "^1.8.0",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.10.0",
"@azure/logger": "^1.1.4",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=14.0.0"
"node": ">=18.0.0"
}
},
"node_modules/@azure/keyvault-common/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/@azure/keyvault-keys": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.9.0.tgz",
"integrity": "sha512-ZBP07+K4Pj3kS4TF4XdkqFcspWwBHry3vJSOFM5k5ZABvf7JfiMonvaFk2nBF6xjlEbMpz5PE1g45iTMme0raQ==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.3.0",
"@azure/core-client": "^1.5.0",
"@azure/core-http-compat": "^2.0.1",
"@azure/core-lro": "^2.2.0",
"@azure/core-paging": "^1.1.1",
"@azure/core-rest-pipeline": "^1.8.1",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.0.0",
"@azure/keyvault-common": "^2.0.0",
"@azure/logger": "^1.0.0",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/keyvault-keys/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/@azure/logger": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.2.0.tgz",
"integrity": "sha512-0hKEzLhpw+ZTAfNJyRrn6s+V0nDWzXk9OjBr2TiGIu0OfMr5s2V4FpKLTAK3Ca5r5OKLbf4hkOGDPyiRjie/jA==",
"dependencies": {
"@typespec/ts-http-runtime": "^0.2.2",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/logger/node_modules/tslib": {
@@ -13072,6 +13139,56 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typespec/ts-http-runtime": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.2.2.tgz",
"integrity": "sha512-Gz/Sm64+Sq/vklJu1tt9t+4R2lvnud8NbTD/ZfpZtMiUX7YeVpCA8j6NSW8ptwcoLL+NmYANwqP8DV0q/bwl2w==",
"dependencies": {
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@typespec/ts-http-runtime/node_modules/agent-base": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
"engines": {
"node": ">= 14"
}
},
"node_modules/@typespec/ts-http-runtime/node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@typespec/ts-http-runtime/node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@typespec/ts-http-runtime/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/@ungap/url-search-params": {
"version": "0.2.2",
"license": "ISC"
@@ -13240,6 +13357,19 @@
"node": ">=10.0.0"
}
},
"node_modules/@xterm/addon-fit": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
"integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
},
"node_modules/@xterm/xterm": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
},
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"license": "BSD-3-Clause"

View File

@@ -5,7 +5,7 @@
"main": "index.js",
"dependencies": {
"@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.2.0-beta.1",
"@azure/cosmos": "4.3.0",
"@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "4.5.0",
"@azure/msal-browser": "2.14.2",
@@ -46,6 +46,8 @@
"@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7",
"@xmldom/xmldom": "0.7.13",
"@xterm/xterm": "5.5.0",
"@xterm/addon-fit": "0.10.0",
"allotment": "1.20.2",
"applicationinsights": "1.8.0",
"bootstrap": "3.4.1",

View File

@@ -1,5 +1,4 @@
import { defineConfig, devices } from "@playwright/test";
/**
* See https://playwright.dev/docs/test-configuration.
*/
@@ -29,24 +28,60 @@ export default defineConfig({
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
use: {
...devices["Desktop Chrome"],
launchOptions: {
args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"],
},
},
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
use: {
...devices["Desktop Firefox"],
launchOptions: {
firefoxUserPrefs: {
"security.fileuri.strict_origin_policy": false,
"network.http.referer.XOriginPolicy": 0,
"network.http.referer.trimmingPolicy": 0,
"privacy.file_unique_origin": false,
"security.csp.enable": false,
"network.cors_preflight.allow_client_cert": true,
"dom.security.https_first": false,
"network.http.cross-origin-embedder-policy": false,
"network.http.cross-origin-opener-policy": false,
"browser.tabs.remote.useCrossOriginPolicy": false,
"browser.tabs.remote.useCORP": false,
},
args: ["--disable-web-security"],
},
},
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
use: {
...devices["Desktop Safari"],
},
},
/* Test against branded browsers. */
{
name: "Google Chrome",
use: { ...devices["Desktop Chrome"], channel: "chrome" }, // or 'chrome-beta'
use: {
...devices["Desktop Chrome"],
channel: "chrome",
launchOptions: {
args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"],
},
},
},
{
name: "Microsoft Edge",
use: { ...devices["Desktop Edge"], channel: "msedge" }, // or 'msedge-dev'
use: {
...devices["Desktop Edge"],
channel: "msedge",
launchOptions: {
args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"],
},
},
},
],

View File

@@ -10,7 +10,7 @@
"hasInstallScript": true,
"dependencies": {
"@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.2.0-beta.1",
"@azure/cosmos": "4.3.0",
"@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "4.5.0",
"@azure/msal-browser": "2.14.2",
@@ -377,8 +377,8 @@
"license": "0BSD"
},
"node_modules/@azure/cosmos": {
"version": "4.2.0-beta.1",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.2.0-beta.1.tgz",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.3.0.tgz",
"integrity": "sha512-mREONehm1DxjEKXGaNU6Wmpf9Ckb9IrhKFXhDFVs45pxmoEb3y2s/Ub0owuFmqlphpcS1zgtYQn5exn+lwnJuQ==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",

View File

@@ -257,6 +257,7 @@ export class Areas {
public static ShareDialog: string = "Share Access Dialog";
public static Notebook: string = "Notebook";
public static Copilot: string = "Copilot";
public static CloudShell: string = "Cloud Shell";
}
export class HttpHeaders {
@@ -530,6 +531,9 @@ export class ariaLabelForLearnMoreLink {
public static readonly AzureSynapseLink = "Learn more about Azure Synapse Link.";
}
export class GlobalSecondaryIndexLabels {
public static readonly NewGlobalSecondaryIndex: string = "New Global Secondary Index";
}
export class FeedbackLabels {
public static readonly provideFeedback: string = "Provide feedback";
}

View File

@@ -125,7 +125,11 @@ export const endpoint = () => {
const location = _global.parent ? _global.parent.location : _global.location;
return configContext.EMULATOR_ENDPOINT || location.origin;
}
return userContext.endpoint || userContext?.databaseAccount?.properties?.documentEndpoint;
return (
userContext.selectedRegionalEndpoint ||
userContext.endpoint ||
userContext?.databaseAccount?.properties?.documentEndpoint
);
};
export async function getTokenFromAuthService(
@@ -203,6 +207,7 @@ export function client(): Cosmos.CosmosClient {
userAgentSuffix: "Azure Portal",
defaultHeaders: _defaultHeaders,
connectionPolicy: {
enableEndpointDiscovery: !userContext.selectedRegionalEndpoint,
retryOptions: {
maxRetryAttemptCount: LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts),
fixedRetryIntervalInMilliseconds: LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval),

View File

@@ -1,5 +1,6 @@
import { TagNames, WorkloadType } from "Common/Constants";
import { Tags } from "Contracts/DataModels";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { userContext } from "../UserContext";
function isVirtualNetworkFilterEnabled() {
@@ -26,3 +27,9 @@ export function getWorkloadType(): WorkloadType {
}
return workloadType;
}
export function isGlobalSecondaryIndexEnabled(): boolean {
return (
!isFabric() && userContext.apiType === "SQL" && userContext.databaseAccount?.properties?.enableMaterializedViews
);
}

View File

@@ -1,5 +1,7 @@
import { QueryOperationOptions } from "@azure/cosmos";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as Constants from "../Common/Constants";
import { QueryResults } from "../Contracts/ViewModels";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
interface QueryResponse {
// [Todo] remove any
@@ -11,17 +13,15 @@ interface QueryResponse {
}
export interface MinimalQueryIterator {
fetchNext: (queryOperationOptions?: QueryOperationOptions) => Promise<QueryResponse>;
fetchNext: () => Promise<QueryResponse>;
}
// Pick<QueryIterator<any>, "fetchNext">;
export function nextPage(
documentsIterator: MinimalQueryIterator,
firstItemIndex: number,
queryOperationOptions?: QueryOperationOptions,
): Promise<QueryResults> {
return documentsIterator.fetchNext(queryOperationOptions).then((response) => {
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> {
TelemetryProcessor.traceStart(Action.ExecuteQuery);
return documentsIterator.fetchNext().then((response) => {
TelemetryProcessor.traceSuccess(Action.ExecuteQuery, { dataExplorerArea: Constants.Areas.Tab });
const documents = response.resources;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any

View File

@@ -1,5 +1,4 @@
import { monaco } from "Explorer/LazyMonaco";
import { getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
export enum QueryErrorSeverity {
Error = "Error",
@@ -103,20 +102,9 @@ export interface ErrorEnrichment {
learnMoreUrl?: string;
}
const REPLACEMENT_MESSAGES: Record<string, (original: string) => string> = {
OPERATION_RU_LIMIT_EXCEEDED: (original) => {
if (ruThresholdEnabled()) {
const threshold = getRUThreshold();
return `Query exceeded the Request Unit (RU) limit of ${threshold} RUs. You can change this limit in Data Explorer settings.`;
}
return original;
},
};
const REPLACEMENT_MESSAGES: Record<string, (original: string) => string> = {};
const HELP_LINKS: Record<string, string> = {
OPERATION_RU_LIMIT_EXCEEDED:
"https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer#configure-request-unit-threshold",
};
const HELP_LINKS: Record<string, string> = {};
export default class QueryError {
message: string;

View File

@@ -4,13 +4,18 @@ import * as React from "react";
export interface TooltipProps {
children: string;
className?: string;
ariaLabelForTooltip?: string;
}
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children, className }: TooltipProps) => {
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({
children,
className,
ariaLabelForTooltip = children,
}: TooltipProps) => {
return (
<span className={className}>
<TooltipHost content={children}>
<Icon iconName="Info" ariaLabel={children} className="panelInfoIcon" tabIndex={0} />
<Icon iconName="Info" aria-label={ariaLabelForTooltip} className="panelInfoIcon" tabIndex={0} />
</TooltipHost>
</span>
);

View File

@@ -3,6 +3,7 @@
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
{
"disableNonStreamingOrderByQuery": true,
"enableQueryControl": false,
"enableScanInQuery": true,
"forceQueryPlan": true,
"maxDegreeOfParallelism": 0,
@@ -14,6 +15,7 @@ exports[`getCommonQueryOptions builds the correct default options objects 1`] =
exports[`getCommonQueryOptions reads from localStorage 1`] = `
{
"disableNonStreamingOrderByQuery": true,
"enableQueryControl": false,
"enableScanInQuery": true,
"forceQueryPlan": true,
"maxDegreeOfParallelism": 17,

View File

@@ -0,0 +1,74 @@
import { constructRpOptions } from "Common/dataAccess/createCollection";
import { handleError } from "Common/ErrorHandlingUtils";
import { Collection, CreateMaterializedViewsParams as CreateGlobalSecondaryIndexParams } from "Contracts/DataModels";
import { userContext } from "UserContext";
import { createUpdateSqlContainer } from "Utils/arm/generatedClients/cosmos/sqlResources";
import {
CreateUpdateOptions,
SqlContainerResource,
SqlDatabaseCreateUpdateParameters,
} from "Utils/arm/generatedClients/cosmos/types";
import { logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
export const createGlobalSecondaryIndex = async (params: CreateGlobalSecondaryIndexParams): Promise<Collection> => {
const clearMessage = logConsoleProgress(
`Creating a new global secondary index ${params.materializedViewId} for database ${params.databaseId}`,
);
const options: CreateUpdateOptions = constructRpOptions(params);
const resource: SqlContainerResource = {
id: params.materializedViewId,
};
if (params.materializedViewDefinition) {
resource.materializedViewDefinition = params.materializedViewDefinition;
}
if (params.analyticalStorageTtl) {
resource.analyticalStorageTtl = params.analyticalStorageTtl;
}
if (params.indexingPolicy) {
resource.indexingPolicy = params.indexingPolicy;
}
if (params.partitionKey) {
resource.partitionKey = params.partitionKey;
}
if (params.uniqueKeyPolicy) {
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
}
if (params.vectorEmbeddingPolicy) {
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
}
if (params.fullTextPolicy) {
resource.fullTextPolicy = params.fullTextPolicy;
}
const rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: {
resource,
options,
},
};
try {
const createResponse = await createUpdateSqlContainer(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.materializedViewId,
rpPayload,
);
logConsoleInfo(`Successfully created global secondary index ${params.materializedViewId}`);
return createResponse && (createResponse.properties.resource as Collection);
} catch (error) {
handleError(
error,
"CreateGlobalSecondaryIndex",
`Error while creating global secondary index ${params.materializedViewId}`,
);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -42,6 +42,7 @@ export interface IBulkDeleteResult {
export const deleteDocuments = async (
collection: CollectionBase,
documentIds: DocumentId[],
abortSignal: AbortSignal,
): Promise<IBulkDeleteResult[]> => {
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
try {
@@ -65,7 +66,11 @@ export const deleteDocuments = async (
operationType: BulkOperationType.Delete,
}));
const promise = v2Container.items.bulk(operations).then((bulkResults) => {
const promise = v2Container.items
.bulk(operations, undefined, {
abortSignal,
})
.then((bulkResults) => {
return bulkResults.map((bulkResult, index) => {
const documentId = documentIdsChunk[index];
return { ...bulkResult, documentId };

View File

@@ -26,6 +26,7 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
options.maxItemCount ||
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
Queries.itemsPerPage;
options.enableQueryControl = LocalStorageUtility.getEntryBoolean(StorageKey.QueryControlEnabled);
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled();
return options;

View File

@@ -1,4 +1,3 @@
import { QueryOperationOptions } from "@azure/cosmos";
import { QueryResults } from "../../Contracts/ViewModels";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { getEntityName } from "../DocumentUtility";
@@ -9,13 +8,12 @@ export const queryDocumentsPage = async (
resourceName: string,
documentsIterator: MinimalQueryIterator,
firstItemIndex: number,
queryOperationOptions?: QueryOperationOptions,
): Promise<QueryResults> => {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
try {
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex, queryOperationOptions);
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex);
const itemCount = (result.documents && result.documents.length) || 0;
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
return result;

View File

@@ -1,3 +1,4 @@
import { isFabric } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType";
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
@@ -13,6 +14,11 @@ import { readOfferWithSDK } from "./readOfferWithSDK";
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
if (isFabric()) {
// Not exposing offers in Fabric
return undefined;
}
try {
if (
userContext.authType === AuthType.AAD &&

View File

@@ -126,5 +126,12 @@ async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Co
throw new Error(`Unsupported default experience type: ${apiType}`);
}
return rpResponse?.value?.map((collection) => collection.properties?.resource as DataModels.Collection);
// TO DO: Remove when we get RP API Spec with materializedViews
/* eslint-disable @typescript-eslint/no-explicit-any */
return rpResponse?.value?.map((collection: any) => {
const collectionDataModel: DataModels.Collection = collection.properties?.resource as DataModels.Collection;
collectionDataModel.materializedViews = collection.properties?.resource?.materializedViews;
collectionDataModel.materializedViewDefinition = collection.properties?.resource?.materializedViewDefinition;
return collectionDataModel;
});
}

View File

@@ -18,10 +18,13 @@ export type DataExploreMessageV3 =
| {
type: FabricMessageTypes.GetAllResourceTokens;
id: string;
}
| {
type: FabricMessageTypes.OpenSettings;
settingsId: string;
};
export type GetCosmosTokenMessageOptions = {
export interface GetCosmosTokenMessageOptions {
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges";
resourceId: string;
};
}

View File

@@ -32,6 +32,7 @@ export interface DatabaseAccountExtendedProperties {
writeLocations?: DatabaseAccountResponseLocation[];
enableFreeTier?: boolean;
enableAnalyticalStorage?: boolean;
enableMaterializedViews?: boolean;
isVirtualNetworkFilterEnabled?: boolean;
ipRules?: IpRule[];
privateEndpointConnections?: unknown[];
@@ -164,6 +165,8 @@ export interface Collection extends Resource {
schema?: ISchema;
requestSchema?: () => void;
computedProperties?: ComputedProperties;
materializedViews?: MaterializedView[];
materializedViewDefinition?: MaterializedViewDefinition;
}
export interface CollectionsWithPagination {
@@ -207,7 +210,7 @@ export interface IndexingPolicy {
export interface VectorIndex {
path: string;
type: "flat" | "diskANN" | "quantizedFlat";
diskANNShardKey?: string;
vectorIndexShardKey?: string[];
indexingSearchListSize?: number;
quantizationByteSize?: number;
}
@@ -223,6 +226,17 @@ export interface ComputedProperty {
export type ComputedProperties = ComputedProperty[];
export interface MaterializedView {
id: string;
_rid: string;
}
export interface MaterializedViewDefinition {
definition: string;
sourceCollectionId: string;
sourceCollectionRid?: string;
}
export interface PartitionKey {
paths: string[];
kind: "Hash" | "Range" | "MultiHash";
@@ -345,9 +359,7 @@ export interface CreateDatabaseParams {
offerThroughput?: number;
}
export interface CreateCollectionParams {
createNewDatabase: boolean;
collectionId: string;
export interface CreateCollectionParamsBase {
databaseId: string;
databaseLevelThroughput: boolean;
offerThroughput?: number;
@@ -361,6 +373,16 @@ export interface CreateCollectionParams {
fullTextPolicy?: FullTextPolicy;
}
export interface CreateCollectionParams extends CreateCollectionParamsBase {
createNewDatabase: boolean;
collectionId: string;
}
export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase {
materializedViewId: string;
materializedViewDefinition: MaterializedViewDefinition;
}
export interface VectorEmbeddingPolicy {
vectorEmbeddings: VectorEmbedding[];
}

View File

@@ -6,6 +6,7 @@ export enum FabricMessageTypes {
GetAllResourceTokens = "GetAllResourceTokens",
GetAccessToken = "GetAccessToken",
Ready = "Ready",
OpenSettings = "OpenSettings",
}
export interface AuthorizationToken {

View File

@@ -81,6 +81,13 @@ export type FabricMessageV3 =
error: string | undefined;
data: { accessToken: string };
};
}
| {
type: "refreshResourceTree";
message: {
id: string;
error: string | undefined;
};
};
export enum CosmosDbArtifactType {

View File

@@ -1,4 +1,6 @@
import {
ItemDefinition,
JSONObject,
QueryMetrics,
Resource,
StoredProcedureDefinition,
@@ -29,8 +31,11 @@ export interface UploadDetailsRecord {
numFailed: number;
numThrottled: number;
errors: string[];
resources?: ItemDefinition[];
}
export type BulkInsertResult = Omit<UploadDetailsRecord, "fileName">;
export interface QueryResultsMetadata {
hasMoreResults: boolean;
firstItemIndex: number;
@@ -45,6 +50,7 @@ export interface QueryResults extends QueryResultsMetadata {
roundTrips?: number;
headers?: any;
queryMetrics?: QueryMetrics;
ruThresholdExceeded?: boolean;
}
export interface Button {
@@ -143,6 +149,8 @@ export interface Collection extends CollectionBase {
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
documentIds: ko.ObservableArray<DocumentId>;
computedProperties: ko.Observable<DataModels.ComputedProperties>;
materializedViews: ko.Observable<DataModels.MaterializedView[]>;
materializedViewDefinition: ko.Observable<DataModels.MaterializedViewDefinition>;
cassandraKeys: CassandraTableKeys;
cassandraSchema: CassandraTableKey[];
@@ -204,6 +212,12 @@ export interface Collection extends CollectionBase {
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
bulkInsertDocuments(documents: JSONObject[]): Promise<{
numSucceeded: number;
numFailed: number;
numThrottled: number;
errors: string[];
}>;
}
/**

View File

@@ -1,5 +1,11 @@
import { GlobalSecondaryIndexLabels } from "Common/Constants";
import { isGlobalSecondaryIndexEnabled } from "Common/DatabaseAccountUtility";
import { configContext, Platform } from "ConfigContext";
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import {
AddGlobalSecondaryIndexPanel,
AddGlobalSecondaryIndexPanelProps,
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
import { useDatabases } from "Explorer/useDatabases";
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
import { Action } from "Shared/Telemetry/TelemetryConstants";
@@ -97,17 +103,23 @@ export const createCollectionContextMenuButton = (
iconSrc: HostedTerminalIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
if (useNotebook.getState().isShellEnabled) {
if (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
},
label: useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell",
label:
useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell
? "Open Mongo Shell"
: "New Shell",
});
}
if (useNotebook.getState().isShellEnabled && userContext.apiType === "Cassandra") {
if (
(useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) &&
userContext.apiType === "Cassandra"
) {
items.push({
iconSrc: HostedTerminalIcon,
onClick: () => {
@@ -164,6 +176,24 @@ export const createCollectionContextMenuButton = (
});
}
if (isGlobalSecondaryIndexEnabled() && !selectedCollection.materializedViewDefinition()) {
items.push({
label: GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
onClick: () => {
const addMaterializedViewPanelProps: AddGlobalSecondaryIndexPanelProps = {
explorer: container,
sourceContainer: selectedCollection,
};
useSidePanel
.getState()
.openSidePanel(
GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
<AddGlobalSecondaryIndexPanel {...addMaterializedViewPanelProps} />,
);
},
});
}
return items;
};

View File

@@ -214,8 +214,10 @@ export const Dialog: FC = () => {
{contentHtml}
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
<DialogFooter>
<PrimaryButton {...primaryButtonProps} />
{secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />}
<PrimaryButton {...primaryButtonProps} data-test={`DialogButton:${primaryButtonText}`} />
{secondaryButtonProps && (
<DefaultButton {...secondaryButtonProps} data-test={`DialogButton:${secondaryButtonText}`} />
)}
</DialogFooter>
</FluentDialog>
) : (

View File

@@ -193,6 +193,7 @@ export const InputDataList: FC<InputDataListProps> = ({
<>
<Input
id="filterInput"
data-test={"DocumentsTab/FilterInput"}
ref={inputRef}
type="text"
size="small"

View File

@@ -1,7 +1,6 @@
import { AuthType } from "AuthType";
import { shallow } from "enzyme";
import ko from "knockout";
import { Features } from "Platform/Hosted/extractFeatures";
import React from "react";
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
@@ -253,7 +252,7 @@ describe("SettingsComponent", () => {
it("should save throughput bucket changes when Save button is clicked", async () => {
updateUserContext({
apiType: "SQL",
features: { enableThroughputBuckets: true } as Features,
throughputBucketsEnabled: true,
authType: AuthType.AAD,
});

View File

@@ -12,7 +12,8 @@ import {
ThroughputBucketsComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
import { useDatabases } from "Explorer/useDatabases";
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
import * as React from "react";
import DiscardIcon from "../../../../images/discard.svg";
@@ -44,6 +45,10 @@ import {
ConflictResolutionComponent,
ConflictResolutionComponentProps,
} from "./SettingsSubComponents/ConflictResolutionComponent";
import {
GlobalSecondaryIndexComponent,
GlobalSecondaryIndexComponentProps,
} from "./SettingsSubComponents/GlobalSecondaryIndexComponent";
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
import {
MongoIndexingPolicyComponent,
@@ -162,6 +167,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private shouldShowComputedPropertiesEditor: boolean;
private shouldShowIndexingPolicyEditor: boolean;
private shouldShowPartitionKeyEditor: boolean;
private isGlobalSecondaryIndex: boolean;
private isVectorSearchEnabled: boolean;
private isFullTextSearchEnabled: boolean;
private totalThroughputUsed: number;
@@ -179,14 +185,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
this.isGlobalSecondaryIndex =
!!this.collection?.materializedViewDefinition() || !!this.collection?.materializedViews();
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
this.isFullTextSearchEnabled = userContext.apiType === "SQL";
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
this.throughputBucketsEnabled =
userContext.apiType === "SQL" &&
userContext.features.enableThroughputBuckets &&
userContext.authType === AuthType.AAD;
this.throughputBucketsEnabled = userContext.throughputBucketsEnabled;
// Mongo container with system partition key still treat as "Fixed"
this.isFixedContainer =
@@ -1066,11 +1071,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
databaseId: this.collection.databaseId,
collectionId: this.collection.id(),
currentOffer: this.collection.offer(),
autopilotThroughput: this.collection.offer().autoscaleMaxThroughput
? this.collection.offer().autoscaleMaxThroughput
autopilotThroughput: this.collection.offer?.()?.autoscaleMaxThroughput
? this.collection.offer?.()?.autoscaleMaxThroughput
: undefined,
manualThroughput: this.collection.offer().manualThroughput
? this.collection.offer().manualThroughput
manualThroughput: this.collection.offer?.()?.manualThroughput
? this.collection.offer?.()?.manualThroughput
: undefined,
throughputBuckets: this.state.throughputBuckets,
});
@@ -1086,6 +1091,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
currentOffer: this.collection.offer(),
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput,
throughputBuckets: this.throughputBucketsEnabled ? this.state.throughputBuckets : undefined,
};
if (this.hasProvisioningTypeChanged()) {
if (this.state.isAutoPilotSelected) {
@@ -1141,6 +1147,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
collection: this.collection,
database: this.database,
isFixedContainer: this.isFixedContainer,
isGlobalSecondaryIndex: this.isGlobalSecondaryIndex,
onThroughputChange: this.onThroughputChange,
throughput: this.state.throughput,
throughputBaseline: this.state.throughputBaseline,
@@ -1206,6 +1213,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
isFullTextSearchEnabled: this.isFullTextSearchEnabled,
shouldDiscardContainerPolicies: this.state.shouldDiscardContainerPolicies,
resetShouldDiscardContainerPolicyChange: this.resetShouldDiscardContainerPolicies,
isGlobalSecondaryIndex: this.isGlobalSecondaryIndex,
};
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
@@ -1270,6 +1278,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
collection: this.collection,
explorer: this.props.settingsTab.getContainer(),
isReadOnly: isFabricNative(),
};
const globalSecondaryIndexComponentProps: GlobalSecondaryIndexComponentProps = {
collection: this.collection,
explorer: this.props.settingsTab.getContainer(),
};
const tabs: SettingsV2TabInfo[] = [];
@@ -1328,13 +1342,20 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
});
}
if (this.throughputBucketsEnabled) {
if (this.throughputBucketsEnabled && !hasDatabaseSharedThroughput(this.collection) && this.offer) {
tabs.push({
tab: SettingsV2TabTypes.ThroughputBucketsTab,
content: <ThroughputBucketsComponent {...throughputBucketsComponentProps} />,
});
}
if (this.isGlobalSecondaryIndex) {
tabs.push({
tab: SettingsV2TabTypes.GlobalSecondaryIndexTab,
content: <GlobalSecondaryIndexComponent {...globalSecondaryIndexComponentProps} />,
});
}
const pivotProps: IPivotProps = {
onLinkClick: this.onPivotChange,
selectedKey: SettingsV2TabTypes[this.state.selectedTab],

View File

@@ -22,6 +22,7 @@ export interface ContainerPolicyComponentProps {
isFullTextSearchEnabled: boolean;
shouldDiscardContainerPolicies: boolean;
resetShouldDiscardContainerPolicyChange: () => void;
isGlobalSecondaryIndex?: boolean;
}
export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> = ({

View File

@@ -0,0 +1,46 @@
import { shallow } from "enzyme";
import React from "react";
import { collection, container } from "../TestUtils";
import { GlobalSecondaryIndexComponent } from "./GlobalSecondaryIndexComponent";
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
describe("GlobalSecondaryIndexComponent", () => {
let testCollection: typeof collection;
let testExplorer: typeof container;
beforeEach(() => {
testCollection = { ...collection };
});
it("renders only the source component when materializedViewDefinition is missing", () => {
testCollection.materializedViews([
{ id: "view1", _rid: "rid1" },
{ id: "view2", _rid: "rid2" },
]);
testCollection.materializedViewDefinition(null);
const wrapper = shallow(<GlobalSecondaryIndexComponent collection={testCollection} explorer={testExplorer} />);
expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(true);
expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(false);
});
it("renders only the target component when materializedViews is missing", () => {
testCollection.materializedViews(null);
testCollection.materializedViewDefinition({
definition: "SELECT * FROM c WHERE c.id = 1",
sourceCollectionId: "source1",
sourceCollectionRid: "rid123",
});
const wrapper = shallow(<GlobalSecondaryIndexComponent collection={testCollection} explorer={testExplorer} />);
expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(false);
expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(true);
});
it("renders neither component when both are missing", () => {
testCollection.materializedViews(null);
testCollection.materializedViewDefinition(null);
const wrapper = shallow(<GlobalSecondaryIndexComponent collection={testCollection} explorer={testExplorer} />);
expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(false);
expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(false);
});
});

View File

@@ -0,0 +1,41 @@
import { FontIcon, Link, Stack, Text } from "@fluentui/react";
import Explorer from "Explorer/Explorer";
import React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
export interface GlobalSecondaryIndexComponentProps {
collection: ViewModels.Collection;
explorer: Explorer;
}
export const GlobalSecondaryIndexComponent: React.FC<GlobalSecondaryIndexComponentProps> = ({
collection,
explorer,
}) => {
const isTargetContainer = !!collection?.materializedViewDefinition();
const isSourceContainer = !!collection?.materializedViews();
return (
<Stack tokens={{ childrenGap: 8 }} styles={{ root: { maxWidth: 600 } }}>
<Stack horizontal verticalAlign="center" wrap tokens={{ childrenGap: 8 }}>
{isSourceContainer && (
<Text styles={{ root: { fontWeight: 600 } }}>This container has the following indexes defined for it.</Text>
)}
<Text>
<Link
target="_blank"
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
>
Learn more
<FontIcon iconName="NavigateExternalInline" style={{ marginLeft: "4px" }} />
</Link>{" "}
about how to define global secondary indexes and how to use them.
</Text>
</Stack>
{isSourceContainer && <GlobalSecondaryIndexSourceComponent collection={collection} explorer={explorer} />}
{isTargetContainer && <GlobalSecondaryIndexTargetComponent collection={collection} />}
</Stack>
);
};

View File

@@ -0,0 +1,42 @@
import { PrimaryButton } from "@fluentui/react";
import { shallow } from "enzyme";
import React from "react";
import { collection, container } from "../TestUtils";
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
describe("GlobalSecondaryIndexSourceComponent", () => {
let testCollection: typeof collection;
let testExplorer: typeof container;
beforeEach(() => {
testCollection = { ...collection };
});
it("renders without crashing", () => {
const wrapper = shallow(
<GlobalSecondaryIndexSourceComponent collection={testCollection} explorer={testExplorer} />,
);
expect(wrapper.exists()).toBe(true);
});
it("renders the PrimaryButton", () => {
const wrapper = shallow(
<GlobalSecondaryIndexSourceComponent collection={testCollection} explorer={testExplorer} />,
);
expect(wrapper.find(PrimaryButton).exists()).toBe(true);
});
it("updates when new global secondary indexes are provided", () => {
const wrapper = shallow(
<GlobalSecondaryIndexSourceComponent collection={testCollection} explorer={testExplorer} />,
);
// Simulating an update by modifying the observable directly
testCollection.materializedViews([{ id: "view3", _rid: "rid3" }]);
wrapper.setProps({ collection: testCollection });
wrapper.update();
expect(wrapper.find(PrimaryButton).exists()).toBe(true);
});
});

View File

@@ -0,0 +1,114 @@
import { PrimaryButton } from "@fluentui/react";
import { GlobalSecondaryIndexLabels } from "Common/Constants";
import { MaterializedView } from "Contracts/DataModels";
import Explorer from "Explorer/Explorer";
import { loadMonaco } from "Explorer/LazyMonaco";
import { AddGlobalSecondaryIndexPanel } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
import { useDatabases } from "Explorer/useDatabases";
import { useSidePanel } from "hooks/useSidePanel";
import * as monaco from "monaco-editor";
import React, { useEffect, useRef } from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
export interface GlobalSecondaryIndexSourceComponentProps {
collection: ViewModels.Collection;
explorer: Explorer;
}
export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexSourceComponentProps> = ({
collection,
explorer,
}) => {
const editorContainerRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(null);
const globalSecondaryIndexes: MaterializedView[] = collection?.materializedViews() ?? [];
// Helper function to fetch the definition and partition key of targetContainer by traversing through all collections and matching id from MaterializedView[] with collection id.
const getViewDetails = (viewId: string): { definition: string; partitionKey: string[] } => {
let definition = "";
let partitionKey: string[] = [];
useDatabases.getState().databases.find((database) => {
const collection = database.collections().find((collection) => collection.id() === viewId);
if (collection) {
const globalSecondaryIndexDefinition = collection.materializedViewDefinition();
globalSecondaryIndexDefinition && (definition = globalSecondaryIndexDefinition.definition);
collection.partitionKey?.paths && (partitionKey = collection.partitionKey.paths);
}
});
return { definition, partitionKey };
};
//JSON value for the editor using the fetched id and definitions.
const jsonValue = JSON.stringify(
globalSecondaryIndexes.map((view) => {
const { definition, partitionKey } = getViewDetails(view.id);
return {
name: view.id,
partitionKey: partitionKey.join(", "),
definition,
};
}),
null,
2,
);
// Initialize Monaco editor with the computed JSON value.
useEffect(() => {
let disposed = false;
const initMonaco = async () => {
const monacoInstance = await loadMonaco();
if (disposed || !editorContainerRef.current) {
return;
}
editorRef.current = monacoInstance.editor.create(editorContainerRef.current, {
value: jsonValue,
language: "json",
ariaLabel: "Global Secondary Index JSON",
readOnly: true,
});
};
initMonaco();
return () => {
disposed = true;
editorRef.current?.dispose();
};
}, [jsonValue]);
// Update the editor when the jsonValue changes.
useEffect(() => {
if (editorRef.current) {
editorRef.current.setValue(jsonValue);
}
}, [jsonValue]);
return (
<div>
<div
ref={editorContainerRef}
style={{
height: 250,
border: "1px solid #ccc",
borderRadius: 4,
overflow: "hidden",
}}
/>
<PrimaryButton
text="Add index"
styles={{ root: { width: "fit-content", marginTop: 12 } }}
onClick={() =>
useSidePanel
.getState()
.openSidePanel(
GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
<AddGlobalSecondaryIndexPanel explorer={explorer} sourceContainer={collection} />,
)
}
/>
</div>
);
};

View File

@@ -0,0 +1,32 @@
import { Text } from "@fluentui/react";
import { Collection } from "Contracts/ViewModels";
import { shallow } from "enzyme";
import React from "react";
import { collection } from "../TestUtils";
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
describe("GlobalSecondaryIndexTargetComponent", () => {
let testCollection: Collection;
beforeEach(() => {
testCollection = {
...collection,
materializedViewDefinition: collection.materializedViewDefinition,
};
});
it("renders without crashing", () => {
const wrapper = shallow(<GlobalSecondaryIndexTargetComponent collection={testCollection} />);
expect(wrapper.exists()).toBe(true);
});
it("displays the source container ID", () => {
const wrapper = shallow(<GlobalSecondaryIndexTargetComponent collection={testCollection} />);
expect(wrapper.find(Text).at(2).dive().text()).toBe("source1");
});
it("displays the global secondary index definition", () => {
const wrapper = shallow(<GlobalSecondaryIndexTargetComponent collection={testCollection} />);
expect(wrapper.find(Text).at(4).dive().text()).toBe("SELECT * FROM c WHERE c.id = 1");
});
});

View File

@@ -0,0 +1,45 @@
import { Stack, Text } from "@fluentui/react";
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
export interface GlobalSecondaryIndexTargetComponentProps {
collection: ViewModels.Collection;
}
export const GlobalSecondaryIndexTargetComponent: React.FC<GlobalSecondaryIndexTargetComponentProps> = ({
collection,
}) => {
const globalSecondaryIndexDefinition = collection?.materializedViewDefinition();
const textHeadingStyle = {
root: { fontWeight: "600", fontSize: 16 },
};
const valueBoxStyle = {
root: {
backgroundColor: "#f3f3f3",
padding: "5px 10px",
borderRadius: "4px",
},
};
return (
<Stack tokens={{ childrenGap: 15 }} styles={{ root: { maxWidth: 600 } }}>
<Text styles={textHeadingStyle}>Global Secondary Index Settings</Text>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={{ root: { fontWeight: "600" } }}>Source container</Text>
<Stack styles={valueBoxStyle}>
<Text>{globalSecondaryIndexDefinition?.sourceCollectionId}</Text>
</Stack>
</Stack>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={{ root: { fontWeight: "600" } }}>Global secondary index definition</Text>
<Stack styles={valueBoxStyle}>
<Text>{globalSecondaryIndexDefinition?.definition}</Text>
</Stack>
</Stack>
</Stack>
);
};

View File

@@ -0,0 +1,41 @@
import { shallow } from "enzyme";
import {
PartitionKeyComponent,
PartitionKeyComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent";
import Explorer from "Explorer/Explorer";
import React from "react";
describe("PartitionKeyComponent", () => {
// Create a test setup function to get fresh instances for each test
const setupTest = () => {
// Create an instance of the mocked Explorer
const explorer = new Explorer();
// Create minimal mock objects for database and collection
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockDatabase = {} as any as import("../../../../Contracts/ViewModels").Database;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockCollection = {} as any as import("../../../../Contracts/ViewModels").Collection;
// Create props with the mocked Explorer instance
const props: PartitionKeyComponentProps = {
database: mockDatabase,
collection: mockCollection,
explorer,
};
return { explorer, props };
};
it("renders default component and matches snapshot", () => {
const { props } = setupTest();
const wrapper = shallow(<PartitionKeyComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders read-only component and matches snapshot", () => {
const { props } = setupTest();
const wrapper = shallow(<PartitionKeyComponent {...props} isReadOnly={true} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -29,16 +29,26 @@ export interface PartitionKeyComponentProps {
database: ViewModels.Database;
collection: ViewModels.Collection;
explorer: Explorer;
isReadOnly?: boolean; // true: cannot change partition key
}
export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ database, collection, explorer }) => {
export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
database,
collection,
explorer,
isReadOnly,
}) => {
const { dataTransferJobs } = useDataTransferJobs();
const [portalDataTransferJob, setPortalDataTransferJob] = React.useState<DataTransferJobGetResults>(null);
React.useEffect(() => {
if (isReadOnly) {
return;
}
const loadDataTransferJobs = refreshDataTransferOperations;
loadDataTransferJobs();
}, []);
}, [isReadOnly]);
React.useEffect(() => {
const currentJob = findPortalDataTransferJob();
@@ -151,7 +161,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ da
return (
<Stack tokens={{ childrenGap: 20 }} styles={{ root: { maxWidth: 600 } }}>
<Stack tokens={{ childrenGap: 10 }}>
<Text styles={textHeadingStyle}>Change {partitionKeyName.toLowerCase()}</Text>
{!isReadOnly && <Text styles={textHeadingStyle}>Change {partitionKeyName.toLowerCase()}</Text>}
<Stack horizontal tokens={{ childrenGap: 20 }}>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
@@ -163,9 +173,12 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ da
</Stack>
</Stack>
</Stack>
{!isReadOnly && (
<>
<MessageBar messageBarType={MessageBarType.warning}>
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to the
source container for the entire duration of the partition key change process.
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to
the source container for the entire duration of the partition key change process.
<Link
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
target="_blank"
@@ -175,8 +188,8 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ da
</Link>
</MessageBar>
<Text>
To change the partition key, a new destination container must be created or an existing destination container
selected. Data will then be copied to the destination container.
To change the partition key, a new destination container must be created or an existing destination
container selected. Data will then be copied to the destination container.
</Text>
{configContext.platform !== Platform.Emulator && (
<PrimaryButton
@@ -214,6 +227,8 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ da
</Stack>
</Stack>
)}
</>
)}
</Stack>
);
};

View File

@@ -9,6 +9,7 @@ describe("ScaleComponent", () => {
collection: collection,
database: undefined,
isFixedContainer: false,
isGlobalSecondaryIndex: false,
onThroughputChange: () => {
return;
},

View File

@@ -22,6 +22,7 @@ export interface ScaleComponentProps {
collection: ViewModels.Collection;
database: ViewModels.Database;
isFixedContainer: boolean;
isGlobalSecondaryIndex: boolean;
onThroughputChange: (newThroughput: number) => void;
throughput: number;
throughputBaseline: number;
@@ -143,6 +144,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
throughputError={this.props.throughputError}
instantMaximumThroughput={this.offer?.instantMaximumThroughput}
softAllowedMaximumThroughput={this.offer?.softAllowedMaximumThroughput}
isGlobalSecondaryIndex={this.props.isGlobalSecondaryIndex}
/>
);

View File

@@ -143,4 +143,39 @@ describe("SubSettingsComponent", () => {
expect(subSettingsComponentInstance.getTtlValue(TtlType.On)).toEqual(TtlOn);
expect(subSettingsComponentInstance.getTtlValue(TtlType.Off)).toEqual(TtlOff);
});
it("uniqueKey is visible", () => {
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableSQL" }],
},
} as DatabaseAccount,
});
const subSettingsComponent = new SubSettingsComponent(baseProps);
expect(subSettingsComponent.getUniqueKeyVisible()).toEqual(true);
});
it("uniqueKey not visible due to no keys", () => {
const props = {
...baseProps,
...(baseProps.collection.rawDataModel.uniqueKeyPolicy.uniqueKeys = []),
};
const subSettingsComponent = new SubSettingsComponent(props);
expect(subSettingsComponent.getUniqueKeyVisible()).toEqual(false);
});
it("uniqueKey not visible for API", () => {
const newContainer = new Explorer();
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableMongo" }],
},
} as DatabaseAccount,
});
const props = { ...baseProps, container: newContainer };
const subSettingsComponent = new SubSettingsComponent(props);
expect(subSettingsComponent.getUniqueKeyVisible()).toEqual(false);
});
});

View File

@@ -63,12 +63,16 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
private geospatialVisible: boolean;
private partitionKeyValue: string;
private partitionKeyName: string;
private uniqueKeyName: string;
private uniqueKeyValue: string;
constructor(props: SubSettingsComponentProps) {
super(props);
this.geospatialVisible = userContext.apiType === "SQL";
this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
this.partitionKeyValue = this.getPartitionKeyValue();
this.uniqueKeyName = "Unique keys";
this.uniqueKeyValue = this.getUniqueKeyValue();
}
componentDidMount(): void {
@@ -351,6 +355,28 @@ 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 getUniqueKeyVisible = (): boolean => {
return this.props.collection.rawDataModel.uniqueKeyPolicy?.uniqueKeys.length > 0 && userContext.apiType === "SQL";
};
private getUniqueKeyValue = (): string => {
const paths = this.props.collection.rawDataModel.uniqueKeyPolicy?.uniqueKeys?.[0]?.paths;
return paths?.join(", ") || "";
};
private getUniqueKeyComponent = (): JSX.Element => (
<Stack {...titleAndInputStackProps}>
{this.getUniqueKeyVisible() && (
<TextField
label={this.uniqueKeyName}
disabled
styles={getTextFieldStyles(undefined, undefined)}
defaultValue={this.uniqueKeyValue}
/>
)}
</Stack>
);
public render(): JSX.Element {
return (
<Stack {...subComponentStackProps}>
@@ -363,6 +389,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
{this.props.changeFeedPolicyVisible && this.getChangeFeedComponent()}
{this.getPartitionKeyComponent()}
{this.getUniqueKeyComponent()}
</Stack>
);
}

View File

@@ -26,7 +26,7 @@ describe("ThroughputBucketsComponent", () => {
it("renders the correct number of buckets", () => {
render(<ThroughputBucketsComponent {...defaultProps} />);
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
expect(screen.getAllByText(/Bucket \d+/)).toHaveLength(5);
});
it("renders buckets in the correct order even if input is unordered", () => {
@@ -36,8 +36,14 @@ describe("ThroughputBucketsComponent", () => {
];
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={unorderedBuckets} />);
const bucketLabels = screen.getAllByText(/Group \d+/).map((el) => el.textContent);
expect(bucketLabels).toEqual(["Group 1 (Data Explorer Query Bucket)", "Group 2", "Group 3", "Group 4", "Group 5"]);
const bucketLabels = screen.getAllByText(/Bucket \d+/).map((el) => el.textContent);
expect(bucketLabels).toEqual([
"Bucket 1 (Data Explorer Query Bucket)",
"Bucket 2",
"Bucket 3",
"Bucket 4",
"Bucket 5",
]);
});
it("renders all provided buckets even if they exceed the max default bucket count", () => {
@@ -53,7 +59,7 @@ describe("ThroughputBucketsComponent", () => {
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={oversizedBuckets} />);
expect(screen.getAllByText(/Group \d+/)).toHaveLength(7);
expect(screen.getAllByText(/Bucket \d+/)).toHaveLength(7);
expect(screen.getByDisplayValue("50")).toBeInTheDocument();
expect(screen.getByDisplayValue("60")).toBeInTheDocument();
@@ -171,7 +177,7 @@ describe("ThroughputBucketsComponent", () => {
it("ensures default buckets are used when no buckets are provided", () => {
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={[]} />);
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
expect(screen.getAllByText(/Bucket \d+/)).toHaveLength(5);
expect(screen.getAllByDisplayValue("100")).toHaveLength(5);
});
});

View File

@@ -76,7 +76,7 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
value={bucket.maxThroughputPercentage}
onChange={(newValue) => handleBucketChange(bucket.id, newValue)}
showValue={false}
label={`Group ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
label={`Bucket ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
styles={{ root: { flex: 2, maxWidth: 400 } }}
disabled={bucket.maxThroughputPercentage === 100}
/>

View File

@@ -44,6 +44,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
},
instantMaximumThroughput: 5000,
softAllowedMaximumThroughput: 1000000,
isGlobalSecondaryIndex: false,
};
it("throughput input visible", () => {

View File

@@ -80,6 +80,7 @@ export interface ThroughputInputAutoPilotV3Props {
throughputError?: string;
instantMaximumThroughput: number;
softAllowedMaximumThroughput: number;
isGlobalSecondaryIndex: boolean;
}
interface ThroughputInputAutoPilotV3State {
@@ -284,7 +285,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
serverId,
numberOfRegions,
isMultimaster,
true,
false,
);
return (
<div>
@@ -375,6 +376,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
toolTipElement={getToolTipContainer(this.props.infoBubbleText)}
/>
</Label>
{!this.props.isGlobalSecondaryIndex && (
<>
{this.overrideWithProvisionedThroughputSettings() && (
<MessageBar
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
@@ -391,6 +394,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
ariaLabelledBy={labelId}
styles={getChoiceGroupStyles(this.props.wasAutopilotOriginallySet, this.props.isAutoPilotSelected, true)}
/>
</>
)}
</Stack>
);
};

View File

@@ -0,0 +1,196 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PartitionKeyComponent renders default component and matches snapshot 1`] = `
<Stack
styles={
{
"root": {
"maxWidth": 600,
},
}
}
tokens={
{
"childrenGap": 20,
}
}
>
<Stack
tokens={
{
"childrenGap": 10,
}
}
>
<Text
styles={
{
"root": {
"fontSize": 16,
"fontWeight": 600,
},
}
}
>
Change
partition key
</Text>
<Stack
horizontal={true}
tokens={
{
"childrenGap": 20,
}
}
>
<Stack
tokens={
{
"childrenGap": 5,
}
}
>
<Text
styles={
{
"root": {
"fontWeight": 600,
},
}
}
>
Current
partition key
</Text>
<Text
styles={
{
"root": {
"fontWeight": 600,
},
}
}
>
Partitioning
</Text>
</Stack>
<Stack
tokens={
{
"childrenGap": 5,
}
}
>
<Text />
<Text>
Non-hierarchical
</Text>
</Stack>
</Stack>
</Stack>
<StyledMessageBar
messageBarType={5}
>
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to the source container for the entire duration of the partition key change process.
<StyledLinkBase
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
target="_blank"
underline={true}
>
Learn more
</StyledLinkBase>
</StyledMessageBar>
<Text>
To change the partition key, a new destination container must be created or an existing destination container selected. Data will then be copied to the destination container.
</Text>
<CustomizedPrimaryButton
onClick={[Function]}
styles={
{
"root": {
"width": "fit-content",
},
}
}
text="Change"
/>
</Stack>
`;
exports[`PartitionKeyComponent renders read-only component and matches snapshot 1`] = `
<Stack
styles={
{
"root": {
"maxWidth": 600,
},
}
}
tokens={
{
"childrenGap": 20,
}
}
>
<Stack
tokens={
{
"childrenGap": 10,
}
}
>
<Stack
horizontal={true}
tokens={
{
"childrenGap": 20,
}
}
>
<Stack
tokens={
{
"childrenGap": 5,
}
}
>
<Text
styles={
{
"root": {
"fontWeight": 600,
},
}
}
>
Current
partition key
</Text>
<Text
styles={
{
"root": {
"fontWeight": 600,
},
}
}
>
Partitioning
</Text>
</Stack>
<Stack
tokens={
{
"childrenGap": 5,
}
}
>
<Text />
<Text>
Non-hierarchical
</Text>
</Stack>
</Stack>
</Stack>
</Stack>
`;

View File

@@ -231,6 +231,34 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
Non-hierarchically partitioned container.
</Text>
</Stack>
<Stack
tokens={
{
"childrenGap": 5,
}
}
>
<StyledTextFieldBase
defaultValue="/id"
disabled={true}
label="Unique keys"
styles={
{
"fieldGroup": {
"borderColor": "",
"height": 25,
"selectors": {
":disabled": {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
/>
</Stack>
</Stack>
`;
@@ -520,6 +548,34 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
Non-hierarchically partitioned container.
</Text>
</Stack>
<Stack
tokens={
{
"childrenGap": 5,
}
}
>
<StyledTextFieldBase
defaultValue="/id"
disabled={true}
label="Unique keys"
styles={
{
"fieldGroup": {
"borderColor": "",
"height": 25,
"selectors": {
":disabled": {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
/>
</Stack>
</Stack>
`;
@@ -769,6 +825,34 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
Non-hierarchically partitioned container.
</Text>
</Stack>
<Stack
tokens={
{
"childrenGap": 5,
}
}
>
<StyledTextFieldBase
defaultValue="/id"
disabled={true}
label="Unique keys"
styles={
{
"fieldGroup": {
"borderColor": "",
"height": 25,
"selectors": {
":disabled": {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
/>
</Stack>
</Stack>
`;
@@ -1083,6 +1167,34 @@ exports[`SubSettingsComponent renders 1`] = `
Non-hierarchically partitioned container.
</Text>
</Stack>
<Stack
tokens={
{
"childrenGap": 5,
}
}
>
<StyledTextFieldBase
defaultValue="/id"
disabled={true}
label="Unique keys"
styles={
{
"fieldGroup": {
"borderColor": "",
"height": 25,
"selectors": {
":disabled": {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
/>
</Stack>
</Stack>
`;
@@ -1371,5 +1483,33 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
Non-hierarchically partitioned container.
</Text>
</Stack>
<Stack
tokens={
{
"childrenGap": 5,
}
}
>
<StyledTextFieldBase
defaultValue="/id"
disabled={true}
label="Unique keys"
styles={
{
"fieldGroup": {
"borderColor": "",
"height": 25,
"selectors": {
":disabled": {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
/>
</Stack>
</Stack>
`;

View File

@@ -1,6 +1,7 @@
import * as Constants from "../../../Common/Constants";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
const zeroValue = 0;
@@ -57,6 +58,7 @@ export enum SettingsV2TabTypes {
ComputedPropertiesTab,
ContainerVectorPolicyTab,
ThroughputBucketsTab,
GlobalSecondaryIndexTab,
}
export enum ContainerPolicyTabTypes {
@@ -164,13 +166,15 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
case SettingsV2TabTypes.IndexingPolicyTab:
return "Indexing Policy";
case SettingsV2TabTypes.PartitionKeyTab:
return "Partition Keys (preview)";
return isFabricNative() ? "Partition Keys" : "Partition Keys (preview)";
case SettingsV2TabTypes.ComputedPropertiesTab:
return "Computed Properties";
case SettingsV2TabTypes.ContainerVectorPolicyTab:
return "Container Policies";
case SettingsV2TabTypes.ThroughputBucketsTab:
return "Throughput Buckets";
case SettingsV2TabTypes.GlobalSecondaryIndexTab:
return "Global Secondary Index (Preview)";
default:
throw new Error(`Unknown tab ${tab}`);
}

View File

@@ -17,7 +17,15 @@ export const collection = {
includedPaths: [],
excludedPaths: [],
}),
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
rawDataModel: {
uniqueKeyPolicy: {
uniqueKeys: [
{
paths: ["/id"],
},
],
},
},
usageSizeInKB: ko.observable(100),
offer: ko.observable<DataModels.Offer>({
autoscaleMaxThroughput: undefined,
@@ -48,6 +56,15 @@ export const collection = {
]),
vectorEmbeddingPolicy: ko.observable<DataModels.VectorEmbeddingPolicy>({} as DataModels.VectorEmbeddingPolicy),
fullTextPolicy: ko.observable<DataModels.FullTextPolicy>({} as DataModels.FullTextPolicy),
materializedViews: ko.observable<DataModels.MaterializedView[]>([
{ id: "view1", _rid: "rid1" },
{ id: "view2", _rid: "rid2" },
]),
materializedViewDefinition: ko.observable<DataModels.MaterializedViewDefinition>({
definition: "SELECT * FROM c WHERE c.id = 1",
sourceCollectionId: "source1",
sourceCollectionRid: "rid123",
}),
readSettings: () => {
return;
},

View File

@@ -60,6 +60,8 @@ exports[`SettingsComponent renders 1`] = `
"getDatabase": [Function],
"id": [Function],
"indexingPolicy": [Function],
"materializedViewDefinition": [Function],
"materializedViews": [Function],
"offer": [Function],
"partitionKey": {
"kind": "hash",
@@ -69,14 +71,25 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyProperties": [
"partitionKey",
],
"rawDataModel": {
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
}
}
isAutoPilotSelected={false}
isFixedContainer={false}
isGlobalSecondaryIndex={true}
onAutoPilotSelected={[Function]}
onMaxAutoPilotThroughputChange={[Function]}
onScaleDiscardableChange={[Function]}
@@ -139,6 +152,8 @@ exports[`SettingsComponent renders 1`] = `
"getDatabase": [Function],
"id": [Function],
"indexingPolicy": [Function],
"materializedViewDefinition": [Function],
"materializedViews": [Function],
"offer": [Function],
"partitionKey": {
"kind": "hash",
@@ -148,8 +163,18 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyProperties": [
"partitionKey",
],
"rawDataModel": {
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
}
@@ -173,6 +198,32 @@ exports[`SettingsComponent renders 1`] = `
timeToLiveSecondsBaseline={5}
/>
</PivotItem>
<PivotItem
headerText="Container Policies"
itemKey="ContainerVectorPolicyTab"
key="ContainerVectorPolicyTab"
style={
{
"marginTop": 20,
}
}
>
<ContainerPolicyComponent
fullTextPolicy={{}}
fullTextPolicyBaseline={{}}
isFullTextSearchEnabled={true}
isGlobalSecondaryIndex={true}
isVectorSearchEnabled={false}
onFullTextPolicyChange={[Function]}
onFullTextPolicyDirtyChange={[Function]}
onVectorEmbeddingPolicyChange={[Function]}
onVectorEmbeddingPolicyDirtyChange={[Function]}
resetShouldDiscardContainerPolicyChange={[Function]}
shouldDiscardContainerPolicies={false}
vectorEmbeddingPolicy={{}}
vectorEmbeddingPolicyBaseline={{}}
/>
</PivotItem>
<PivotItem
headerText="Indexing Policy"
itemKey="IndexingPolicyTab"
@@ -258,6 +309,8 @@ exports[`SettingsComponent renders 1`] = `
"getDatabase": [Function],
"id": [Function],
"indexingPolicy": [Function],
"materializedViewDefinition": [Function],
"materializedViews": [Function],
"offer": [Function],
"partitionKey": {
"kind": "hash",
@@ -267,8 +320,18 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyProperties": [
"partitionKey",
],
"rawDataModel": {
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
}
@@ -300,6 +363,7 @@ exports[`SettingsComponent renders 1`] = `
},
}
}
isReadOnly={false}
/>
</PivotItem>
<PivotItem
@@ -336,6 +400,111 @@ exports[`SettingsComponent renders 1`] = `
shouldDiscardComputedProperties={false}
/>
</PivotItem>
<PivotItem
headerText="Global Secondary Index (Preview)"
itemKey="GlobalSecondaryIndexTab"
key="GlobalSecondaryIndexTab"
style={
{
"marginTop": 20,
}
}
>
<GlobalSecondaryIndexComponent
collection={
{
"analyticalStorageTtl": [Function],
"changeFeedPolicy": [Function],
"computedProperties": [Function],
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
},
"databaseId": "test",
"defaultTtl": [Function],
"fullTextPolicy": [Function],
"geospatialConfig": [Function],
"getDatabase": [Function],
"id": [Function],
"indexingPolicy": [Function],
"materializedViewDefinition": [Function],
"materializedViews": [Function],
"offer": [Function],
"partitionKey": {
"kind": "hash",
"paths": [],
"version": 2,
},
"partitionKeyProperties": [
"partitionKey",
],
"rawDataModel": {
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function],
"usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
}
}
explorer={
Explorer {
"_isInitializingNotebooks": false,
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
}
}
/>
</PivotItem>
</StyledPivot>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { Text } from "@fluentui/react";
import { Stack, Text } from "@fluentui/react";
import React, { FunctionComponent } from "react";
import { InfoTooltip } from "../../../../Common/Tooltip/InfoTooltip";
import * as SharedConstants from "../../../../Shared/Constants";
@@ -44,13 +44,19 @@ export const CostEstimateText: FunctionComponent<CostEstimateTextProps> = ({
const currencySign: string = getCurrencySign(serverId);
const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multiplier) : getPricePerRu(serverId, multiplier);
const estimatedMonthlyCost = "Estimated monthly cost";
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>{estimatedCostDisclaimer}</InfoTooltip>;
const iconWithEstimatedCostDisclaimer: JSX.Element = (
<InfoTooltip ariaLabelForTooltip={`${estimatedMonthlyCost} ${currency} ${estimatedCostDisclaimer}`}>
{estimatedCostDisclaimer}
</InfoTooltip>
);
if (isAutoscale) {
return (
<Stack style={{ marginBottom: 6 }}>
<Text variant="small">
Estimated monthly cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
{estimatedMonthlyCost} ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
<b>
{currencySign + calculateEstimateNumber(monthlyPrice / 10)} -{" "}
{currencySign + calculateEstimateNumber(monthlyPrice)}{" "}
@@ -58,10 +64,12 @@ export const CostEstimateText: FunctionComponent<CostEstimateTextProps> = ({
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits / 10} - {requestUnits}{" "}
RU/s, {currencySign + pricePerRu}/RU)
</Text>
</Stack>
);
}
return (
<Stack style={{ marginBottom: 8 }}>
<Text variant="small">
Estimated cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
<b>
@@ -72,5 +80,6 @@ export const CostEstimateText: FunctionComponent<CostEstimateTextProps> = ({
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "}
{currencySign + pricePerRu}/RU)
</Text>
</Stack>
);
};

View File

@@ -1,5 +1,6 @@
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
import { Checkbox, DirectionalHint, Link, Separator, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
import { getWorkloadType } from "Common/DatabaseAccountUtility";
import { CostEstimateText } from "Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText";
import { useDatabases } from "Explorer/useDatabases";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants";
@@ -9,8 +10,8 @@ import { userContext } from "../../../UserContext";
import { getCollectionName } from "../../../Utils/APITypeUtils";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../../Utils/PricingUtils";
import { CostEstimateText } from "./CostEstimateText/CostEstimateText";
import "./ThroughputInput.less";
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
export interface ThroughputInputProps {
isDatabase: boolean;
@@ -18,6 +19,7 @@ export interface ThroughputInputProps {
isFreeTier: boolean;
showFreeTierExceedThroughputTooltip: boolean;
isQuickstart?: boolean;
isGlobalSecondaryIndex?: boolean;
setThroughputValue: (throughput: number) => void;
setIsAutoscale: (isAutoscale: boolean) => void;
setIsThroughputCapExceeded: (isThroughputCapExceeded: boolean) => void;
@@ -30,6 +32,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
isFreeTier,
showFreeTierExceedThroughputTooltip,
isQuickstart,
isGlobalSecondaryIndex,
setThroughputValue,
setIsAutoscale,
setIsThroughputCapExceeded,
@@ -41,7 +44,8 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
if (
isFreeTier ||
isQuickstart ||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType) ||
isFabricNative()
) {
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
} else if (workloadType === Constants.WorkloadType.Production) {
@@ -193,7 +197,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
</Text>
<InfoTooltip>{PricingUtils.getRuToolTipText()}</InfoTooltip>
</Stack>
{!isGlobalSecondaryIndex && (
<Stack horizontal verticalAlign="center">
<div role="radiogroup">
<input
@@ -227,37 +231,66 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
</label>
</div>
</Stack>
)}
{isAutoscaleSelected && (
<Stack className="throughputInputSpacing">
<Text variant="small" aria-label="capacity calculator of azure cosmos db">
Estimate your required RU/s with{" "}
<Link
className="underlinedLink outlineNone"
target="_blank"
href="https://cosmos.azure.com/capacitycalculator/"
aria-label="capacity calculator of azure cosmos db"
<Text style={{ marginTop: -2, fontSize: 12 }}>
Your container throughput will automatically scale up to the maximum value you select, from a minimum of 10%
of that value.
</Text>
<Stack horizontal verticalAlign="end" tokens={{ childrenGap: 8 }}>
<Stack tokens={{ childrenGap: 4 }}>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
Minimum RU/s
</Text>
<InfoTooltip>The minimum RU/s your container will scale to</InfoTooltip>
</Stack>
<Text
style={{
fontFamily: "Segoe UI",
width: 70,
height: 27,
border: "none",
fontSize: 14,
backgroundColor: "transparent",
fontWeight: 400,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
capacity calculator
</Link>
.
{Math.round(throughput / 10).toString()}
</Text>
</Stack>
<Text
style={{
fontFamily: "Segoe UI",
fontSize: 12,
fontWeight: 400,
paddingBottom: 6,
}}
>
x 10 =
</Text>
<Stack horizontal>
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }} aria-label="maxRUDescription">
{isDatabase ? "Database" : getCollectionName()} Max RU/s
<Stack tokens={{ childrenGap: 4 }}>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
Maximum RU/s
</Text>
<InfoTooltip>{getAutoScaleTooltip()}</InfoTooltip>
</Stack>
<TextField
id="autoscaleRUValueField"
type="number"
styles={{
fieldGroup: { width: 300, height: 27 },
field: { fontSize: 12 },
fieldGroup: { width: 100, height: 27, flexShrink: 0 },
field: { fontSize: 14, fontWeight: 400 },
}}
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
onChange={(_event, newInput?: string) => onThroughputValueChange(newInput)}
step={AutoPilotUtils.autoPilotIncrementStep}
min={AutoPilotUtils.autoPilotThroughput1K}
max={isSharded ? Number.MAX_SAFE_INTEGER.toString() : "10000"}
@@ -266,16 +299,26 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
required={true}
errorMessage={throughputError}
/>
</Stack>
</Stack>
<Text variant="small">
Your {isDatabase ? "database" : getCollectionName().toLocaleLowerCase()} throughput will automatically scale
from{" "}
<b>
{AutoPilotUtils.getMinRUsBasedOnUserInput(throughput)} RU/s (10% of max RU/s) - {throughput} RU/s
</b>{" "}
based on usage.
<CostEstimateText requestUnits={throughput} isAutoscale={isAutoscaleSelected} />
<Stack className="throughputInputSpacing">
<Text variant="small" aria-label="ruDescription">
Estimate your required RU/s with&nbsp;
<Link
className="underlinedLink"
target="_blank"
href="https://cosmos.azure.com/capacitycalculator/"
aria-label="Capacity calculator"
>
capacity calculator
</Link>
.
</Text>
</Stack>
<Separator className="panelSeparator" style={{ paddingTop: -8, paddingBottom: -8 }} />
</Stack>
)}
{!isAutoscaleSelected && (
@@ -298,7 +341,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
</Text>
<InfoTooltip>{getAutoScaleTooltip()}</InfoTooltip>
</Stack>
<TooltipHost
directionalHint={DirectionalHint.topLeftEdge}
content={
@@ -323,11 +365,10 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
errorMessage={throughputError}
/>
</TooltipHost>
<CostEstimateText requestUnits={throughput} isAutoscale={isAutoscaleSelected} />
</Stack>
)}
<CostEstimateText requestUnits={throughput} isAutoscale={isAutoscaleSelected} />
{throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && (
<Stack horizontal verticalAlign="start">
<Checkbox

View File

@@ -9,6 +9,7 @@ import {
Stack,
TextField,
} from "@fluentui/react";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
import {
@@ -29,6 +30,7 @@ export interface IVectorEmbeddingPoliciesComponentProps {
discardChanges?: boolean;
onChangesDiscarded?: () => void;
disabled?: boolean;
isGlobalSecondaryIndex?: boolean;
}
export interface VectorEmbeddingPolicyData {
@@ -39,8 +41,7 @@ export interface VectorEmbeddingPolicyData {
indexType: VectorIndex["type"] | "none";
pathError: string;
dimensionsError: string;
diskANNShardKey?: string;
diskANNShardKeyError?: string;
vectorIndexShardKey?: string[];
indexingSearchListSize?: number;
indexingSearchListSizeError?: string;
quantizationByteSize?: number;
@@ -87,6 +88,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
discardChanges,
onChangesDiscarded,
disabled,
isGlobalSecondaryIndex,
}): JSX.Element => {
const onVectorEmbeddingPathError = (path: string, index?: number): string => {
let error = "";
@@ -132,12 +134,6 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
return error;
};
//TODO: no restrictions yet due to this field being removed for now.
// Uncomment and replace with validation code when field is reinstated
// const onDiskANNShardKeyError = (shardKey: string): string => {
// return "";
// };
const initializeData = (vectorEmbeddings: VectorEmbedding[], vectorIndexes: VectorIndex[]) => {
const mergedData: VectorEmbeddingPolicyData[] = [];
vectorEmbeddings.forEach((embedding) => {
@@ -147,6 +143,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
indexType: matchingIndex?.type || "none",
indexingSearchListSize: matchingIndex?.indexingSearchListSize || undefined,
quantizationByteSize: matchingIndex?.quantizationByteSize || undefined,
vectorIndexShardKey: matchingIndex?.vectorIndexShardKey || undefined,
pathError: onVectorEmbeddingPathError(embedding.path),
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
});
@@ -186,6 +183,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
type: policy.indexType,
indexingSearchListSize: policy.indexingSearchListSize,
quantizationByteSize: policy.quantizationByteSize,
vectorIndexShardKey: policy.vectorIndexShardKey,
}) as VectorIndex,
);
const validationPassed = vectorEmbeddingPolicyData.every(
@@ -247,20 +245,16 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
// TODO: uncomment after Ignite
// DiskANNShardKey was removed for Ignite due to backend problems. Leaving this here as it will be reinstated immediately after Ignite
// const onDiskANNShardKeyChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
// const value = event.target.value.trim();
// const vectorEmbeddings = [...vectorEmbeddingPolicyData];
// if (!vectorEmbeddings[index]?.diskANNShardKey && !value.startsWith("/")) {
// vectorEmbeddings[index].diskANNShardKey = "/" + value;
// } else {
// vectorEmbeddings[index].diskANNShardKey = value;
// }
// const error = onDiskANNShardKeyError(value);
// vectorEmbeddings[index].diskANNShardKeyError = error;
// setVectorEmbeddingPolicyData(vectorEmbeddings);
// }
const onShardKeyChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value.trim();
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
if (!vectorEmbeddings[index]?.vectorIndexShardKey?.[0] && !value.startsWith("/")) {
vectorEmbeddings[index].vectorIndexShardKey = ["/" + value];
} else {
vectorEmbeddings[index].vectorIndexShardKey = [value];
}
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
const onVectorEmbeddingPolicyChange = (
index: number,
@@ -292,6 +286,11 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
const getQuantizationByteSizeTooltipContent = (): string => {
const containerName: string = isGlobalSecondaryIndex ? "global secondary index" : "container";
return `This is dynamically set by the ${containerName} if left blank, or it can be set to a fixed number`;
};
return (
<Stack tokens={{ childrenGap: 4 }}>
{vectorEmbeddingPolicyData &&
@@ -402,6 +401,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
styles={labelStyles}
>
Quantization byte size
<InfoTooltip>{getQuantizationByteSizeTooltipContent()}</InfoTooltip>
</Label>
<TextField
disabled={
@@ -431,26 +431,18 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
}
/>
</Stack>
{/*TODO: uncomment after Ignite */}
{/* DiskANNShardKey was removed for Ignite due to backend problems. Leaving this here as it will be reinstated immediately after Ignite
<Stack
style={{ marginLeft: "10px" }}
>
<Label
disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"}
styles={labelStyles}
>DiskANN shard key</Label>
<Stack style={{ marginLeft: "10px" }}>
<Label disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"} styles={labelStyles}>
Vector index shard key
</Label>
<TextField
disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"}
id={`vector-policy-diskANNShardKey-${index + 1}`}
id={`vector-policy-vectorIndexShardKey-${index + 1}`}
styles={textFieldStyles}
value={String(vectorEmbeddingPolicy.diskANNShardKey || "")}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
onDiskANNShardKeyChange(index, event)
}
value={String(vectorEmbeddingPolicy.vectorIndexShardKey?.[0] ?? "")}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onShardKeyChange(index, event)}
/>
</Stack>
*/}
</Stack>
)}
</Stack>

View File

@@ -6,6 +6,7 @@ import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
// TODO: this does not seem to be used. Remove?
export class DataSamplesUtil {
private static readonly DialogTitle = "Create Sample Container";
constructor(private container: Explorer) {}

View File

@@ -12,6 +12,7 @@ import { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } fro
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
import { featureRegistered } from "Utils/FeatureRegistrationUtils";
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as ko from "knockout";
@@ -30,6 +31,7 @@ import { readDatabases } from "../Common/dataAccess/readDatabases";
import * as DataModels from "../Contracts/DataModels";
import { ContainerConnectionInfo, IPhoenixServiceInfo, IProvisionData, IResponse } from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { UploadDetailsRecord } from "../Contracts/ViewModels";
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
import { PhoenixClient } from "../Phoenix/PhoenixClient";
import * as ExplorerSettings from "../Shared/ExplorerSettings";
@@ -55,7 +57,7 @@ import type NotebookManager from "./Notebook/NotebookManager";
import { NotebookPaneContent } from "./Notebook/NotebookManager";
import { NotebookUtil } from "./Notebook/NotebookUtil";
import { useNotebook } from "./Notebook/useNotebook";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel";
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
@@ -282,6 +284,42 @@ export default class Explorer {
}
}
public openInVsCode(): void {
const activeTab = useTabs.getState().activeTab;
const resourceId = encodeURIComponent(userContext.databaseAccount.id);
const database = encodeURIComponent(activeTab?.collection?.databaseId);
const container = encodeURIComponent(activeTab?.collection?.id());
const baseUrl = `vscode://ms-azuretools.vscode-cosmosdb?resourceId=${resourceId}`;
const vscodeUrl = activeTab ? `${baseUrl}&database=${database}&container=${container}` : baseUrl;
const openVSCodeDialogProps: DialogProps = {
linkProps: {
linkText: "Download Visual Studio Code",
linkUrl: "https://code.visualstudio.com/download",
},
isModal: true,
title: `Open your Azure Cosmos DB account in Visual Studio Code`,
subText: `Please ensure Visual Studio Code is installed on your device.
If you don't have it installed, please download it from the link below.`,
primaryButtonText: "Open in VS Code",
secondaryButtonText: "Cancel",
onPrimaryButtonClick: () => {
try {
window.location.href = vscodeUrl;
TelemetryProcessor.traceStart(Action.OpenVSCode);
} catch (error) {
logConsoleError(`Failed to open VS Code: ${getErrorMessage(error)}`);
}
},
onSecondaryButtonClick: () => {
useDialog.getState().closeDialog();
TelemetryProcessor.traceCancel(Action.OpenVSCode);
},
};
useDialog.getState().openDialog(openVSCodeDialogProps);
}
public async openCESCVAFeedbackBlade(): Promise<void> {
sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade });
Logger.logInfo(
@@ -910,7 +948,9 @@ export default class Explorer {
}
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
if (useNotebook.getState().isPhoenixFeatures) {
if (userContext.features.enableCloudShell) {
this.connectToNotebookTerminal(kind);
} else if (useNotebook.getState().isPhoenixFeatures) {
await this.allocateContainer(PoolIdType.DefaultPoolId);
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
@@ -1076,8 +1116,8 @@ export default class Explorer {
}
}
public openUploadItemsPane(): void {
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
public openUploadItemsPane(onUpload?: (data: UploadDetailsRecord[]) => void): void {
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane onUpload={onUpload} />);
}
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
useSidePanel
@@ -1085,7 +1125,7 @@ export default class Explorer {
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
}
public getDownloadModalConent(fileName: string): JSX.Element {
public getDownloadModalContent(fileName: string): JSX.Element {
if (useNotebook.getState().isPhoenixNotebooks) {
return (
<>
@@ -1131,6 +1171,11 @@ export default class Explorer {
await this.initNotebooks(userContext.databaseAccount);
}
if (userContext.authType === AuthType.AAD && userContext.apiType === "SQL") {
const throughputBucketsEnabled = await featureRegistered(userContext.subscriptionId, "ThroughputBucketing");
updateUserContext({ throughputBucketsEnabled });
}
this.refreshSampleData();
}

View File

@@ -16,7 +16,12 @@ import * as StorageUtility from "../../../Shared/StorageUtility";
import { LocalStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import {
logConsoleError,
logConsoleInfo,
logConsoleProgress,
logConsoleWarning,
} from "../../../Utils/NotificationConsoleUtils";
import { EditorReact } from "../../Controls/Editor/EditorReact";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import * as TabComponent from "../../Controls/Tabs/TabComponent";
@@ -1083,6 +1088,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
public static reportToConsole(type: ConsoleDataType.InProgress, msg: string, ...errorData: any[]): () => void;
public static reportToConsole(type: ConsoleDataType.Info, msg: string, ...errorData: any[]): void;
public static reportToConsole(type: ConsoleDataType.Error, msg: string, ...errorData: any[]): void;
public static reportToConsole(type: ConsoleDataType.Warning, msg: string, ...errorData: any[]): void;
public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) {
let errorDataStr = "";
if (errorData && errorData.length > 0) {
@@ -1099,6 +1105,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return logConsoleInfo(consoleMessage);
case ConsoleDataType.InProgress:
return logConsoleProgress(consoleMessage);
case ConsoleDataType.Warning:
return logConsoleWarning(consoleMessage);
}
}

View File

@@ -14,6 +14,7 @@ import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
import SettingsIcon from "../../../../images/settings_15x15.svg";
import SynapseIcon from "../../../../images/synapse-link.svg";
import VSCodeIcon from "../../../../images/vscode.svg";
import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants";
import { Platform, configContext } from "../../../ConfigContext";
@@ -60,6 +61,10 @@ export function createStaticCommandBarButtons(
addDivider();
buttons.push(addSynapseLink);
}
if (userContext.apiType !== "Gremlin") {
const addVsCode = createOpenVsCodeDialogButton(container);
buttons.push(addVsCode);
}
}
if (isDataplaneRbacSupported(userContext.apiType)) {
@@ -126,13 +131,14 @@ export function createContextCommandBarButtons(
const buttons: CommandButtonComponentProps[] = [];
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
const label =
useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell ? "Open Mongo Shell" : "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (useNotebook.getState().isShellEnabled) {
if (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
@@ -146,7 +152,7 @@ export function createContextCommandBarButtons(
}
if (
useNotebook.getState().isShellEnabled &&
(useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) &&
!selectedNodeState.isDatabaseNodeOrNoneSelected() &&
userContext.apiType === "Cassandra"
) {
@@ -267,6 +273,18 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
};
}
function createOpenVsCodeDialogButton(container: Explorer): CommandButtonComponentProps {
const label = "Visual Studio Code";
return {
iconSrc: VSCodeIcon,
iconAlt: label,
onCommandClick: () => container.openInVsCode(),
commandButtonLabel: label,
hasPopup: false,
ariaLabel: label,
};
}
function createLoginForEntraIDButton(container: Explorer): CommandButtonComponentProps {
if (configContext.platform !== Platform.Portal) {
return undefined;
@@ -455,7 +473,7 @@ function createOpenTerminalButtonByKind(
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (useNotebook.getState().isNotebookEnabled) {
if (useNotebook.getState().isNotebookEnabled || userContext.features.enableCloudShell) {
container.openNotebookTerminal(terminalKind);
}
},
@@ -499,6 +517,6 @@ export function createPostgreButtons(container: Explorer): CommandButtonComponen
export function createVCoreMongoButtons(container: Explorer): CommandButtonComponentProps[] {
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.VCoreMongo);
return [openVCoreMongoTerminalButton];
const addVsCode = createOpenVsCodeDialogButton(container);
return [openVCoreMongoTerminalButton, addVsCode];
}

View File

@@ -13,4 +13,5 @@ export enum ConsoleDataType {
Info = 0,
Error = 1,
InProgress = 2,
Warning = 3,
}

View File

@@ -173,8 +173,20 @@
.message {
flex-grow: 1;
white-space:pre-wrap;
overflow-wrap: break-word;
word-break: break-word;
}
}
}
}
@media (max-width: 768px) {
.notificationConsoleContents {
overflow-y: auto;
.notificationConsoleData {
overflow: visible;
}
}
}
}

View File

@@ -14,6 +14,7 @@ import ErrorRedIcon from "../../../../images/error_red.svg";
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
import InfoIcon from "../../../../images/info_color.svg";
import LoadingIcon from "../../../../images/loading.svg";
import WarningIcon from "../../../../images/warning.svg";
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
import { userContext } from "../../../UserContext";
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
@@ -91,6 +92,9 @@ export class NotificationConsoleComponent extends React.Component<
const numInfoItems = this.state.allConsoleData.filter(
(data: ConsoleData) => data.type === ConsoleDataType.Info,
).length;
const numWarningItems = this.state.allConsoleData.filter(
(data: ConsoleData) => data.type === ConsoleDataType.Warning,
).length;
return (
<div className="notificationConsoleContainer">
@@ -118,6 +122,10 @@ export class NotificationConsoleComponent extends React.Component<
<img src={infoBubbleIcon} alt="Info items" />
<span className="numInfoItems">{numInfoItems}</span>
</span>
<span className="notificationConsoleHeaderIconWithData">
<img src={WarningIcon} alt="Warning items" />
<span className="numWarningItems">{numWarningItems}</span>
</span>
</span>
{userContext.features.pr && <PrPreview pr={userContext.features.pr} />}
<span className="consoleSplitter" />
@@ -198,6 +206,7 @@ export class NotificationConsoleComponent extends React.Component<
{item.type === ConsoleDataType.Info && <img className="infoIcon" src={InfoIcon} alt="info" />}
{item.type === ConsoleDataType.Error && <img className="errorIcon" src={ErrorRedIcon} alt="error" />}
{item.type === ConsoleDataType.InProgress && <img className="loaderIcon" src={LoaderIcon} alt="in progress" />}
{item.type === ConsoleDataType.Warning && <img className="warningIcon" src={WarningIcon} alt="warning" />}
<span className="date">{item.date}</span>
<span className="message" role="alert" aria-live="assertive">
{item.message}

View File

@@ -59,6 +59,19 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
0
</span>
</span>
<span
className="notificationConsoleHeaderIconWithData"
>
<img
alt="Warning items"
src={{}}
/>
<span
className="numWarningItems"
>
0
</span>
</span>
</span>
<span
className="consoleSplitter"
@@ -229,6 +242,19 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
1
</span>
</span>
<span
className="notificationConsoleHeaderIconWithData"
>
<img
alt="Warning items"
src={{}}
/>
<span
className="numWarningItems"
>
0
</span>
</span>
</span>
<span
className="consoleSplitter"

View File

@@ -1,6 +1,6 @@
import { shallow } from "enzyme";
import React from "react";
import Explorer from "../Explorer";
import Explorer from "../../Explorer";
import { AddCollectionPanel } from "./AddCollectionPanel";
const props = {

View File

@@ -21,11 +21,25 @@ import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels";
import {
FullTextPoliciesComponent,
getFullTextLanguageOptions,
} from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
import {
AllPropertiesIndexed,
AnalyticalStoreHeader,
ContainerVectorPolicyTooltipContent,
FullTextPolicyDefault,
getPartitionKey,
getPartitionKeyName,
getPartitionKeyPlaceHolder,
getPartitionKeyTooltipText,
isFreeTierAccount,
isSynapseLinkEnabled,
parseUniqueKeys,
scrollToSection,
SharedDatabaseDefault,
shouldShowAnalyticalStoreOptions,
UniqueKeysHeader,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import { useSidePanel } from "hooks/useSidePanel";
import { useTeachingBubble } from "hooks/useTeachingBubble";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
@@ -35,22 +49,18 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils";
import {
isCapabilityEnabled,
isFullTextSearchEnabled,
isServerlessAccount,
isVectorSearchEnabled,
} from "Utils/CapabilityUtils";
import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { getUpsellMessage } from "Utils/PricingUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
import "../Controls/ThroughputInput/ThroughputInput.less";
import { ContainerSampleGenerator } from "../DataSamples/ContainerSampleGenerator";
import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { PanelFooterComponent } from "./PanelFooterComponent";
import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent";
import { PanelLoadingScreen } from "./PanelLoadingScreen";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import { CollapsibleSectionComponent } from "../../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import { ContainerSampleGenerator } from "../../DataSamples/ContainerSampleGenerator";
import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases";
import { PanelFooterComponent } from "../PanelFooterComponent";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
import { PanelLoadingScreen } from "../PanelLoadingScreen";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
export interface AddCollectionPanelProps {
explorer: Explorer;
@@ -58,40 +68,6 @@ export interface AddCollectionPanelProps {
isQuickstart?: boolean;
}
const SharedDatabaseDefault: DataModels.IndexingPolicy = {
indexingMode: "consistent",
automatic: true,
includedPaths: [],
excludedPaths: [
{
path: "/*",
},
],
};
export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
indexingMode: "consistent",
automatic: true,
includedPaths: [
{
path: "/*",
indexes: [
{
kind: "Range",
dataType: "Number",
precision: -1,
},
{
kind: "Range",
dataType: "String",
precision: -1,
},
],
},
],
excludedPaths: [],
};
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
vectorEmbeddings: [],
};
@@ -130,6 +106,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
private collectionThroughput: number;
private isCollectionAutoscale: boolean;
private isCostAcknowledged: boolean;
private showFullTextSearch: boolean;
constructor(props: AddCollectionPanelProps) {
super(props);
@@ -144,7 +121,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
enableIndexing: true,
isSharded: userContext.apiType !== "Tables",
partitionKey: this.getPartitionKey(),
partitionKey: getPartitionKey(props.isQuickstart),
subPartitionKeys: [],
enableDedicatedThroughput: false,
createMongoWildCardIndex:
@@ -160,10 +137,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
vectorEmbeddingPolicy: [],
vectorIndexingPolicy: [],
vectorPolicyValidated: true,
fullTextPolicy: { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] },
fullTextPolicy: FullTextPolicyDefault,
fullTextIndexes: [],
fullTextPolicyValidated: true,
};
this.showFullTextSearch = userContext.apiType === "SQL";
}
componentDidMount(): void {
@@ -174,7 +153,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
componentDidUpdate(_prevProps: AddCollectionPanelProps, prevState: AddCollectionPanelState): void {
if (this.state.errorMessage && this.state.errorMessage !== prevState.errorMessage) {
this.scrollToSection("panelContainer");
scrollToSection("panelContainer");
}
}
@@ -191,7 +170,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/>
)}
{!this.state.errorMessage && this.isFreeTierAccount() && (
{!this.state.errorMessage && isFreeTierAccount() && (
<PanelInfoErrorComponent
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
messageType="info"
@@ -286,7 +265,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<div className="panelMainContent">
{!(isFabricNative() && this.props.databaseId !== undefined) && (
<Stack hidden={userContext.apiType === "Tables"}>
<Stack hidden={userContext.apiType === "Tables"} style={{ marginBottom: -2 }}>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
@@ -351,8 +330,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
required
type="text"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder="Type a new database id"
size={40}
className="panelTextField"
@@ -399,10 +378,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={true}
isSharded={this.state.isSharded}
isFreeTier={this.isFreeTierAccount()}
isFreeTier={isFreeTierAccount()}
isQuickstart={this.props.isQuickstart}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
@@ -428,12 +407,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
responsiveMode={999}
/>
)}
<Separator className="panelSeparator" />
</Stack>
)}
<Separator className="panelSeparator" style={{ marginTop: -4, marginBottom: -4 }} />
<Stack>
<Stack horizontal>
<Stack horizontal style={{ marginTop: -5, marginBottom: 1 }}>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{`${getCollectionName()} id`}
@@ -459,8 +438,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-required
required
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={`e.g., ${getCollectionName()}1`}
size={40}
className="panelTextField"
@@ -471,10 +450,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
/>
</Stack>
<Separator className="panelSeparator" style={{ marginTop: -5, marginBottom: -5 }} />
{this.shouldShowIndexingOptionsForFreeTierAccount() && (
<Stack>
<Stack horizontal>
<Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Indexing
@@ -520,7 +499,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
(!this.state.isSharedThroughputChecked ||
this.props.explorer.isFixedCollectionWithSharedThroughputSupported()) && (
<Stack>
<Stack horizontal>
<Stack horizontal style={{ marginTop: -5, marginBottom: -4 }}>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Sharding
@@ -576,20 +555,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.state.isSharded && (
<Stack>
<Stack horizontal>
<Stack horizontal style={{ marginTop: -5, marginBottom: -4 }}>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{this.getPartitionKeyName()}
{getPartitionKeyName()}
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={this.getPartitionKeyTooltipText()}
>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={this.getPartitionKeyTooltipText()}
ariaLabel={getPartitionKeyTooltipText()}
/>
</TooltipHost>
</Stack>
@@ -603,8 +579,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
required
size={40}
className="panelTextField"
placeholder={this.getPartitionKeyPlaceHolder()}
aria-label={this.getPartitionKeyName()}
placeholder={getPartitionKeyPlaceHolder()}
aria-label={getPartitionKeyName()}
pattern={userContext.apiType === "Gremlin" ? "^/[^/]*" : ".*"}
title={userContext.apiType === "Gremlin" ? "May not use composite partition key" : ""}
value={this.state.partitionKey}
@@ -623,7 +599,7 @@ 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>
<Stack style={{ marginBottom: 2, marginTop: -5 }} key={`uniqueKey${index}`} horizontal>
<div
style={{
width: "20px",
@@ -642,8 +618,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={index > 0 ? 1 : 0}
className="panelTextField"
autoComplete="off"
placeholder={this.getPartitionKeyPlaceHolder(index)}
aria-label={this.getPartitionKeyName()}
placeholder={getPartitionKeyPlaceHolder(index)}
aria-label={getPartitionKeyName()}
pattern={".*"}
title={""}
value={subPartitionKey}
@@ -691,6 +667,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)}
</Stack>
)}
<Separator className="panelSeparator" style={{ marginTop: 2, marginBottom: -4 }} />
</Stack>
)}
@@ -734,10 +711,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowCollectionThroughputInput() && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={false}
isSharded={this.state.isSharded}
isFreeTier={this.isFreeTierAccount()}
isFreeTier={isFreeTierAccount()}
isQuickstart={this.props.isQuickstart}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
@@ -751,28 +728,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)}
{!isFabricNative() && userContext.apiType === "SQL" && (
<Stack>
<Stack horizontal>
<Text className="panelTextBold" variant="small">
Unique keys
</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."
}
>
<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>
<Stack style={{ marginTop: -2, marginBottom: -4 }}>
{UniqueKeysHeader()}
{this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => {
return (
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${i}`} horizontal>
@@ -820,10 +777,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack>
)}
{this.shouldShowAnalyticalStoreOptions() && (
<Stack className="panelGroupSpacing">
<Separator className="panelSeparator" style={{ marginTop: -15, marginBottom: -4 }} />
{shouldShowAnalyticalStoreOptions() && (
<Stack className="panelGroupSpacing" style={{ marginTop: -4 }}>
<Text className="panelTextBold" variant="small">
{this.getAnalyticalStorageContent()}
{AnalyticalStoreHeader()}
</Text>
<Stack horizontal verticalAlign="center">
@@ -831,7 +790,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input
className="panelRadioBtn"
checked={this.state.enableAnalyticalStore}
disabled={!this.isSynapseLinkEnabled()}
disabled={!isSynapseLinkEnabled()}
aria-label="Enable analytical store"
aria-checked={this.state.enableAnalyticalStore}
name="analyticalStore"
@@ -846,7 +805,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input
className="panelRadioBtn"
checked={!this.state.enableAnalyticalStore}
disabled={!this.isSynapseLinkEnabled()}
disabled={!isSynapseLinkEnabled()}
aria-label="Disable analytical store"
aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore"
@@ -860,11 +819,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</div>
</Stack>
{!this.isSynapseLinkEnabled() && (
{!isSynapseLinkEnabled() && (
<Stack className="panelGroupSpacing">
<Text variant="small">
Azure Synapse Link is required for creating an analytical store{" "}
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account.{" "}
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br />
<Link
href="https://aka.ms/cosmosdb-synapselink"
target="_blank"
@@ -890,9 +849,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
title="Container Vector Policy"
isExpandedByDefault={false}
onExpand={() => {
this.scrollToSection("collapsibleVectorPolicySectionContent");
scrollToSection("collapsibleVectorPolicySectionContent");
}}
tooltipContent={this.getContainerVectorPolicyTooltipContent()}
tooltipContent={ContainerVectorPolicyTooltipContent()}
>
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}>
@@ -918,7 +877,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
title="Container Full Text Search Policy"
isExpandedByDefault={false}
onExpand={() => {
this.scrollToSection("collapsibleFullTextPolicySectionContent");
scrollToSection("collapsibleFullTextPolicySectionContent");
}}
//TODO: uncomment when learn more text becomes available
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
@@ -946,7 +905,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
isExpandedByDefault={false}
onExpand={() => {
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
this.scrollToSection("collapsibleAdvancedSectionContent");
scrollToSection("collapsibleAdvancedSectionContent");
}}
>
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
@@ -1056,31 +1015,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}));
}
private getPartitionKeyName(isLowerCase?: boolean): string {
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
}
private getPartitionKeyPlaceHolder(index?: number): string {
switch (userContext.apiType) {
case "Mongo":
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";
}
}
private onCreateNewDatabaseRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
if (event.target.checked && !this.state.createNewDatabase) {
this.setState({
@@ -1168,48 +1102,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return !!selectedDatabase?.offer();
}
private isFreeTierAccount(): boolean {
return userContext.databaseAccount?.properties?.enableFreeTier;
}
private getFreeTierIndexingText(): string {
return this.state.enableIndexing
? "All properties in your documents will be indexed by default for flexible and efficient queries."
: "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.";
}
private getPartitionKeyTooltipText(): string {
if (userContext.apiType === "Mongo") {
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. Its critical to choose a field that will evenly distribute your data.";
}
let tooltipText = `The ${this.getPartitionKeyName(
true,
)} 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.`;
if (userContext.apiType === "SQL") {
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
}
return tooltipText;
}
private getPartitionKey(): string {
if (userContext.apiType !== "SQL" && userContext.apiType !== "Mongo") {
return "";
}
if (userContext.features.partitionKeyDefault) {
return userContext.apiType === "SQL" ? "/id" : "_id";
}
if (userContext.features.partitionKeyDefault2) {
return userContext.apiType === "SQL" ? "/pk" : "pk";
}
if (this.props.isQuickstart) {
return userContext.apiType === "SQL" ? "/categoryId" : "categoryId";
}
return "";
}
private getPartitionKeySubtext(): string {
if (
userContext.features.partitionKeyDefault &&
@@ -1221,34 +1119,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return "";
}
private getAnalyticalStorageContent(): JSX.Element {
return (
<Text variant="small">
Enable analytical store capability to perform near real-time analytics on your operational data, without
impacting the performance of transactional workloads.{" "}
<Link
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
target="_blank"
href="https://aka.ms/analytical-store-overview"
>
Learn more
</Link>
</Text>
);
}
private getContainerVectorPolicyTooltipContent(): JSX.Element {
return (
<Text variant="small">
Describe any properties in your data that contain vectors, so that they can be made available for similarity
queries.{" "}
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
Learn more
</Link>
</Text>
);
}
//TODO: uncomment when learn more text becomes available
// private getContainerFullTextPolicyTooltipContent(): JSX.Element {
// return (
@@ -1279,7 +1149,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
if (!this.isFreeTierAccount()) {
if (!isFreeTierAccount()) {
return false;
}
@@ -1288,45 +1158,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
: this.isSelectedDatabaseSharedThroughput();
}
private shouldShowAnalyticalStoreOptions(): boolean {
if (isFabricNative() || configContext.platform === Platform.Emulator) {
return false;
}
switch (userContext.apiType) {
case "SQL":
case "Mongo":
return true;
default:
return false;
}
}
private isSynapseLinkEnabled(): boolean {
if (!userContext.databaseAccount) {
return false;
}
const { properties } = userContext.databaseAccount;
if (!properties) {
return false;
}
if (properties.enableAnalyticalStorage) {
return true;
}
return properties.capabilities?.some(
(capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics,
);
}
private shouldShowVectorSearchParameters() {
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
}
private shouldShowFullTextSearchParameters() {
return isFullTextSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
return !isFabricNative() && this.showFullTextSearch;
}
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
@@ -1401,11 +1238,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
private getAnalyticalStorageTtl(): number {
if (!this.isSynapseLinkEnabled()) {
if (!isSynapseLinkEnabled()) {
return undefined;
}
if (!this.shouldShowAnalyticalStoreOptions()) {
if (!shouldShowAnalyticalStoreOptions()) {
return undefined;
}
@@ -1419,10 +1256,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return Constants.AnalyticalStorageTtl.Disabled;
}
private scrollToSection(id: string): void {
document.getElementById(id)?.scrollIntoView();
}
private getSampleDBName(): string {
const existingSampleDBs = useDatabases
.getState()
@@ -1457,7 +1290,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
partitionKeyString = "/'$pk'";
}
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = this.parseUniqueKeys();
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(this.state.uniqueKeys);
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
const partitionKey: DataModels.PartitionKey = partitionKeyString
? {
@@ -1485,7 +1318,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
};
}
if (this.shouldShowFullTextSearchParameters()) {
if (this.showFullTextSearch) {
indexingPolicy.fullTextIndexes = this.state.fullTextIndexes;
}
@@ -1519,7 +1352,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
let offerThroughput: number;
let autoPilotMaxThroughput: number;
if (databaseLevelThroughput) {
// Throughput
if (isFabricNative()) {
// Fabric Native accounts are always autoscale and have a fixed throughput of 1K
autoPilotMaxThroughput = AutoPilotUtils.autoPilotThroughput1K;
offerThroughput = undefined;
} else if (databaseLevelThroughput) {
if (this.state.createNewDatabase) {
if (this.isNewDatabaseAutoscale) {
autoPilotMaxThroughput = this.newDatabaseThroughput;

View File

@@ -0,0 +1,232 @@
import { DirectionalHint, Icon, Link, Stack, Text, TooltipHost } from "@fluentui/react";
import * as Constants from "Common/Constants";
import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels";
import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import React from "react";
import { userContext } from "UserContext";
export function getPartitionKeyTooltipText(): string {
if (userContext.apiType === "Mongo") {
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. Its critical to choose a field that will evenly distribute your data.";
}
let tooltipText = `The ${getPartitionKeyName(
true,
)} 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.`;
if (userContext.apiType === "SQL") {
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
}
return tooltipText;
}
export function getPartitionKeyName(isLowerCase?: boolean): string {
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
}
export function getPartitionKeyPlaceHolder(index?: number): string {
switch (userContext.apiType) {
case "Mongo":
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";
}
}
export function getPartitionKey(isQuickstart?: boolean): string {
if (userContext.apiType !== "SQL" && userContext.apiType !== "Mongo") {
return "";
}
if (userContext.features.partitionKeyDefault) {
return userContext.apiType === "SQL" ? "/id" : "_id";
}
if (userContext.features.partitionKeyDefault2) {
return userContext.apiType === "SQL" ? "/pk" : "pk";
}
if (isQuickstart) {
return userContext.apiType === "SQL" ? "/categoryId" : "categoryId";
}
return "";
}
export function isFreeTierAccount(): boolean {
return userContext.databaseAccount?.properties?.enableFreeTier;
}
export function UniqueKeysHeader(): JSX.Element {
const tooltipContent =
"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.";
return (
<Stack horizontal style={{ marginBottom: -2 }}>
<Text className="panelTextBold" variant="small">
Unique keys
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
</TooltipHost>
</Stack>
);
}
export function shouldShowAnalyticalStoreOptions(): boolean {
if (isFabricNative() || configContext.platform === Platform.Emulator) {
return false;
}
switch (userContext.apiType) {
case "SQL":
case "Mongo":
return true;
default:
return false;
}
}
export function AnalyticalStoreHeader(): JSX.Element {
const tooltipContent =
"Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.";
return (
<Stack horizontal style={{ marginBottom: -2 }}>
<Text className="panelTextBold" variant="small">
Analytical Store
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
</TooltipHost>
</Stack>
);
}
export function AnalyticalStorageContent(): JSX.Element {
return (
<Text variant="small">
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting
the performance of transactional workloads.{" "}
<Link
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
target="_blank"
href="https://aka.ms/analytical-store-overview"
>
Learn more
</Link>
</Text>
);
}
export function isSynapseLinkEnabled(): boolean {
if (!userContext.databaseAccount) {
return false;
}
const { properties } = userContext.databaseAccount;
if (!properties) {
return false;
}
if (properties.enableAnalyticalStorage) {
return true;
}
return properties.capabilities?.some(
(capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics,
);
}
export function scrollToSection(id: string): void {
document.getElementById(id)?.scrollIntoView();
}
export function ContainerVectorPolicyTooltipContent(): JSX.Element {
return (
<Text variant="small">
Describe any properties in your data that contain vectors, so that they can be made available for similarity
queries.{" "}
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
Learn more
</Link>
</Text>
);
}
export function parseUniqueKeys(uniqueKeys: string[]): DataModels.UniqueKeyPolicy {
if (uniqueKeys?.length === 0) {
return undefined;
}
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = { uniqueKeys: [] };
uniqueKeys.forEach((uniqueKey: string) => {
if (uniqueKey) {
const validPaths: string[] = uniqueKey.split(",")?.filter((path) => path?.length > 0);
const trimmedPaths: string[] = validPaths?.map((path) => path.trim());
if (trimmedPaths?.length > 0) {
if (userContext.apiType === "Mongo") {
trimmedPaths.map((path) => {
const transformedPath = path.split(".").join("/");
if (transformedPath[0] !== "/") {
return "/" + transformedPath;
}
return transformedPath;
});
}
uniqueKeyPolicy.uniqueKeys.push({ paths: trimmedPaths });
}
}
});
return uniqueKeyPolicy;
}
export const SharedDatabaseDefault: DataModels.IndexingPolicy = {
indexingMode: "consistent",
automatic: true,
includedPaths: [],
excludedPaths: [
{
path: "/*",
},
],
};
export const FullTextPolicyDefault: DataModels.FullTextPolicy = {
defaultLanguage: getFullTextLanguageOptions()[0].key as never,
fullTextPaths: [],
};
export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
indexingMode: "consistent",
automatic: true,
includedPaths: [
{
path: "/*",
indexes: [
{
kind: "Range",
dataType: "Number",
precision: -1,
},
{
kind: "Range",
dataType: "String",
precision: -1,
},
],
},
],
excludedPaths: [],
};

View File

@@ -11,6 +11,11 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
>
<Stack
hidden={false}
style={
{
"marginBottom": -2,
}
}
>
<Stack
horizontal={true}
@@ -93,7 +98,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
id="newDatabaseId"
name="newDatabaseId"
onChange={[Function]}
pattern="[^/?#\\\\]*[^/?# \\\\]"
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
placeholder="Type a new database id"
required={true}
size={40}
@@ -138,13 +143,25 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
</StyledTooltipHostBase>
</Stack>
</Stack>
</Stack>
<Separator
className="panelSeparator"
style={
{
"marginBottom": -4,
"marginTop": -4,
}
}
/>
</Stack>
<Stack>
<Stack
horizontal={true}
style={
{
"marginBottom": 1,
"marginTop": -5,
}
}
>
<span
className="mandatoryStar"
@@ -178,7 +195,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
id="collectionId"
name="collectionId"
onChange={[Function]}
pattern="[^/?#\\\\]*[^/?# \\\\]"
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
placeholder="e.g., Container1"
required={true}
size={40}
@@ -187,9 +204,24 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
value=""
/>
</Stack>
<Separator
className="panelSeparator"
style={
{
"marginBottom": -5,
"marginTop": -5,
}
}
/>
<Stack>
<Stack
horizontal={true}
style={
{
"marginBottom": -4,
"marginTop": -5,
}
}
>
<span
className="mandatoryStar"
@@ -254,6 +286,15 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
Add hierarchical partition key
</CustomizedDefaultButton>
</Stack>
<Separator
className="panelSeparator"
style={
{
"marginBottom": -4,
"marginTop": 2,
}
}
/>
</Stack>
<ThroughputInput
isDatabase={false}
@@ -263,9 +304,21 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
setIsThroughputCapExceeded={[Function]}
setThroughputValue={[Function]}
/>
<Stack>
<Stack
style={
{
"marginBottom": -4,
"marginTop": -2,
}
}
>
<Stack
horizontal={true}
style={
{
"marginBottom": -2,
}
}
>
<Text
className="panelTextBold"
@@ -306,26 +359,53 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
Add unique key
</CustomizedActionButton>
</Stack>
<Separator
className="panelSeparator"
style={
{
"marginBottom": -4,
"marginTop": -15,
}
}
/>
<Stack
className="panelGroupSpacing"
style={
{
"marginTop": -4,
}
}
>
<Text
className="panelTextBold"
variant="small"
>
<Stack
horizontal={true}
style={
{
"marginBottom": -2,
}
}
>
<Text
className="panelTextBold"
variant="small"
>
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.
<StyledLinkBase
aria-label="Learn more about analytical store."
href="https://aka.ms/analytical-store-overview"
target="_blank"
>
Learn more
</StyledLinkBase>
Analytical Store
</Text>
<StyledTooltipHostBase
content="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
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}
/>
</StyledTooltipHostBase>
</Stack>
</Text>
<Stack
horizontal={true}
@@ -382,7 +462,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
container
. Enable Synapse Link for this Cosmos DB account.
<br />
<StyledLinkBase
aria-label="Learn more about Azure Synapse Link."
className="capacitycalculator-link"
@@ -411,6 +491,44 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
/>
</Stack>
</Stack>
<Stack>
<CollapsibleSectionComponent
isExpandedByDefault={false}
onExpand={[Function]}
title="Container Full Text Search Policy"
>
<Stack
id="collapsibleFullTextPolicySectionContent"
styles={
{
"root": {
"position": "relative",
},
}
}
>
<Stack
styles={
{
"root": {
"paddingLeft": 40,
},
}
}
>
<FullTextPoliciesComponent
fullTextPolicy={
{
"defaultLanguage": "en-US",
"fullTextPaths": [],
}
}
onFullTextPathChange={[Function]}
/>
</Stack>
</Stack>
</CollapsibleSectionComponent>
</Stack>
<CollapsibleSectionComponent
isExpandedByDefault={false}
onExpand={[Function]}

View File

@@ -1,5 +1,6 @@
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
@@ -204,8 +205,8 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
type="text"
aria-required="true"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
size={40}
aria-label={databaseIdLabel}
placeholder={databaseIdPlaceHolder}

View File

@@ -39,7 +39,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
data-lpignore={true}
id="database-id"
onChange={[Function]}
pattern="[^/?#\\\\]*[^/?# \\\\]"
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
placeholder="Type a new database id"
size={40}
styles={

View File

@@ -0,0 +1,28 @@
import { shallow, ShallowWrapper } from "enzyme";
import Explorer from "Explorer/Explorer";
import {
AddGlobalSecondaryIndexPanel,
AddGlobalSecondaryIndexPanelProps,
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
import React, { Component } from "react";
const props: AddGlobalSecondaryIndexPanelProps = {
explorer: new Explorer(),
};
describe("AddGlobalSecondaryIndexPanel", () => {
it("render default panel", () => {
const wrapper: ShallowWrapper<AddGlobalSecondaryIndexPanelProps, object, Component> = shallow(
<AddGlobalSecondaryIndexPanel {...props} />,
);
expect(wrapper).toMatchSnapshot();
});
it("should render form", () => {
const wrapper: ShallowWrapper<AddGlobalSecondaryIndexPanelProps, object, Component> = shallow(
<AddGlobalSecondaryIndexPanel {...props} />,
);
const form = wrapper.find("form").first();
expect(form).toBeDefined();
});
});

View File

@@ -0,0 +1,400 @@
import {
DirectionalHint,
Dropdown,
DropdownMenuItemType,
Icon,
IDropdownOption,
Link,
Separator,
Stack,
Text,
TooltipHost,
} from "@fluentui/react";
import * as Constants from "Common/Constants";
import { createGlobalSecondaryIndex } from "Common/dataAccess/createMaterializedView";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import * as DataModels from "Contracts/DataModels";
import { FullTextIndex, FullTextPolicy, VectorEmbedding, VectorIndex } from "Contracts/DataModels";
import { Collection, Database } from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer";
import {
AllPropertiesIndexed,
FullTextPolicyDefault,
getPartitionKey,
isSynapseLinkEnabled,
scrollToSection,
shouldShowAnalyticalStoreOptions,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import {
chooseSourceContainerStyle,
chooseSourceContainerStyles,
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanelStyles";
import { AdvancedComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AdvancedComponent";
import { AnalyticalStoreComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AnalyticalStoreComponent";
import { FullTextSearchComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/FullTextSearchComponent";
import { PartitionKeyComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/PartitionKeyComponent";
import { ThroughputComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/ThroughputComponent";
import { VectorSearchComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/VectorSearchComponent";
import { PanelFooterComponent } from "Explorer/Panes/PanelFooterComponent";
import { PanelInfoErrorComponent } from "Explorer/Panes/PanelInfoErrorComponent";
import { PanelLoadingScreen } from "Explorer/Panes/PanelLoadingScreen";
import { useDatabases } from "Explorer/useDatabases";
import { useSidePanel } from "hooks/useSidePanel";
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
import { CollectionCreation } from "Shared/Constants";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
export interface AddGlobalSecondaryIndexPanelProps {
explorer: Explorer;
sourceContainer?: Collection;
}
export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanelProps): JSX.Element => {
const { explorer, sourceContainer } = props;
const [sourceContainerOptions, setSourceContainerOptions] = useState<IDropdownOption[]>();
const [selectedSourceContainer, setSelectedSourceContainer] = useState<Collection>(sourceContainer);
const [globalSecondaryIndexId, setGlobalSecondaryIndexId] = useState<string>();
const [definition, setDefinition] = useState<string>();
const [partitionKey, setPartitionKey] = useState<string>(getPartitionKey());
const [subPartitionKeys, setSubPartitionKeys] = useState<string[]>([]);
const [useHashV1, setUseHashV1] = useState<boolean>();
const [enableDedicatedThroughput, setEnabledDedicatedThroughput] = useState<boolean>();
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>();
const [enableAnalyticalStore, setEnableAnalyticalStore] = useState<boolean>();
const [vectorEmbeddingPolicy, setVectorEmbeddingPolicy] = useState<VectorEmbedding[]>([]);
const [vectorIndexingPolicy, setVectorIndexingPolicy] = useState<VectorIndex[]>([]);
const [vectorPolicyValidated, setVectorPolicyValidated] = useState<boolean>(true);
const [fullTextPolicy, setFullTextPolicy] = useState<FullTextPolicy>(FullTextPolicyDefault);
const [fullTextIndexes, setFullTextIndexes] = useState<FullTextIndex[]>([]);
const [fullTextPolicyValidated, setFullTextPolicyValidated] = useState<boolean>(true);
const [errorMessage, setErrorMessage] = useState<string>();
const [showErrorDetails, setShowErrorDetails] = useState<boolean>();
const [isExecuting, setIsExecuting] = useState<boolean>();
const showFullTextSearch: MutableRefObject<boolean> = useRef<boolean>(userContext.apiType === "SQL");
useEffect(() => {
const sourceContainerOptions: IDropdownOption[] = [];
useDatabases.getState().databases.forEach((database: Database) => {
sourceContainerOptions.push({
key: database.rid,
text: database.id(),
itemType: DropdownMenuItemType.Header,
});
database.collections().forEach((collection: Collection) => {
const isGlobalSecondaryIndex: boolean = !!collection.materializedViewDefinition();
sourceContainerOptions.push({
key: collection.rid,
text: collection.id(),
disabled: isGlobalSecondaryIndex,
...(isGlobalSecondaryIndex && {
title: "This is a global secondary index.",
}),
data: collection,
});
});
});
setSourceContainerOptions(sourceContainerOptions);
}, []);
useEffect(() => {
scrollToSection("panelContainer");
}, [errorMessage]);
let globalSecondaryIndexThroughput: number;
let isCostAcknowledged: boolean;
const globalSecondaryIndexThroughputOnChange = (globalSecondaryIndexThroughputValue: number): void => {
globalSecondaryIndexThroughput = globalSecondaryIndexThroughputValue;
};
const isCostAknowledgedOnChange = (isCostAcknowledgedValue: boolean): void => {
isCostAcknowledged = isCostAcknowledgedValue;
};
const isSelectedSourceContainerSharedThroughput = (): boolean => {
if (!selectedSourceContainer) {
return false;
}
return !!selectedSourceContainer.getDatabase().offer();
};
const showCollectionThroughputInput = (): boolean => {
if (isServerlessAccount()) {
return false;
}
if (enableDedicatedThroughput) {
return true;
}
return !!selectedSourceContainer && !isSelectedSourceContainerSharedThroughput();
};
const showVectorSearchParameters = (): boolean => {
return isVectorSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput());
};
const getAnalyticalStorageTtl = (): number => {
if (!isSynapseLinkEnabled()) {
return undefined;
}
if (!shouldShowAnalyticalStoreOptions()) {
return undefined;
}
if (enableAnalyticalStore) {
// TODO: always default to 90 days once the backend hotfix is deployed
return userContext.features.ttl90Days
? Constants.AnalyticalStorageTtl.Days90
: Constants.AnalyticalStorageTtl.Infinite;
}
return Constants.AnalyticalStorageTtl.Disabled;
};
const validateInputs = (): boolean => {
if (!selectedSourceContainer) {
setErrorMessage("Please select a source container");
return false;
}
if (globalSecondaryIndexThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
const errorMessage: string = "Please acknowledge the estimated monthly spend.";
setErrorMessage(errorMessage);
return false;
}
if (showVectorSearchParameters()) {
if (!vectorPolicyValidated) {
setErrorMessage("Please fix errors in container vector policy");
return false;
}
if (!fullTextPolicyValidated) {
setErrorMessage("Please fix errors in container full text search policy");
return false;
}
}
return true;
};
const submit = async (event?: React.FormEvent<HTMLFormElement>): Promise<void> => {
event?.preventDefault();
if (!validateInputs()) {
return;
}
const globalSecondaryIdTrimmed: string = globalSecondaryIndexId.trim();
const globalSecondaryIndexDefinition: DataModels.MaterializedViewDefinition = {
sourceCollectionId: selectedSourceContainer.id(),
definition: definition,
};
const partitionKeyTrimmed: string = partitionKey.trim();
const partitionKeyVersion = useHashV1 ? undefined : 2;
const partitionKeyPaths: DataModels.PartitionKey = partitionKeyTrimmed
? {
paths: [
partitionKeyTrimmed,
...(userContext.apiType === "SQL" && subPartitionKeys.length > 0 ? subPartitionKeys : []),
],
kind: userContext.apiType === "SQL" && subPartitionKeys.length > 0 ? "MultiHash" : "Hash",
version: partitionKeyVersion,
}
: undefined;
const indexingPolicy: DataModels.IndexingPolicy = AllPropertiesIndexed;
let vectorEmbeddingPolicyFinal: DataModels.VectorEmbeddingPolicy;
if (showVectorSearchParameters()) {
indexingPolicy.vectorIndexes = vectorIndexingPolicy;
vectorEmbeddingPolicyFinal = {
vectorEmbeddings: vectorEmbeddingPolicy,
};
}
if (showFullTextSearch) {
indexingPolicy.fullTextIndexes = fullTextIndexes;
}
const telemetryData: TelemetryProcessor.TelemetryData = {
database: {
id: selectedSourceContainer.databaseId,
shared: isSelectedSourceContainerSharedThroughput(),
},
collection: {
id: globalSecondaryIdTrimmed,
throughput: globalSecondaryIndexThroughput,
isAutoscale: true,
partitionKeyPaths,
collectionWithDedicatedThroughput: enableDedicatedThroughput,
},
subscriptionQuotaId: userContext.quotaId,
dataExplorerArea: Constants.Areas.ContextualPane,
};
const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, telemetryData);
const databaseLevelThroughput: boolean = isSelectedSourceContainerSharedThroughput() && !enableDedicatedThroughput;
const createGlobalSecondaryIndexParams: DataModels.CreateMaterializedViewsParams = {
materializedViewId: globalSecondaryIdTrimmed,
materializedViewDefinition: globalSecondaryIndexDefinition,
databaseId: selectedSourceContainer.databaseId,
databaseLevelThroughput: databaseLevelThroughput,
...(!databaseLevelThroughput && {
autoPilotMaxThroughput: globalSecondaryIndexThroughput,
}),
analyticalStorageTtl: getAnalyticalStorageTtl(),
indexingPolicy: indexingPolicy,
partitionKey: partitionKeyPaths,
vectorEmbeddingPolicy: vectorEmbeddingPolicyFinal,
fullTextPolicy: fullTextPolicy,
};
setIsExecuting(true);
try {
await createGlobalSecondaryIndex(createGlobalSecondaryIndexParams);
await explorer.refreshAllDatabases();
TelemetryProcessor.traceSuccess(Action.CreateGlobalSecondaryIndex, telemetryData, startKey);
useSidePanel.getState().closeSidePanel();
} catch (error) {
const errorMessage: string = getErrorMessage(error);
setErrorMessage(errorMessage);
setShowErrorDetails(true);
const failureTelemetryData = { ...telemetryData, error: errorMessage, errorStack: getErrorStack(error) };
TelemetryProcessor.traceFailure(Action.CreateGlobalSecondaryIndex, failureTelemetryData, startKey);
} finally {
setIsExecuting(false);
}
};
return (
<form className="panelFormWrapper" id="panelGlobalSecondaryIndex" onSubmit={submit}>
{errorMessage && (
<PanelInfoErrorComponent message={errorMessage} messageType="error" showErrorDetails={showErrorDetails} />
)}
<div className="panelMainContent">
<Stack>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Source container id
</Text>
</Stack>
<Dropdown
placeholder="Choose source container"
options={sourceContainerOptions}
defaultSelectedKey={selectedSourceContainer?.rid}
styles={chooseSourceContainerStyles()}
style={chooseSourceContainerStyle()}
onChange={(_, options: IDropdownOption) => setSelectedSourceContainer(options.data as Collection)}
/>
<Separator className="panelSeparator" />
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Global secondary index container id
</Text>
</Stack>
<input
id="globalSecondaryIndexId"
type="text"
aria-required
required
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={`e.g., indexbyEmailId`}
size={40}
className="panelTextField"
value={globalSecondaryIndexId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setGlobalSecondaryIndexId(event.target.value)}
/>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Global secondary index definition
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={
<Link
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
target="blank"
>
Learn more about defining global secondary indexes.
</Link>
}
>
<Icon role="button" iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost>
</Stack>
<input
id="globalSecondaryIndexDefinition"
type="text"
aria-required
required
autoComplete="off"
placeholder={"SELECT c.email, c.accountId FROM c"}
size={40}
className="panelTextField"
value={definition || ""}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setDefinition(event.target.value)}
/>
<PartitionKeyComponent
{...{ partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 }}
/>
<ThroughputComponent
{...{
enableDedicatedThroughput,
setEnabledDedicatedThroughput,
isSelectedSourceContainerSharedThroughput,
showCollectionThroughputInput,
globalSecondaryIndexThroughputOnChange,
setIsThroughputCapExceeded,
isCostAknowledgedOnChange,
}}
/>
{shouldShowAnalyticalStoreOptions() && (
<AnalyticalStoreComponent {...{ explorer, enableAnalyticalStore, setEnableAnalyticalStore }} />
)}
{showVectorSearchParameters() && (
<VectorSearchComponent
{...{
vectorEmbeddingPolicy,
setVectorEmbeddingPolicy,
vectorIndexingPolicy,
setVectorIndexingPolicy,
vectorPolicyValidated,
setVectorPolicyValidated,
isGlobalSecondaryIndex: true,
}}
/>
)}
{showFullTextSearch && (
<FullTextSearchComponent
{...{ fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated }}
/>
)}
<AdvancedComponent {...{ useHashV1, setUseHashV1, setSubPartitionKeys }} />
</Stack>
</div>
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={isThroughputCapExceeded} />
{isExecuting && <PanelLoadingScreen />}
</form>
);
};

View File

@@ -0,0 +1,15 @@
import { IDropdownStyleProps, IDropdownStyles, IStyleFunctionOrObject } from "@fluentui/react";
import { CSSProperties } from "react";
export function chooseSourceContainerStyles(): IStyleFunctionOrObject<IDropdownStyleProps, IDropdownStyles> {
return {
title: { height: 27, lineHeight: 27 },
dropdownItem: { fontSize: 12 },
dropdownItemDisabled: { fontSize: 12 },
dropdownItemSelected: { fontSize: 12 },
};
}
export function chooseSourceContainerStyle(): CSSProperties {
return { width: 300, fontSize: 12 };
}

View File

@@ -0,0 +1,54 @@
import { Checkbox, Icon, Link, Stack, Text } from "@fluentui/react";
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
export interface AdvancedComponentProps {
useHashV1: boolean;
setUseHashV1: React.Dispatch<React.SetStateAction<boolean>>;
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
}
export const AdvancedComponent = (props: AdvancedComponentProps): JSX.Element => {
const { useHashV1, setUseHashV1, setSubPartitionKeys } = props;
const useHashV1CheckboxOnChange = (isChecked: boolean): void => {
setUseHashV1(isChecked);
setSubPartitionKeys([]);
};
return (
<CollapsibleSectionComponent
title="Advanced"
isExpandedByDefault={false}
onExpand={() => {
TelemetryProcessor.traceOpen(Action.ExpandAddGlobalSecondaryIndexPaneAdvancedSection);
scrollToSection("collapsibleAdvancedSectionContent");
}}
>
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
<Checkbox
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
checked={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) => {
useHashV1CheckboxOnChange(isChecked);
}}
/>
<Text variant="small">
<Icon iconName="InfoSolid" className="removeIcon" /> 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>
</CollapsibleSectionComponent>
);
};

View File

@@ -0,0 +1,99 @@
import { DefaultButton, Link, Stack, Text } from "@fluentui/react";
import * as Constants from "Common/Constants";
import Explorer from "Explorer/Explorer";
import {
AnalyticalStorageContent,
isSynapseLinkEnabled,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
import { getCollectionName } from "Utils/APITypeUtils";
export interface AnalyticalStoreComponentProps {
explorer: Explorer;
enableAnalyticalStore: boolean;
setEnableAnalyticalStore: React.Dispatch<React.SetStateAction<boolean>>;
}
export const AnalyticalStoreComponent = (props: AnalyticalStoreComponentProps): JSX.Element => {
const { explorer, enableAnalyticalStore, setEnableAnalyticalStore } = props;
const onEnableAnalyticalStoreRadioButtonChange = (checked: boolean): void => {
if (checked && !enableAnalyticalStore) {
setEnableAnalyticalStore(true);
}
};
const onDisableAnalyticalStoreRadioButtonnChange = (checked: boolean): void => {
if (checked && enableAnalyticalStore) {
setEnableAnalyticalStore(false);
}
};
return (
<Stack className="panelGroupSpacing">
<Text className="panelTextBold" variant="small">
{AnalyticalStorageContent()}
</Text>
<Stack horizontal verticalAlign="center">
<div role="radiogroup">
<input
className="panelRadioBtn"
checked={enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
aria-label="Enable analytical store"
aria-checked={enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="enableAnalyticalStoreBtn"
tabIndex={0}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
onEnableAnalyticalStoreRadioButtonChange(event.target.checked);
}}
/>
<span className="panelRadioBtnLabel">On</span>
<input
className="panelRadioBtn"
checked={!enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
aria-label="Disable analytical store"
aria-checked={!enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="disableAnalyticalStoreBtn"
tabIndex={0}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
onDisableAnalyticalStoreRadioButtonnChange(event.target.checked);
}}
/>
<span className="panelRadioBtnLabel">Off</span>
</div>
</Stack>
{!isSynapseLinkEnabled() && (
<Stack className="panelGroupSpacing">
<Text variant="small">
Azure Synapse Link is required for creating an analytical store {getCollectionName().toLocaleLowerCase()}.
Enable Synapse Link for this Cosmos DB account.{" "}
<Link
href="https://aka.ms/cosmosdb-synapselink"
target="_blank"
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
className="capacitycalculator-link"
>
Learn more
</Link>
</Text>
<DefaultButton
text="Enable"
onClick={() => explorer.openEnableSynapseLinkDialog()}
style={{ height: 27, width: 80 }}
styles={{ label: { fontSize: 12 } }}
/>
</Stack>
)}
</Stack>
);
};

View File

@@ -0,0 +1,45 @@
import { Stack } from "@fluentui/react";
import { FullTextIndex, FullTextPolicy } from "Contracts/DataModels";
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
export interface FullTextSearchComponentProps {
fullTextPolicy: FullTextPolicy;
setFullTextPolicy: React.Dispatch<React.SetStateAction<FullTextPolicy>>;
setFullTextIndexes: React.Dispatch<React.SetStateAction<FullTextIndex[]>>;
setFullTextPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
}
export const FullTextSearchComponent = (props: FullTextSearchComponentProps): JSX.Element => {
const { fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated } = props;
return (
<Stack>
<CollapsibleSectionComponent
title="Container Full Text Search Policy"
isExpandedByDefault={false}
onExpand={() => {
scrollToSection("collapsibleFullTextPolicySectionContent");
}}
>
<Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}>
<FullTextPoliciesComponent
fullTextPolicy={fullTextPolicy}
onFullTextPathChange={(
fullTextPolicy: FullTextPolicy,
fullTextIndexes: FullTextIndex[],
fullTextPolicyValidated: boolean,
) => {
setFullTextPolicy(fullTextPolicy);
setFullTextIndexes(fullTextIndexes);
setFullTextPolicyValidated(fullTextPolicyValidated);
}}
/>
</Stack>
</Stack>
</CollapsibleSectionComponent>
</Stack>
);
};

View File

@@ -0,0 +1,132 @@
import { DefaultButton, DirectionalHint, Icon, IconButton, Link, Stack, Text, TooltipHost } from "@fluentui/react";
import * as Constants from "Common/Constants";
import {
getPartitionKeyName,
getPartitionKeyPlaceHolder,
getPartitionKeyTooltipText,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
export interface PartitionKeyComponentProps {
partitionKey?: string;
setPartitionKey: React.Dispatch<React.SetStateAction<string>>;
subPartitionKeys: string[];
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
useHashV1: boolean;
}
export const PartitionKeyComponent = (props: PartitionKeyComponentProps): JSX.Element => {
const { partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 } = props;
const partitionKeyValueOnChange = (value: string): void => {
if (!partitionKey && !value.startsWith("/")) {
setPartitionKey("/" + value);
} else {
setPartitionKey(value);
}
};
const subPartitionKeysValueOnChange = (value: string, index: number): void => {
const updatedSubPartitionKeys: string[] = [...subPartitionKeys];
if (!updatedSubPartitionKeys[index] && !value.startsWith("/")) {
updatedSubPartitionKeys[index] = "/" + value.trim();
} else {
updatedSubPartitionKeys[index] = value.trim();
}
setSubPartitionKeys(updatedSubPartitionKeys);
};
return (
<Stack>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Partition key
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost>
</Stack>
<input
type="text"
id="addGlobalSecondaryIndex-partitionKeyValue"
aria-required
required
size={40}
className="panelTextField"
placeholder={getPartitionKeyPlaceHolder()}
aria-label={getPartitionKeyName()}
pattern=".*"
value={partitionKey}
style={{ marginBottom: 8 }}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
partitionKeyValueOnChange(event.target.value);
}}
/>
{subPartitionKeys.map((subPartitionKey: string, subPartitionKeyIndex: number) => {
return (
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${subPartitionKeyIndex}`} horizontal>
<div
style={{
width: "20px",
border: "solid",
borderWidth: "0px 0px 1px 1px",
marginRight: "5px",
}}
></div>
<input
type="text"
id="addGlobalSecondaryIndex-partitionKeyValue"
key={`addGlobalSecondaryIndex-partitionKeyValue_${subPartitionKeyIndex}`}
aria-required
required
size={40}
tabIndex={subPartitionKeyIndex > 0 ? 1 : 0}
className="panelTextField"
autoComplete="off"
placeholder={getPartitionKeyPlaceHolder(subPartitionKeyIndex)}
aria-label={getPartitionKeyName()}
pattern={".*"}
title={""}
value={subPartitionKey}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
subPartitionKeysValueOnChange(event.target.value, subPartitionKeyIndex);
}}
/>
<IconButton
iconProps={{ iconName: "Delete" }}
style={{ height: 27 }}
onClick={() => {
const updatedSubPartitionKeys = subPartitionKeys.filter(
(_, subPartitionKeyIndexToRemove) => subPartitionKeyIndex !== subPartitionKeyIndexToRemove,
);
setSubPartitionKeys(updatedSubPartitionKeys);
}}
/>
</Stack>
);
})}
<Stack className="panelGroupSpacing">
<DefaultButton
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
hidden={useHashV1}
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
>
Add hierarchical partition key
</DefaultButton>
{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>
);
};

View File

@@ -0,0 +1,68 @@
import { Checkbox, Stack } from "@fluentui/react";
import { ThroughputInput } from "Explorer/Controls/ThroughputInput/ThroughputInput";
import { isFreeTierAccount } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import { useDatabases } from "Explorer/useDatabases";
import React from "react";
import { getCollectionName } from "Utils/APITypeUtils";
import { isServerlessAccount } from "Utils/CapabilityUtils";
export interface ThroughputComponentProps {
enableDedicatedThroughput: boolean;
setEnabledDedicatedThroughput: React.Dispatch<React.SetStateAction<boolean>>;
isSelectedSourceContainerSharedThroughput: () => boolean;
showCollectionThroughputInput: () => boolean;
globalSecondaryIndexThroughputOnChange: (globalSecondaryIndexThroughputValue: number) => void;
setIsThroughputCapExceeded: React.Dispatch<React.SetStateAction<boolean>>;
isCostAknowledgedOnChange: (isCostAknowledgedValue: boolean) => void;
}
export const ThroughputComponent = (props: ThroughputComponentProps): JSX.Element => {
const {
enableDedicatedThroughput,
setEnabledDedicatedThroughput,
isSelectedSourceContainerSharedThroughput,
showCollectionThroughputInput,
globalSecondaryIndexThroughputOnChange,
setIsThroughputCapExceeded,
isCostAknowledgedOnChange,
} = props;
return (
<Stack>
{!isServerlessAccount() && isSelectedSourceContainerSharedThroughput() && (
<Stack horizontal verticalAlign="center">
<Checkbox
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
checked={enableDedicatedThroughput}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
onChange={(_, isChecked: boolean) => setEnabledDedicatedThroughput(isChecked)}
/>
</Stack>
)}
{showCollectionThroughputInput() && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !useDatabases.getState().isFirstResourceCreated()}
isDatabase={false}
isSharded={true}
isFreeTier={isFreeTierAccount()}
isQuickstart={false}
isGlobalSecondaryIndex={true}
setThroughputValue={(throughput: number) => {
globalSecondaryIndexThroughputOnChange(throughput);
}}
setIsAutoscale={() => {}}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => {
setIsThroughputCapExceeded(isThroughputCapExceeded);
}}
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
isCostAknowledgedOnChange(isAcknowledged);
}}
/>
)}
</Stack>
);
};

View File

@@ -0,0 +1,61 @@
import { Stack } from "@fluentui/react";
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
import {
ContainerVectorPolicyTooltipContent,
scrollToSection,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
export interface VectorSearchComponentProps {
vectorEmbeddingPolicy: VectorEmbedding[];
setVectorEmbeddingPolicy: React.Dispatch<React.SetStateAction<VectorEmbedding[]>>;
vectorIndexingPolicy: VectorIndex[];
setVectorIndexingPolicy: React.Dispatch<React.SetStateAction<VectorIndex[]>>;
setVectorPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
isGlobalSecondaryIndex?: boolean;
}
export const VectorSearchComponent = (props: VectorSearchComponentProps): JSX.Element => {
const {
vectorEmbeddingPolicy,
setVectorEmbeddingPolicy,
vectorIndexingPolicy,
setVectorIndexingPolicy,
setVectorPolicyValidated,
isGlobalSecondaryIndex,
} = props;
return (
<Stack>
<CollapsibleSectionComponent
title="Container Vector Policy"
isExpandedByDefault={false}
onExpand={() => {
scrollToSection("collapsibleVectorPolicySectionContent");
}}
tooltipContent={ContainerVectorPolicyTooltipContent()}
>
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}>
<VectorEmbeddingPoliciesComponent
vectorEmbeddings={vectorEmbeddingPolicy}
vectorIndexes={vectorIndexingPolicy}
onVectorEmbeddingChange={(
vectorEmbeddingPolicy: VectorEmbedding[],
vectorIndexingPolicy: VectorIndex[],
vectorPolicyValidated: boolean,
) => {
setVectorEmbeddingPolicy(vectorEmbeddingPolicy);
setVectorIndexingPolicy(vectorIndexingPolicy);
setVectorPolicyValidated(vectorPolicyValidated);
}}
isGlobalSecondaryIndex={isGlobalSecondaryIndex}
/>
</Stack>
</Stack>
</CollapsibleSectionComponent>
</Stack>
);
};

View File

@@ -0,0 +1,196 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
<form
className="panelFormWrapper"
id="panelGlobalSecondaryIndex"
onSubmit={[Function]}
>
<div
className="panelMainContent"
>
<Stack>
<Stack
horizontal={true}
>
<span
className="mandatoryStar"
>
* 
</span>
<Text
className="panelTextBold"
variant="small"
>
Source container id
</Text>
</Stack>
<Dropdown
onChange={[Function]}
placeholder="Choose source container"
style={
{
"fontSize": 12,
"width": 300,
}
}
styles={
{
"dropdownItem": {
"fontSize": 12,
},
"dropdownItemDisabled": {
"fontSize": 12,
},
"dropdownItemSelected": {
"fontSize": 12,
},
"title": {
"height": 27,
"lineHeight": 27,
},
}
}
/>
<Separator
className="panelSeparator"
/>
<Stack
horizontal={true}
>
<span
className="mandatoryStar"
>
* 
</span>
<Text
className="panelTextBold"
variant="small"
>
Global secondary index container id
</Text>
</Stack>
<input
aria-required={true}
autoComplete="off"
className="panelTextField"
id="globalSecondaryIndexId"
onChange={[Function]}
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
placeholder="e.g., indexbyEmailId"
required={true}
size={40}
title="May not end with space nor contain characters '\\' '/' '#' '?'"
type="text"
/>
<Stack
horizontal={true}
>
<span
className="mandatoryStar"
>
* 
</span>
<Text
className="panelTextBold"
variant="small"
>
Global secondary index definition
</Text>
<StyledTooltipHostBase
content={
<StyledLinkBase
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
target="blank"
>
Learn more about defining global secondary indexes.
</StyledLinkBase>
}
directionalHint={4}
>
<Icon
className="panelInfoIcon"
iconName="Info"
role="button"
tabIndex={0}
/>
</StyledTooltipHostBase>
</Stack>
<input
aria-required={true}
autoComplete="off"
className="panelTextField"
id="globalSecondaryIndexDefinition"
onChange={[Function]}
placeholder="SELECT c.email, c.accountId FROM c"
required={true}
size={40}
type="text"
value=""
/>
<PartitionKeyComponent
partitionKey=""
setPartitionKey={[Function]}
setSubPartitionKeys={[Function]}
subPartitionKeys={[]}
/>
<ThroughputComponent
globalSecondaryIndexThroughputOnChange={[Function]}
isCostAknowledgedOnChange={[Function]}
isSelectedSourceContainerSharedThroughput={[Function]}
setEnabledDedicatedThroughput={[Function]}
setIsThroughputCapExceeded={[Function]}
showCollectionThroughputInput={[Function]}
/>
<AnalyticalStoreComponent
explorer={
Explorer {
"_isInitializingNotebooks": false,
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
}
}
setEnableAnalyticalStore={[Function]}
/>
<FullTextSearchComponent
fullTextPolicy={
{
"defaultLanguage": "en-US",
"fullTextPaths": [],
}
}
setFullTextIndexes={[Function]}
setFullTextPolicy={[Function]}
setFullTextPolicyValidated={[Function]}
/>
<AdvancedComponent
setSubPartitionKeys={[Function]}
setUseHashV1={[Function]}
/>
</Stack>
</div>
<PanelFooterComponent
buttonLabel="OK"
/>
</form>
`;

View File

@@ -7,6 +7,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { isServerlessAccount } from "Utils/CapabilityUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
@@ -202,8 +203,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
required={true}
autoComplete="off"
styles={getTextFieldStyles()}
pattern="[^/?#\\-]*[^/?#- \\]"
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder="Type a new keyspace id"
size={40}
value={newKeyspaceId}
@@ -292,8 +293,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
required={true}
ariaLabel="addCollection-table Id Create table"
autoComplete="off"
pattern="[^/?#\\-]*[^/?#- \\]"
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder="Enter table Id"
size={20}
value={tableId}

View File

@@ -28,6 +28,7 @@ import { RightPaneForm } from "Explorer/Panes/RightPaneForm/RightPaneForm";
import { useDatabases } from "Explorer/useDatabases";
import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import { useSidePanel } from "hooks/useSidePanel";
import * as React from "react";
@@ -235,8 +236,8 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
aria-required
required
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={`e.g., ${getCollectionName()}1`}
size={40}
className="panelTextField"

View File

@@ -6,7 +6,9 @@ import {
Checkbox,
ChoiceGroup,
DefaultButton,
Dropdown,
IChoiceGroupOption,
IDropdownOption,
ISpinButtonStyles,
IToggleStyles,
Position,
@@ -21,7 +23,15 @@ import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { Platform, configContext } from "ConfigContext";
import { useDialog } from "Explorer/Controls/Dialog";
import { useDatabases } from "Explorer/useDatabases";
import { deleteAllStates } from "Shared/AppStatePersistenceUtility";
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
import {
AppStateComponentNames,
deleteAllStates,
deleteState,
hasState,
loadState,
saveState,
} from "Shared/AppStatePersistenceUtility";
import {
DefaultRUThreshold,
LocalStorageUtility,
@@ -37,6 +47,7 @@ import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react";
@@ -143,6 +154,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
? LocalStorageUtility.getEntryString(StorageKey.IsGraphAutoVizDisabled)
: "false",
);
const [selectedRegionalEndpoint, setSelectedRegionalEndpoint] = useState<string>(
hasState({
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
globalAccountName: userContext.databaseAccount?.name,
})
? (loadState({
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
globalAccountName: userContext.databaseAccount?.name,
}) as string)
: undefined,
);
const [retryAttempts, setRetryAttempts] = useState<number>(
LocalStorageUtility.hasItem(StorageKey.RetryAttempts)
? LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts)
@@ -158,6 +180,11 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
? LocalStorageUtility.getEntryNumber(StorageKey.MaxWaitTimeInSeconds)
: Constants.Queries.DefaultMaxWaitTimeInSeconds,
);
const [queryControlEnabled, setQueryControlEnabled] = useState<boolean>(
LocalStorageUtility.hasItem(StorageKey.QueryControlEnabled)
? LocalStorageUtility.getEntryString(StorageKey.QueryControlEnabled) === "true"
: false,
);
const [maxDegreeOfParallelism, setMaxDegreeOfParallelism] = useState<number>(
LocalStorageUtility.hasItem(StorageKey.MaxDegreeOfParellism)
? LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism)
@@ -182,6 +209,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
!isEmulator;
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin" && !isEmulator;
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator;
const shouldShowEnhancedQueryControl = userContext.apiType === "SQL";
const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator;
const showEnableEntraIdRbac =
isDataplaneRbacSupported(userContext.apiType) &&
@@ -189,6 +217,44 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
configContext.platform !== Platform.Fabric &&
!isEmulator;
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled() && !isEmulator;
const uniqueAccountRegions = new Set<string>();
const regionOptions: IDropdownOption[] = [];
regionOptions.push({
key: userContext?.databaseAccount?.properties?.documentEndpoint,
text: `Global (Default)`,
data: {
isGlobal: true,
writeEnabled: true,
},
});
userContext?.databaseAccount?.properties?.writeLocations?.forEach((loc) => {
if (!uniqueAccountRegions.has(loc.locationName)) {
uniqueAccountRegions.add(loc.locationName);
regionOptions.push({
key: loc.documentEndpoint,
text: `${loc.locationName} (Read/Write)`,
data: {
isGlobal: false,
writeEnabled: true,
},
});
}
});
userContext?.databaseAccount?.properties?.readLocations?.forEach((loc) => {
if (!uniqueAccountRegions.has(loc.locationName)) {
uniqueAccountRegions.add(loc.locationName);
regionOptions.push({
key: loc.documentEndpoint,
text: `${loc.locationName} (Read)`,
data: {
isGlobal: false,
writeEnabled: false,
},
});
}
});
const shouldShowCopilotSampleDBOption =
userContext.apiType === "SQL" &&
useQueryCopilot.getState().copilotEnabled &&
@@ -274,6 +340,46 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
}
}
const storedRegionalEndpoint = loadState({
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
globalAccountName: userContext.databaseAccount?.name,
}) as string;
const selectedRegionIsGlobal =
selectedRegionalEndpoint === userContext?.databaseAccount?.properties?.documentEndpoint;
if (selectedRegionIsGlobal && storedRegionalEndpoint) {
deleteState({
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
globalAccountName: userContext.databaseAccount?.name,
});
updateUserContext({
selectedRegionalEndpoint: undefined,
writeEnabledInSelectedRegion: true,
refreshCosmosClient: true,
});
useClientWriteEnabled.setState({ clientWriteEnabled: true });
} else if (
selectedRegionalEndpoint &&
!selectedRegionIsGlobal &&
selectedRegionalEndpoint !== storedRegionalEndpoint
) {
saveState(
{
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
globalAccountName: userContext.databaseAccount?.name,
},
selectedRegionalEndpoint,
);
const validWriteEndpoint = userContext.databaseAccount?.properties?.writeLocations?.find(
(loc) => loc.documentEndpoint === selectedRegionalEndpoint,
);
updateUserContext({
selectedRegionalEndpoint: selectedRegionalEndpoint,
writeEnabledInSelectedRegion: !!validWriteEndpoint,
refreshCosmosClient: true,
});
useClientWriteEnabled.setState({ clientWriteEnabled: !!validWriteEndpoint });
}
LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled);
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts);
@@ -281,6 +387,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
LocalStorageUtility.setEntryNumber(StorageKey.MaxWaitTimeInSeconds, MaxWaitTimeInSeconds);
LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString());
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
LocalStorageUtility.setEntryString(StorageKey.QueryControlEnabled, queryControlEnabled.toString());
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString());
LocalStorageUtility.setEntryString(StorageKey.CopilotSampleDBEnabled, copilotSampleDBEnabled.toString());
@@ -310,6 +417,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`,
);
logConsoleInfo(`${crossPartitionQueryEnabled ? "Enabled" : "Disabled"} cross-partition query feed option`);
logConsoleInfo(`${queryControlEnabled ? "Enabled" : "Disabled"} query control option`);
logConsoleInfo(
`Updated the max degree of parallelism query feed option to ${LocalStorageUtility.getEntryNumber(
StorageKey.MaxDegreeOfParellism,
@@ -423,6 +531,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
setDefaultQueryResultsView(option.key as SplitterDirection);
};
const handleOnSelectedRegionOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IDropdownOption): void => {
setSelectedRegionalEndpoint(option.key as string);
};
const handleOnQueryRetryAttemptsSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => {
const retryAttempts = Number(newValue);
if (!isNaN(retryAttempts)) {
@@ -503,7 +615,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
return (
<RightPaneForm {...genericPaneProps}>
<div className={`paneMainContent ${styles.container}`}>
<Accordion className={`customAccordion ${styles.firstItem}`}>
{!isFabricNative() && (
<Accordion className={`customAccordion ${styles.firstItem}`} collapsible>
{shouldShowQueryPageOptions && (
<AccordionItem value="1">
<AccordionHeader>
@@ -561,8 +674,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra ID
RBAC.
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
ID RBAC.
<a
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
target="_blank"
@@ -583,17 +696,47 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel>
</AccordionItem>
)}
{userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && !isFabric() && (
<AccordionItem value="3">
<AccordionHeader>
<div className={styles.header}>Region Selection</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
Changes region the Cosmos Client uses to access account.
</div>
<div>
<span className={styles.subHeader}>Select Region</span>
<InfoTooltip className={styles.headerIcon}>
Changes the account endpoint used to perform client operations.
</InfoTooltip>
</div>
<Dropdown
placeholder={
selectedRegionalEndpoint
? regionOptions.find((option) => option.key === selectedRegionalEndpoint)?.text
: regionOptions[0]?.text
}
onChange={handleOnSelectedRegionOptionChange}
options={regionOptions}
styles={{ root: { marginBottom: "10px" } }}
/>
</div>
</AccordionPanel>
</AccordionItem>
)}
{userContext.apiType === "SQL" && !isEmulator && (
<>
<AccordionItem value="3">
<AccordionItem value="4">
<AccordionHeader>
<div className={styles.header}>Query Timeout</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
When a query reaches a specified time limit, a popup with an option to cancel the query will show
unless automatic cancellation has been enabled.
When a query reaches a specified time limit, a popup with an option to cancel the query will
show unless automatic cancellation has been enabled.
</div>
<Toggle
styles={toggleStyles}
@@ -625,8 +768,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
)}
</AccordionPanel>
</AccordionItem>
<AccordionItem value="4">
<AccordionItem value="5">
<AccordionHeader>
<div className={styles.header}>RU Limit</div>
</AccordionHeader>
@@ -660,7 +802,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel>
</AccordionItem>
<AccordionItem value="5">
<AccordionItem value="6">
<AccordionHeader>
<div className={styles.header}>Default Query Results View</div>
</AccordionHeader>
@@ -681,8 +823,9 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem>
</>
)}
{showRetrySettings && (
<AccordionItem value="6">
<AccordionItem value="7">
<AccordionHeader>
<div className={styles.header}>Retry Settings</div>
</AccordionHeader>
@@ -745,8 +888,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
onChange={handleOnMaxWaitTimeSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1"
decrementButtonAriaLabel="Decrease value by 1"
onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)}
onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)}
onIncrement={(newValue) =>
setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)
}
onDecrement={(newValue) =>
setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)
}
onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)}
styles={spinButtonStyles}
/>
@@ -755,7 +902,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem>
)}
{!isEmulator && (
<AccordionItem value="7">
<AccordionItem value="8">
<AccordionHeader>
<div className={styles.header}>Enable container pagination</div>
</AccordionHeader>
@@ -779,15 +926,15 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem>
)}
{shouldShowCrossPartitionOption && (
<AccordionItem value="8">
<AccordionItem value="9">
<AccordionHeader>
<div className={styles.header}>Enable cross-partition query</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
Send more than one request while executing a query. More than one request is necessary if the query
is not scoped to single partition key value.
Send more than one request while executing a query. More than one request is necessary if the
query is not scoped to single partition key value.
</div>
<Checkbox
styles={{
@@ -803,17 +950,49 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel>
</AccordionItem>
)}
{shouldShowEnhancedQueryControl && (
<AccordionItem value="10">
<AccordionHeader>
<div className={styles.header}>Enhanced query control</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
Query up to the max degree of parallelism.
<a
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/performance-tips-query-sdk?tabs=v3&pivots=programming-language-nodejs#enhanced-query-control"
target="_blank"
rel="noopener noreferrer"
>
{" "}
Learn more{" "}
</a>
</div>
<Checkbox
styles={{
label: { padding: 0 },
}}
className="padding"
ariaLabel="EnableQueryControl"
checked={queryControlEnabled}
onChange={() => setQueryControlEnabled(!queryControlEnabled)}
label="Enable query control"
/>
</div>
</AccordionPanel>
</AccordionItem>
)}
{shouldShowParallelismOption && (
<AccordionItem value="9">
<AccordionItem value="10">
<AccordionHeader>
<div className={styles.header}>Max degree of parallelism</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
Gets or sets the number of concurrent operations run client side during parallel query execution. A
positive property value limits the number of concurrent operations to the set value. If it is set to
less than 0, the system automatically decides the number of concurrent operations to run.
Gets or sets the number of concurrent operations run client side during parallel query execution.
A positive property value limits the number of concurrent operations to the set value. If it is
set to less than 0, the system automatically decides the number of concurrent operations to run.
</div>
<SpinButton
min={-1}
@@ -837,7 +1016,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem>
)}
{shouldShowPriorityLevelOption && (
<AccordionItem value="10">
<AccordionItem value="11">
<AccordionHeader>
<div className={styles.header}>Priority Level</div>
</AccordionHeader>
@@ -860,15 +1039,15 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem>
)}
{shouldShowGraphAutoVizOption && (
<AccordionItem value="11">
<AccordionItem value="12">
<AccordionHeader>
<div className={styles.header}>Display Gremlin query results as:&nbsp;</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
Select Graph to automatically visualize the query results as a Graph or JSON to display the results
as JSON.
Select Graph to automatically visualize the query results as a Graph or JSON to display the
results as JSON.
</div>
<ChoiceGroup
selectedKey={graphAutoVizDisabled}
@@ -881,7 +1060,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem>
)}
{shouldShowCopilotSampleDBOption && (
<AccordionItem value="12">
<AccordionItem value="13">
<AccordionHeader>
<div className={styles.header}>Enable sample database</div>
</AccordionHeader>
@@ -907,6 +1086,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem>
)}
</Accordion>
)}
<div className="settingsSection">
<div className="settingsSectionPart">
@@ -916,7 +1096,15 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
"Clear History",
undefined,
"Are you sure you want to proceed?",
() => deleteAllStates(),
() => {
deleteAllStates();
updateUserContext({
selectedRegionalEndpoint: undefined,
writeEnabledInSelectedRegion: true,
refreshCosmosClient: true,
});
useClientWriteEnabled.setState({ clientWriteEnabled: true });
},
"Cancel",
undefined,
<>
@@ -927,6 +1115,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
<li>Reset your customized tab layout, including the splitter positions</li>
<li>Erase your table column preferences, including any custom columns</li>
<li>Clear your filter history</li>
<li>Reset region selection to global</li>
</ul>
</>,
);

View File

@@ -12,6 +12,7 @@ exports[`Settings Pane should render Default properly 1`] = `
>
<Accordion
className="customAccordion ___1uf6361_0000000 fz7g6wx"
collapsible={true}
>
<AccordionItem
value="1"
@@ -107,7 +108,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="3"
value="4"
>
<AccordionHeader>
<div
@@ -148,7 +149,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="4"
value="5"
>
<AccordionHeader>
<div
@@ -219,7 +220,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="5"
value="6"
>
<AccordionHeader>
<div
@@ -281,7 +282,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="6"
value="7"
>
<AccordionHeader>
<div
@@ -423,7 +424,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="7"
value="8"
>
<AccordionHeader>
<div
@@ -459,7 +460,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="8"
value="9"
>
<AccordionHeader>
<div
@@ -495,7 +496,52 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="9"
value="10"
>
<AccordionHeader>
<div
className="___15c001r_0000000 fq02s40"
>
Enhanced query control
</div>
</AccordionHeader>
<AccordionPanel>
<div
className="___1dfa554_0000000 fo7qwa0"
>
<div
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
>
Query up to the max degree of parallelism.
<a
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/performance-tips-query-sdk?tabs=v3&pivots=programming-language-nodejs#enhanced-query-control"
rel="noopener noreferrer"
target="_blank"
>
Learn more
</a>
</div>
<StyledCheckboxBase
ariaLabel="EnableQueryControl"
checked={false}
className="padding"
label="Enable query control"
onChange={[Function]}
styles={
{
"label": {
"padding": 0,
},
}
}
/>
</div>
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="10"
>
<AccordionHeader>
<div
@@ -573,9 +619,10 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
>
<Accordion
className="customAccordion ___1uf6361_0000000 fz7g6wx"
collapsible={true}
>
<AccordionItem
value="6"
value="7"
>
<AccordionHeader>
<div
@@ -717,7 +764,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="7"
value="8"
>
<AccordionHeader>
<div
@@ -753,7 +800,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="11"
value="12"
>
<AccordionHeader>
<div

View File

@@ -356,7 +356,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
value=""
>
<div
className="ms-TextField is-required root-110"
className="ms-TextField is-required root-116"
>
<div
className="ms-TextField-wrapper"
@@ -647,7 +647,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
}
>
<label
className="ms-Label root-121"
className="ms-Label root-127"
htmlFor="TextField0"
id="TextFieldLabel2"
>
@@ -656,13 +656,13 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
</LabelBase>
</StyledLabelBase>
<div
className="ms-TextField-fieldGroup fieldGroup-111"
className="ms-TextField-fieldGroup fieldGroup-117"
>
<input
aria-invalid={false}
aria-labelledby="TextFieldLabel2"
autoFocus={true}
className="ms-TextField-field field-112"
className="ms-TextField-field field-118"
id="TextField0"
name="collectionIdConfirmation"
onBlur={[Function]}
@@ -2464,7 +2464,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
>
<button
aria-label="Create"
className="ms-Button ms-Button--primary root-122"
className="ms-Button ms-Button--primary root-128"
data-is-focusable={true}
data-test="Panel/OkButton"
id="sidePanelOkButton"
@@ -2477,14 +2477,14 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
type="submit"
>
<span
className="ms-Button-flexContainer flexContainer-123"
className="ms-Button-flexContainer flexContainer-129"
data-automationid="splitbuttonprimary"
>
<span
className="ms-Button-textContainer textContainer-124"
className="ms-Button-textContainer textContainer-130"
>
<span
className="ms-Button-label label-126"
className="ms-Button-label label-132"
id="id__5"
key="id__5"
>

View File

@@ -2,9 +2,13 @@ import {
DetailsList,
DetailsListLayoutMode,
DirectionalHint,
FontIcon,
IColumn,
SelectionMode,
TooltipHost,
getTheme,
mergeStyles,
mergeStyleSets,
} from "@fluentui/react";
import { Upload } from "Common/Upload/Upload";
import { UploadDetailsRecord } from "Contracts/ViewModels";
@@ -14,7 +18,41 @@ import { getErrorMessage } from "../../Tables/Utilities";
import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export const UploadItemsPane: FunctionComponent = () => {
const theme = getTheme();
const iconClass = mergeStyles({
verticalAlign: "middle",
maxHeight: "16px",
maxWidth: "16px",
});
const classNames = mergeStyleSets({
fileIconHeaderIcon: {
padding: 0,
fontSize: "16px",
},
fileIconCell: {
textAlign: "center",
selectors: {
"&:before": {
content: ".",
display: "inline-block",
verticalAlign: "middle",
height: "100%",
width: "0px",
visibility: "hidden",
},
},
},
error: [{ color: theme.semanticColors.errorIcon }, iconClass],
accept: [{ color: theme.semanticColors.successIcon }, iconClass],
warning: [{ color: theme.semanticColors.warningIcon }, iconClass],
});
export type UploadItemsPaneProps = {
onUpload?: (data: UploadDetailsRecord[]) => void;
};
export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpload }) => {
const [files, setFiles] = useState<FileList>();
const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]);
const [formError, setFormError] = useState<string>("");
@@ -37,6 +75,8 @@ export const UploadItemsPane: FunctionComponent = () => {
(uploadDetails) => {
setUploadFileData(uploadDetails.data);
setFiles(undefined);
// Emit the upload details to the parent component
onUpload && onUpload(uploadDetails.data);
},
(error: Error) => {
const errorMessage = getErrorMessage(error);
@@ -60,43 +100,93 @@ export const UploadItemsPane: FunctionComponent = () => {
};
const columns: IColumn[] = [
{
key: "icons",
name: "",
fieldName: "",
className: classNames.fileIconCell,
iconClassName: classNames.fileIconHeaderIcon,
isIconOnly: true,
minWidth: 16,
maxWidth: 16,
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
if (item.numFailed) {
const errorList = (
<ul
aria-label={"error list"}
style={{
margin: "5px 0",
paddingLeft: "20px",
listStyleType: "disc", // Explicitly set to use bullets (dots)
}}
>
{item.errors.map((error, i) => (
<li key={i} style={{ display: "list-item" }}>
{error}
</li>
))}
</ul>
);
return (
<TooltipHost
content={errorList}
id={`tooltip-${index}-${column.key}`}
directionalHint={DirectionalHint.bottomAutoEdge}
>
<FontIcon iconName="Error" className={classNames.error} aria-label="error" />
</TooltipHost>
);
} else if (item.numThrottled) {
return <FontIcon iconName="Warning" className={classNames.warning} aria-label="warning" />;
} else {
return <FontIcon iconName="Accept" className={classNames.accept} aria-label="accept" />;
}
},
},
{
key: "fileName",
name: "FILE NAME",
fieldName: "fileName",
minWidth: 140,
minWidth: 120,
maxWidth: 140,
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
const fieldContent = item.fileName;
return (
<TooltipHost
content={fieldContent}
id={`tooltip-${index}-${column.key}`}
directionalHint={DirectionalHint.bottomAutoEdge}
>
{fieldContent}
</TooltipHost>
);
},
},
{
key: "status",
name: "STATUS",
fieldName: "numSucceeded",
minWidth: 140,
minWidth: 120,
maxWidth: 140,
isRowHeader: true,
isResizable: true,
data: "string",
isPadded: true,
},
];
const _renderItemColumn = (item: UploadDetailsRecord, index: number, column: IColumn) => {
let fieldContent: string;
const tooltipId = `tooltip-${index}-${column.key}`;
switch (column.key) {
case "status":
fieldContent = `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
break;
default:
fieldContent = item.fileName;
}
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
const fieldContent = `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
return (
<TooltipHost content={fieldContent} id={tooltipId} directionalHint={DirectionalHint.rightCenter}>
<TooltipHost
content={fieldContent}
id={`tooltip-${index}-${column.key}`}
directionalHint={DirectionalHint.bottomAutoEdge}
>
{fieldContent}
</TooltipHost>
);
};
},
},
];
return (
<RightPaneForm {...props}>
@@ -115,7 +205,6 @@ export const UploadItemsPane: FunctionComponent = () => {
<DetailsList
items={uploadFileData}
columns={columns}
onRenderItemColumn={_renderItemColumn}
selectionMode={SelectionMode.none}
layoutMode={DetailsListLayoutMode.justified}
isHeaderVisible={true}

View File

@@ -18,7 +18,7 @@ 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 { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import { PromptCard } from "Explorer/QueryCopilot/PromptCard";
import { useDatabases } from "Explorer/useDatabases";
import { useCarousel } from "hooks/useCarousel";

View File

@@ -22,12 +22,17 @@ export const DeletePopup = ({
};
return (
<Modal isOpen={showDeletePopup} styles={{ main: { minHeight: "122px", minWidth: "880px" } }}>
<Modal
isOpen={showDeletePopup}
styles={{ main: { minHeight: "122px", minWidth: "880px" } }}
titleAriaId="deleteDialogTitle"
subtitleAriaId="deleteDialogSubTitle"
>
<Stack style={{ padding: "16px 24px", height: "auto" }}>
<Text style={{ height: 24, fontSize: "18px" }}>
<Text id="deleteDialogTitle" style={{ height: 24, fontSize: "18px" }}>
<b>Delete code?</b>
</Text>
<Text style={{ marginTop: 10, marginBottom: 20 }}>
<Text id="deleteDialogSubTitle" style={{ marginTop: 10, marginBottom: 20 }}>
This will clear the query from the query builder pane along with all comments and also reset the prompt pane
</Text>
<Stack horizontal tokens={{ childrenGap: 10 }} horizontalAlign="start">

View File

@@ -11,6 +11,8 @@ exports[`Delete Popup snapshot test should not render when showDeletePopup is fa
},
}
}
subtitleAriaId="deleteDialogSubTitle"
titleAriaId="deleteDialogTitle"
>
<Stack
style={
@@ -21,6 +23,7 @@ exports[`Delete Popup snapshot test should not render when showDeletePopup is fa
}
>
<Text
id="deleteDialogTitle"
style={
{
"fontSize": "18px",
@@ -33,6 +36,7 @@ exports[`Delete Popup snapshot test should not render when showDeletePopup is fa
</b>
</Text>
<Text
id="deleteDialogSubTitle"
style={
{
"marginBottom": 20,
@@ -89,6 +93,8 @@ exports[`Delete Popup snapshot test should render when showDeletePopup is true 1
},
}
}
subtitleAriaId="deleteDialogSubTitle"
titleAriaId="deleteDialogTitle"
>
<Stack
style={
@@ -99,6 +105,7 @@ exports[`Delete Popup snapshot test should render when showDeletePopup is true 1
}
>
<Text
id="deleteDialogTitle"
style={
{
"fontSize": "18px",
@@ -111,6 +118,7 @@ exports[`Delete Popup snapshot test should render when showDeletePopup is true 1
</b>
</Text>
<Text
id="deleteDialogSubTitle"
style={
{
"marginBottom": 20,

View File

@@ -13,19 +13,27 @@ import {
SplitButton,
} from "@fluentui/react-components";
import { Add16Regular, ArrowSync12Regular, ChevronLeft12Regular, ChevronRight12Regular } from "@fluentui/react-icons";
import { GlobalSecondaryIndexLabels } from "Common/Constants";
import { isGlobalSecondaryIndexEnabled } from "Common/DatabaseAccountUtility";
import { configContext, Platform } from "ConfigContext";
import Explorer from "Explorer/Explorer";
import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel";
import {
AddGlobalSecondaryIndexPanel,
AddGlobalSecondaryIndexPanelProps,
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
import { Tabs } from "Explorer/Tabs/Tabs";
import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
import { ResourceTree } from "Explorer/Tree/ResourceTree";
import { useDatabases } from "Explorer/useDatabases";
import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboardActionGroup } from "KeyboardShortcuts";
import { isFabric, isFabricMirrored, isFabricNative } from "Platform/Fabric/FabricUtil";
import { isFabric, isFabricMirrored, isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext";
import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
import { conditionalClass } from "Utils/StyleUtils";
import { Allotment, AllotmentHandle } from "allotment";
import { useSidePanel } from "hooks/useSidePanel";
import useZoomLevel from "hooks/useZoomLevel";
import { debounce } from "lodash";
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
@@ -98,6 +106,23 @@ const useSidebarStyles = makeStyles({
display: "flex",
},
},
accessibleContent: {
"@media (max-width: 420px)": {
overflow: "scroll",
},
},
minHeightResponsive: {
"@media (max-width: 420px)": {
minHeight: "400px",
},
},
accessibleContentZoom: {
overflow: "scroll",
},
minHeightZoom: {
minHeight: "400px",
},
});
interface GlobalCommandsProps {
@@ -162,6 +187,25 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
});
}
if (isGlobalSecondaryIndexEnabled()) {
const addMaterializedViewPanelProps: AddGlobalSecondaryIndexPanelProps = {
explorer,
};
actions.push({
id: "new_materialized_view",
label: GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
icon: <Add16Regular />,
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
<AddGlobalSecondaryIndexPanel {...addMaterializedViewPanelProps} />,
),
});
}
return actions;
}, [explorer]);
@@ -250,6 +294,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
const [expandedSize, setExpandedSize] = React.useState(300);
const hasSidebar = userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo";
const allotment = useRef<AllotmentHandle>(null);
const isZoomed = useZoomLevel();
const expand = useCallback(() => {
if (!expanded) {
@@ -293,17 +338,30 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
const hasGlobalCommands = !(
isFabricMirrored() ||
isFabricNativeReadOnly() ||
userContext.apiType === "Postgres" ||
userContext.apiType === "VCoreMongo"
);
return (
<div className="sidebarContainer">
<Allotment ref={allotment} onChange={onChange} onDragEnd={onDragEnd} className="resourceTreeAndTabs">
<Allotment
ref={allotment}
onChange={onChange}
onDragEnd={onDragEnd}
className={`resourceTreeAndTabs ${styles.accessibleContent} ${conditionalClass(
isZoomed,
styles.accessibleContentZoom,
)}`}
>
{/* Collections Tree - Start */}
{hasSidebar && (
// When collapsed, we force the pane to 24 pixels wide and make it non-resizable.
<Allotment.Pane minSize={24} preferredSize={250}>
<Allotment.Pane
className={`${styles.minHeightResponsive} ${conditionalClass(isZoomed, styles.minHeightZoom)}`}
minSize={24}
preferredSize={250}
>
<CosmosFluentProvider className={mergeClasses(styles.sidebar)}>
<div className={styles.sidebarContainer}>
{loading && (
@@ -315,6 +373,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
<>
<div className={styles.floatingControlsContainer}>
<div className={styles.floatingControls}>
{!isFabricNative() && (
<button
type="button"
data-test="Sidebar/RefreshButton"
@@ -325,6 +384,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
>
<ArrowSync12Regular />
</button>
)}
<button
type="button"
className={styles.floatingControlButton}
@@ -357,7 +417,10 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
</CosmosFluentProvider>
</Allotment.Pane>
)}
<Allotment.Pane minSize={200}>
<Allotment.Pane
className={`${styles.minHeightResponsive} ${conditionalClass(isZoomed, styles.minHeightZoom)}`}
minSize={200}
>
<Tabs explorer={explorer} />
</Allotment.Pane>
</Allotment>

View File

@@ -1,13 +1,14 @@
/**
* Accordion top class
*/
import { Link, makeStyles, tokens } from "@fluentui/react-components";
import { makeStyles, tokens } from "@fluentui/react-components";
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
import { isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil";
import * as React from "react";
import { userContext } from "UserContext";
import CosmosDbBlackIcon from "../../../images/CosmosDB_black.svg";
import LinkIcon from "../../../images/Link_blue.svg";
import Explorer from "../Explorer";
export interface SplashScreenProps {
@@ -60,6 +61,15 @@ const useStyles = makeStyles({
margin: "auto",
},
},
single: {
gridColumn: "1 / 4",
gridRow: "1 / 3",
"& svg": {
width: "64px",
height: "64px",
margin: "auto",
},
},
buttonContainer: {
height: "100%",
display: "flex",
@@ -108,12 +118,10 @@ const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className
onClick,
}) => {
const styles = useStyles();
// TODO Make this a11y copmliant: aria-label for icon
return (
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick}>
<div className={styles.buttonUpperPart}>{icon}</div>
<div className={styles.buttonLowerPart}>
<div aria-label={title} className={styles.buttonLowerPart}>
<div>{title}</div>
<div>{description}</div>
</div>
@@ -123,6 +131,8 @@ const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className
export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScreenProps) => {
const styles = useStyles();
const [openSampleDataImportDialog, setOpenSampleDataImportDialog] = React.useState(false);
const getSplashScreenButtons = (): JSX.Element => {
const buttons: FabricHomeScreenButtonProps[] = [
{
@@ -138,15 +148,21 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
title: "Sample data",
description: "Automatically load sample data in your database",
icon: <img src={CosmosDbBlackIcon} />,
onClick: () => setOpenSampleDataImportDialog(true),
},
{
title: "App development",
description: "Start here to use an SDK to build your apps",
icon: <LinkMultipleRegular />,
onClick: () => window.open("https://aka.ms/cosmosdbfabricsdk", "_blank"),
},
];
return (
return isFabricNativeReadOnly() ? (
<div className={styles.buttonsContainer}>
<FabricHomeScreenButton className={styles.single} {...buttons[2]} />
</div>
) : (
<div className={styles.buttonsContainer}>
<FabricHomeScreenButton className={styles.one} {...buttons[0]} />
<FabricHomeScreenButton className={styles.two} {...buttons[1]} />
@@ -155,19 +171,27 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
);
};
const title = "Build your database";
const title = isFabricNativeReadOnly() ? "Use your database" : "Build your database";
return (
<div className={styles.homeContainer}>
<>
<CosmosFluentProvider className={styles.homeContainer}>
<SampleDataImportDialog
open={openSampleDataImportDialog}
setOpen={setOpenSampleDataImportDialog}
explorer={props.explorer}
databaseName={userContext.fabricContext?.databaseName}
/>
<div className={styles.title} role="heading" aria-label={title}>
{title}
</div>
{getSplashScreenButtons()}
<div className={styles.footer}>
{/* <div className={styles.footer}>
Need help?{" "}
<Link href="https://cosmos.azure.com/docs" target="_blank">
<Link href="https://aka.ms/cosmosdbfabricdocs" target="_blank">
Learn more <img src={LinkIcon} alt="Learn more" />
</Link>
</div>
</div>
</div> */}
</CosmosFluentProvider>
</>
);
};

View File

@@ -0,0 +1,158 @@
import {
Button,
Dialog,
DialogActions,
DialogBody,
DialogContent,
DialogSurface,
DialogTitle,
makeStyles,
Spinner,
tokens,
} from "@fluentui/react-components";
import Explorer from "Explorer/Explorer";
import { checkContainerExists, createContainer, importData } from "Explorer/SplashScreen/SampleUtil";
import React, { useEffect, useState } from "react";
import * as ViewModels from "../../Contracts/ViewModels";
const SAMPLE_DATA_CONTAINER_NAME = "SampleData";
const useStyles = makeStyles({
dialogContent: {
alignItems: "center",
marginBottom: tokens.spacingVerticalL,
},
});
/**
* This dialog:
* - creates a container
* - imports data into the container
* @param props
* @returns
*/
export const SampleDataImportDialog: React.FC<{
open: boolean;
setOpen: (open: boolean) => void;
explorer: Explorer;
databaseName: string;
}> = (props) => {
const [status, setStatus] = useState<"idle" | "creating" | "importing" | "completed" | "error">("idle");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const containerName = SAMPLE_DATA_CONTAINER_NAME;
const [collection, setCollection] = useState<ViewModels.Collection>(undefined);
const styles = useStyles();
useEffect(() => {
// Reset state when dialog opens
if (props.open) {
setStatus("idle");
setErrorMessage(undefined);
}
}, [props.open]);
const handleStartImport = async (): Promise<void> => {
setStatus("creating");
const databaseName = props.databaseName;
if (checkContainerExists(databaseName, containerName)) {
const msg = `The container "${containerName}" in database "${databaseName}" already exists. Please delete it and retry.`;
setStatus("error");
setErrorMessage(msg);
return;
}
let collection;
try {
collection = await createContainer(databaseName, containerName, props.explorer);
} catch (error) {
setStatus("error");
setErrorMessage(`Failed to create container: ${error instanceof Error ? error.message : String(error)}`);
return;
}
try {
setStatus("importing");
await importData(collection);
setCollection(collection);
setStatus("completed");
} catch (error) {
setStatus("error");
setErrorMessage(`Failed to import data: ${error instanceof Error ? error.message : String(error)}`);
}
};
const handleActionOnClick = () => {
switch (status) {
case "idle":
handleStartImport();
break;
case "error":
props.setOpen(false);
break;
case "creating":
case "importing":
props.setOpen(false);
break;
case "completed":
props.setOpen(false);
collection.openTab();
break;
}
};
const renderContent = () => {
switch (status) {
case "idle":
return `Create a container "${containerName}" and import sample data into it. This may take a few minutes.`;
case "creating":
return <Spinner size="small" labelPosition="above" label={`Creating container "${containerName}"...`} />;
case "importing":
return <Spinner size="small" labelPosition="above" label={`Importing data into "${containerName}"...`} />;
case "completed":
return `Successfully created "${containerName}" with sample data.`;
case "error":
return (
<div style={{ color: "red" }}>
<div>Error: {errorMessage}</div>
</div>
);
}
};
const getButtonLabel = () => {
switch (status) {
case "idle":
return "Start";
case "creating":
case "importing":
return "Close";
case "completed":
return "Close";
case "error":
return "Close";
}
};
return (
<Dialog open={props.open} onOpenChange={(event, data) => props.setOpen(data.open)}>
<DialogSurface>
<DialogBody>
<DialogTitle>Sample Data</DialogTitle>
<DialogContent>
<div className={styles.dialogContent}>{renderContent()}</div>
</DialogContent>
<DialogActions>
<Button
appearance="primary"
onClick={handleActionOnClick}
disabled={status === "creating" || status === "importing"}
>
{getButtonLabel()}
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
);
};

View File

@@ -0,0 +1,64 @@
import { BackendDefaults } from "Common/Constants";
import { createCollection } from "Common/dataAccess/createCollection";
import Explorer from "Explorer/Explorer";
import { useDatabases } from "Explorer/useDatabases";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
/**
* Public for unit tests
* @param databaseName
* @param containerName
* @param containerDatabases
*/
const hasContainer = (
databaseName: string,
containerName: string,
containerDatabases: ViewModels.Database[],
): boolean => {
const filteredDatabases = containerDatabases.filter((database) => database.id() === databaseName);
return (
filteredDatabases.length > 0 &&
filteredDatabases[0].collections().filter((collection) => collection.id() === containerName).length > 0
);
};
export const checkContainerExists = (databaseName: string, containerName: string) =>
hasContainer(databaseName, containerName, useDatabases.getState().databases);
export const createContainer = async (
databaseName: string,
containerName: string,
explorer: Explorer,
): Promise<ViewModels.Collection> => {
const createRequest: DataModels.CreateCollectionParams = {
createNewDatabase: false,
collectionId: containerName,
databaseId: databaseName,
databaseLevelThroughput: false,
partitionKey: {
paths: [`/${SAMPLE_DATA_PARTITION_KEY}`],
kind: "Hash",
version: BackendDefaults.partitionKeyVersion,
},
};
await createCollection(createRequest);
await explorer.refreshAllDatabases();
const database = useDatabases.getState().findDatabaseWithId(databaseName);
if (!database) {
return undefined;
}
await database.loadCollections();
const newCollection = database.findCollectionWithId(containerName);
return newCollection;
};
const SAMPLE_DATA_PARTITION_KEY = "category"; // This pkey is specifically set for queryCopilotSampleData.json below
export const importData = async (collection: ViewModels.Collection): Promise<void> => {
// TODO: keep same chunk as ContainerSampleGenerator
const dataFileContent = await import(
/* webpackChunkName: "queryCopilotSampleData" */ "../../../sampleData/queryCopilotSampleData.json"
);
await collection.bulkInsertDocuments(dataFileContent.data);
};

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