Compare commits

..

78 Commits

Author SHA1 Message Date
Sung-Hyun Kang
98d7e89712 Added VS Code Exnteion for DE 2025-09-18 08:03:06 -05:00
Sung-Hyun Kang
d45e10f5ac Added VS Code Exnteion for DE 2025-09-18 08:01:01 -05:00
Nishtha Ahuja
cfb5db4df6 Removed screenshot for mongo cloudshell (#2211)
Co-authored-by: nishthaAhujaa <nishtha17354@iiittd.ac.in>
2025-09-16 12:16:19 +05:30
Dmitry Shilov
922ca5c523 chore: Update help link in FabricHome component to point to the new documentation (#2206) 2025-09-04 11:58:07 +02:00
Dmitry Shilov
bafe002fa3 chore: Enhance accessibility (#2208)
- Add tabIndex to button
- Add aria attributes to icons and headings
2025-09-04 11:39:11 +02:00
vchske
0817acf404 Commenting or deleting UI references to Query Advisor (#2209)
* Commenting or deleting UI references to Query Advisor

* Removing (commenting out) QueryTabComponent from two views

* Added new splash screen button, commented out copilot prompt bar

* Fixing unit test
2025-08-28 15:47:29 -07:00
asier-isayas
8e2c46301d Allow Mongo users to change thee Guid Representation when conducting CRUD operations for documents (#2204)
* mongo guid representation

* format

* fix return type

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-08-18 12:30:04 -07:00
BChoudhury-ms
012d043c78 Fix CloudShell terminal hanging for Mongo and Cassandra shells due to missing updateTerminalData method (#2199) 2025-08-13 13:02:27 -07:00
Mike Krüger
3afd74a957 Fix faifax default cloud shell region. (#2201) 2025-08-13 11:25:18 -07:00
jawelton74
0ef4399ba4 Support data plane RBAC for E2E tests. (#2176)
* Acquire token for NoSQL account prior to running tests.

* Change client id to user assigned managed identity.

* Change to use managed identity. Add token variables for gremlin and
tables.

* Add RBAC details to test README.

* Add token for SQL readonly database. Skip resource token tests when RBAC
enabled.

* Use hardcoded account name for sql readonly.

* Use specific tag for sql readonly.

* Remove comment.
2025-08-05 10:59:57 -07:00
BChoudhury-ms
870863a723 Disabling telemetry for mongo shell (#2195)
* Disabling telemetry for mongo shell

* fix formatting issues

* removed empty spaces from terminal and segregated disableTelemetry command
2025-07-31 10:03:09 +05:30
JustinKol
e3815734db Min/Max UX changes for Scale & Settings tab (#2192)
* master pull

* changed min max text boxes for autoscale

* test update

* Update .npmrc

* Update settings.json

* Prettier run
2025-07-28 13:24:19 -04:00
BChoudhury-ms
5ea78f9abf Add VCore MongoDB support for VS Code extension integration (#2181)
* support for vCore mongodb support for vscode extension

* added comment for future referance on the double encoded connection string
2025-07-28 22:52:23 +05:30
sindhuba
8a56214ec2 Upgrade SDK version to 4.5.0 (#2194) 2025-07-21 12:44:42 -07:00
Laurent Nguyen
e3ae006100 feat: Disable few checks if Fabric native (#2188)
* Disable few checks if Fabric native

* Fix typo in error message
2025-07-17 08:47:56 +02:00
asier-isayas
589b61afaf Upgrade Cosmos SDK to v4.4.0 (#2187)
* Upgrade Cosmos SDK to v4.4.0

* fix unit tests

* explain crypto.subtle

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-07-15 11:28:28 -07:00
Laurent Nguyen
eb3f6bc93f Fabric: set default throughput for new container to 5k (#2190) 2025-07-15 20:22:06 +02:00
bogercraig
6ec909a97b Use Config.json to Set Environment Specific AAD Auth Settings (#2184)
* Force hosted explorer to load config.json before calling useAADAuth.

* Ensure AAD endpoint from config.json loads before useAADAuth.

* Fix immediate linting errors and warnings.

* Remove separate spinner for waiting on config to load.

* Simplifying auth and reintroducing "No tokens for given scope, and no authorization code was passed to acquireToken." error.  Blocking on login if incorrect AAD_ENDPOINT provided.

* Fix linting errors.

* Add error handling to prevent unhandled errors thrown when login prompt is cancelled prematurely.
2025-07-11 15:34:10 -07:00
asier-isayas
08a51ca6b1 Remove unneeded and misleading console log (#2186)
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-07-10 08:58:11 -07:00
jawelton74
30a3b5c7a4 Support data plane RBAC for Gremlin API (#2182)
* Refactor logic for determining if we should use data plane RBAC to a
common function.

* Support RBAC for gremlin API.

* Refactor to use common function.

* Fix unit tests.

* Move test function inside test scope.

* Minor clean ups.

* Reinstate utf8ToB64 function in case this breaks a corner case.
2025-07-09 16:23:09 -07:00
jawelton74
f370507a27 Refactor logic for determining if we should use data plane RBAC (#2180)
* Refactor logic for determining if we should use data plane RBAC to a
common function.

* Move test function into test scope.
2025-07-08 11:41:43 -07:00
sunghyunkang1111
e0edaf405c MSRC fixes for testExplorer and HeatMap (#2183)
* MSRC fixes for testExplorer and HeatMap

* MSRC fixes for testExplorer and HeatMap
2025-07-07 11:00:29 -05:00
Sourabh Jain
f8231600d6 DB Shell: Fix User Consent Message as per Privacy Guidlines and enable this for customers (#2173)
* Fix user Consent msg and fix issues

* fix format

* test fix

* fix snap file
2025-07-07 21:13:34 +05:30
asier-isayas
45c8d70c77 do not set disableNonStreamingOrderByQuery flag (#2179)
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-06-26 07:43:55 -07:00
bogercraig
70d7ee755b Add Additional Config from Config.json and Clean Up Unused Config (#2178)
* Cleaning up unused config from portal backend migration.

* Remove config used during backend migration.

* Add backend endpoint override from config.json.

* Add AAD and ARM endpoint overrides from config.json.

* Add GRAPH_ENDPOINT override from config.json.

* Remove unused catalog api version.

* Remove isTerminalEnabled from config.  Cannot find reference in DE, DE Release, or Frontend.

* Fix mongo client unit tests.

* Removing BackendApi from constants since no longer referenced in the codebase.

* Talked with Tara and added the CATALOG_API_VERSION back to the config and substituted out the hard coded string it was intended to replace.

* Include existing portal backend endpoints in default allow list.

* Add localhost:1234 endpoint for Mongo unit tests.

* Removing old backend local test endpoint from backend endpoint list.
2025-06-24 12:50:21 -07:00
Vsevolod Kukol
0a4aed4f47 feat: Enable Container Vector Policy for Fabric Native and adjust throughput/vecotr input visibility logic (#2170) 2025-06-23 17:18:19 +02:00
Dmitry Shilov
a7d007e0dd fix: Partition Keys block for fabric (#2174)
* fix: Partition Keys block for fabric

- Show Partition Keys block for fabric
- Adjust layout and visibility of separators in AddCollectionPanel
2025-06-23 17:18:09 +02:00
sunghyunkang1111
5f4a4e5c4c Added Quickstart open action (#2175) 2025-06-17 12:08:12 -05:00
asier-isayas
1b64827c24 Upgrade ARM Client API version to 2025-05-01-preview & Fetch enableMaterializedViews account property from ARM (#2171)
* Upgrade ARM Client API version to 2025-05-01-preview

* fix npm run compile

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-06-13 06:20:08 -04:00
SATYA SB
a6ae784a45 [accessibility-3739744]:[Supporting the Platform - Azure Cosmos DB- Data Explorer - New Vertex]: When page viewport is set to 320x256px, Content under 'New Vertex' pane is not properly visible. (#2163)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2025-06-12 17:58:10 +05:30
SATYA SB
7458107efd [accessibility-3726486]:[Keyboard Navigation - Azure Cosmos DB - Data Explorer - New Graph]: Keyboard focus indicator is not landing on the first interactive control after activating the 'New Graph' control. (#2073)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2025-06-12 17:57:21 +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
203 changed files with 9155 additions and 3875 deletions

View File

@@ -23,8 +23,6 @@ src/Common/MongoUtility.ts
src/Common/NotificationsClientBase.ts
src/Common/QueriesClient.ts
src/Common/Splitter.ts
src/Controls/Heatmap/Heatmap.test.ts
src/Controls/Heatmap/Heatmap.ts
src/Definitions/datatables.d.ts
src/Definitions/gif.d.ts
src/Definitions/globals.d.ts

View File

@@ -164,24 +164,42 @@ jobs:
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
shardTotal: [10]
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.E2E_TESTS_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# We can't use MSAL within playwright so we acquire tokens prior to running the tests
- name: "Acquire RBAC tokens for test accounts"
uses: azure/cli@v2
with:
azcliversion: latest
inlineScript: |
NOSQL_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_TOKEN"
echo NOSQL_TESTACCOUNT_TOKEN=$NOSQL_TESTACCOUNT_TOKEN >> $GITHUB_ENV
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV
- 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

@@ -27,7 +27,7 @@ jobs:
- name: "Az CLI login"
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

2
.npmrc
View File

@@ -1,4 +1,4 @@
save-exact=true
# Ignore peer dependency conflicts
force=true # TODO: Remove this when we update to React 17 or higher!
force=true # TODO: Remove this when we update to React 17 or higher!

View File

@@ -1,5 +1,4 @@
{
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
"isTerminalEnabled": true,
"isPhoenixEnabled": true
}
}

View File

@@ -1,5 +1,4 @@
{
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
"isTerminalEnabled" : false,
"isPhoenixEnabled" : false
}
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
"isPhoenixEnabled": false
}

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

File diff suppressed because it is too large Load Diff

239
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.5.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==",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
"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/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==",
"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.0.2",
"debug": "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": ">=18.0.0"
}
},
"node_modules/@azure/core-rest-pipeline/node_modules/tslib": {
@@ -377,23 +391,25 @@
"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.5.0",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.5.0.tgz",
"integrity": "sha512-JsTh4twb6FcwP7rJwxQiNZQ/LGtuF6gmciaxY9Rnp6/A325Lhsw/SH4R2ArpT0yCvozbZpweIwdPfUkXVBtp5w==",
"license": "MIT",
"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/abort-controller": "^2.1.2",
"@azure/core-auth": "^1.9.0",
"@azure/core-rest-pipeline": "^1.19.1",
"@azure/core-tracing": "^1.2.0",
"@azure/core-util": "^1.11.0",
"@azure/keyvault-keys": "^4.9.0",
"@azure/logger": "^1.1.4",
"fast-json-stable-stringify": "^2.1.0",
"jsbi": "^4.3.0",
"priorityqueuejs": "^2.0.0",
"semaphore": "^1.1.0",
"tslib": "^2.6.2"
"tslib": "^2.8.1"
},
"engines": {
"node": ">=18.0.0"
"node": ">=20.0.0"
}
},
"node_modules/@azure/cosmos-language-service": {
@@ -423,8 +439,9 @@
}
},
"node_modules/@azure/cosmos/node_modules/tslib": {
"version": "2.6.2",
"license": "0BSD"
"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/identity": {
"version": "4.5.0",
@@ -490,14 +507,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 +13141,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 +13359,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"
@@ -27048,11 +27180,6 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/jsbi": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz",
"integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g=="
},
"node_modules/jsbn": {
"version": "0.1.1",
"license": "MIT"

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.5.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

@@ -37,20 +37,51 @@ export default defineConfig({
},
{
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

@@ -138,15 +138,6 @@ export enum MongoBackendEndpointType {
remote,
}
export class BackendApi {
public static readonly GenerateToken: string = "GenerateToken";
public static readonly PortalSettings: string = "PortalSettings";
public static readonly AccountRestrictions: string = "AccountRestrictions";
public static readonly RuntimeProxy: string = "RuntimeProxy";
public static readonly DisallowedLocations: string = "DisallowedLocations";
public static readonly SampleData: string = "SampleData";
}
export class PortalBackendEndpoints {
public static readonly Development: string = "https://localhost:7235";
public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
@@ -257,6 +248,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 {
@@ -773,3 +765,10 @@ export const ShortenedQueryCopilotSampleContainerSchema = {
userPrompt: "find all products",
};
export enum MongoGuidRepresentation {
Standard = "Standard",
CSharpLegacy = "CSharpLegacy",
JavaLegacy = "JavaLegacy",
PythonLegacy = "PythonLegacy",
}

View File

@@ -4,12 +4,12 @@ import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
import { AuthType } from "../AuthType";
import { PriorityLevel } from "../Common/Constants";
import * as Logger from "../Common/Logger";
import { Platform, configContext } from "../ConfigContext";
import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
import { isDataplaneRbacSupported } from "../Utils/APITypeUtils";
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
@@ -20,8 +20,7 @@ const _global = typeof self === "undefined" ? window : self;
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
const { verb, resourceId, resourceType, headers } = requestInfo;
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType);
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
if (useDataplaneRbacAuthorization(userContext)) {
Logger.logInfo(
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
"Explorer/tokenProvider",

View File

@@ -1,4 +1,3 @@
import { QueryOperationOptions } from "@azure/cosmos";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as Constants from "../Common/Constants";
import { QueryResults } from "../Contracts/ViewModels";
@@ -14,18 +13,14 @@ 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> {
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> {
TelemetryProcessor.traceStart(Action.ExecuteQuery);
return documentsIterator.fetchNext(queryOperationOptions).then((response) => {
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

View File

@@ -65,7 +65,6 @@ describe("MongoProxyClient", () => {
});
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
@@ -84,7 +83,6 @@ describe("MongoProxyClient", () => {
it("builds the correct proxy URL in development", () => {
updateConfigContext({
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [],
});
queryDocuments(databaseId, collection, true, "{}");
expect(window.fetch).toHaveBeenCalledWith(
@@ -101,7 +99,6 @@ describe("MongoProxyClient", () => {
});
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
@@ -120,7 +117,6 @@ describe("MongoProxyClient", () => {
it("builds the correct proxy URL in development", () => {
updateConfigContext({
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [],
});
readDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith(
@@ -137,7 +133,6 @@ describe("MongoProxyClient", () => {
});
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
@@ -156,7 +151,6 @@ describe("MongoProxyClient", () => {
it("builds the correct proxy URL in development", () => {
updateConfigContext({
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [],
});
readDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith(
@@ -173,7 +167,6 @@ describe("MongoProxyClient", () => {
});
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
@@ -197,7 +190,6 @@ describe("MongoProxyClient", () => {
});
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
@@ -216,7 +208,6 @@ describe("MongoProxyClient", () => {
it("builds the correct proxy URL in development", () => {
updateConfigContext({
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [],
});
deleteDocuments(databaseId, collection, [documentId]);
expect(window.fetch).toHaveBeenCalledWith(
@@ -233,7 +224,6 @@ describe("MongoProxyClient", () => {
});
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
});

View File

@@ -1,4 +1,5 @@
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
import { getMongoGuidRepresentation } from "Shared/StorageUtility";
import { AuthType } from "../AuthType";
import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
@@ -139,6 +140,9 @@ export function readDocument(
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
? documentId.partitionKeyProperties?.[0]
: "",
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
};
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
@@ -181,6 +185,9 @@ export function createDocument(
partitionKey:
collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
documentContent: JSON.stringify(documentContent),
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
};
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
@@ -228,6 +235,9 @@ export function updateDocument(
? documentId.partitionKeyProperties?.[0]
: "",
documentContent,
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
};
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
@@ -274,6 +284,9 @@ export function deleteDocuments(
subscriptionID: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
databaseAccountName: databaseAccount.name,
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
};
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);

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

@@ -2,7 +2,7 @@
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
{
"disableNonStreamingOrderByQuery": true,
"enableQueryControl": false,
"enableScanInQuery": true,
"forceQueryPlan": true,
"maxDegreeOfParallelism": 0,
@@ -13,7 +13,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

@@ -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,12 +66,16 @@ export const deleteDocuments = async (
operationType: BulkOperationType.Delete,
}));
const promise = v2Container.items.bulk(operations).then((bulkResults) => {
return bulkResults.map((bulkResult, index) => {
const documentId = documentIdsChunk[index];
return { ...bulkResult, documentId };
const promise = v2Container.items
.bulk(operations, undefined, {
abortSignal,
})
.then((bulkResults) => {
return bulkResults.map((bulkResult, index) => {
const documentId = documentIdsChunk[index];
return { ...bulkResult, documentId };
});
});
});
promiseArray.push(promise);
}

View File

@@ -1,3 +1,4 @@
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType";
import { configContext } from "../../ConfigContext";
import { userContext } from "../../UserContext";
@@ -41,7 +42,7 @@ interface MetricsResponse {
}
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
if (userContext.authType !== AuthType.AAD) {
if (userContext.authType !== AuthType.AAD || isFabricNative()) {
return undefined;
}

View File

@@ -1,5 +1,4 @@
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Queries } from "../Constants";
import { client } from "../CosmosClient";
@@ -26,7 +25,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

@@ -126,12 +126,5 @@ async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Co
throw new Error(`Unsupported default experience type: ${apiType}`);
}
// 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;
});
return rpResponse?.value?.map((collection) => collection.properties?.resource as DataModels.Collection);
}

View File

@@ -1,21 +1,15 @@
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
import {
BackendApi,
CassandraProxyEndpoints,
JunoEndpoints,
MongoProxyEndpoints,
PortalBackendEndpoints,
} from "Common/Constants";
import {
allowedAadEndpoints,
allowedArcadiaEndpoints,
allowedEmulatorEndpoints,
allowedGraphEndpoints,
allowedHostedExplorerEndpoints,
allowedJunoOrigins,
allowedMsalRedirectEndpoints,
defaultAllowedAadEndpoints,
defaultAllowedArmEndpoints,
defaultAllowedBackendEndpoints,
defaultAllowedCassandraProxyEndpoints,
defaultAllowedGraphEndpoints,
defaultAllowedMongoProxyEndpoints,
validateEndpoint,
} from "Utils/EndpointUtils";
@@ -29,6 +23,8 @@ export enum Platform {
export interface ConfigContext {
platform: Platform;
allowedAadEndpoints: ReadonlyArray<string>;
allowedGraphEndpoints: ReadonlyArray<string>;
allowedArmEndpoints: ReadonlyArray<string>;
allowedBackendEndpoints: ReadonlyArray<string>;
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
@@ -37,10 +33,8 @@ export interface ConfigContext {
gitSha?: string;
proxyPath?: string;
AAD_ENDPOINT: string;
ARM_AUTH_AREA: string;
ARM_ENDPOINT: string;
EMULATOR_ENDPOINT?: string;
ARM_API_VERSION: string;
GRAPH_ENDPOINT: string;
GRAPH_API_VERSION: string;
// This is the endpoint to get offering Ids to be used to fetch prices. Refer to this doc: https://learn.microsoft.com/en-us/rest/api/marketplacecatalog/dataplane/skus/list?view=rest-marketplacecatalog-dataplane-2023-05-01-preview&tabs=HTTP
@@ -50,27 +44,24 @@ export interface ConfigContext {
ARCADIA_ENDPOINT: string;
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
PORTAL_BACKEND_ENDPOINT: string;
NEW_BACKEND_APIS?: BackendApi[];
MONGO_PROXY_ENDPOINT: string;
CASSANDRA_PROXY_ENDPOINT: string;
NEW_CASSANDRA_APIS?: string[];
PROXY_PATH?: string;
JUNO_ENDPOINT: string;
GITHUB_CLIENT_ID: string;
GITHUB_TEST_ENV_CLIENT_ID: string;
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
isTerminalEnabled: boolean;
isPhoenixEnabled: boolean;
hostedExplorerURL: string;
armAPIVersion?: string;
msalRedirectURI?: string;
globallyEnabledCassandraAPIs?: string[];
globallyEnabledMongoAPIs?: string[];
}
// Default configuration
let configContext: Readonly<ConfigContext> = {
platform: Platform.Portal,
allowedAadEndpoints: defaultAllowedAadEndpoints,
allowedGraphEndpoints: defaultAllowedGraphEndpoints,
allowedArmEndpoints: defaultAllowedArmEndpoints,
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
@@ -85,17 +76,12 @@ let configContext: Readonly<ConfigContext> = {
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
`^https:\\/\\/.*\\.powerbi\\.com$`,
`^https:\\/\\/.*\\.analysis-df\\.net$`,
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
`^https:\\/\\/.*\\.azure-test\\.net$`,
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
], // Webpack injects this at build time
gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/",
AAD_ENDPOINT: "https://login.microsoftonline.com/",
ARM_AUTH_AREA: "https://management.azure.com/",
ARM_ENDPOINT: "https://management.azure.com/",
ARM_API_VERSION: "2016-06-01",
GRAPH_ENDPOINT: "https://graph.microsoft.com",
GRAPH_API_VERSION: "1.6",
CATALOG_ENDPOINT: "https://catalogapi.azure.com/",
@@ -109,11 +95,7 @@ let configContext: Readonly<ConfigContext> = {
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
isTerminalEnabled: false,
isPhoenixEnabled: false,
globallyEnabledCassandraAPIs: [],
globallyEnabledMongoAPIs: [],
};
export function resetConfigContext(): void {
@@ -128,19 +110,21 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
return;
}
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
delete newContext.ARM_ENDPOINT;
if (!validateEndpoint(newContext.AAD_ENDPOINT, configContext.allowedAadEndpoints || defaultAllowedAadEndpoints)) {
delete newContext.AAD_ENDPOINT;
}
if (!validateEndpoint(newContext.AAD_ENDPOINT, allowedAadEndpoints)) {
delete newContext.AAD_ENDPOINT;
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
delete newContext.ARM_ENDPOINT;
}
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
delete newContext.EMULATOR_ENDPOINT;
}
if (!validateEndpoint(newContext.GRAPH_ENDPOINT, allowedGraphEndpoints)) {
if (
!validateEndpoint(newContext.GRAPH_ENDPOINT, configContext.allowedGraphEndpoints || defaultAllowedGraphEndpoints)
) {
delete newContext.GRAPH_ENDPOINT;
}
@@ -148,6 +132,15 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
delete newContext.ARCADIA_ENDPOINT;
}
if (
!validateEndpoint(
newContext.PORTAL_BACKEND_ENDPOINT,
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints,
)
) {
delete newContext.PORTAL_BACKEND_ENDPOINT;
}
if (
!validateEndpoint(
newContext.MONGO_PROXY_ENDPOINT,

View File

@@ -23,6 +23,7 @@ export enum PaneKind {
GlobalSettings,
AdHocAccess,
SwitchDirectory,
QuickStart,
}
/**

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

@@ -7,6 +7,7 @@ export interface ArmEntity {
type: string;
kind: string;
tags?: Tags;
resourceGroup?: string;
}
export interface DatabaseAccount extends ArmEntity {
@@ -43,6 +44,12 @@ export interface DatabaseAccountExtendedProperties {
publicNetworkAccess?: string;
enablePriorityBasedExecution?: boolean;
vcoreMongoEndpoint?: string;
apiProperties?: ApiProperties;
}
export interface ApiProperties {
/* Describes the version of the MongoDB account. */
serverVersion?: "3.2" | "3.6" | "4.0" | "4.2" | "5.0" | "6.0" | "7.0";
}
export interface DatabaseAccountResponseLocation {
@@ -210,7 +217,7 @@ export interface IndexingPolicy {
export interface VectorIndex {
path: string;
type: "flat" | "diskANN" | "quantizedFlat";
diskANNShardKey?: string;
vectorIndexShardKey?: string[];
indexingSearchListSize?: number;
quantizationByteSize?: number;
}
@@ -388,7 +395,7 @@ export interface VectorEmbeddingPolicy {
}
export interface VectorEmbedding {
dataType: "float16" | "float32" | "uint8" | "int8";
dataType: "float32" | "uint8" | "int8";
dimensions: number;
distanceFunction: "euclidean" | "cosine" | "dotproduct";
path: string;

View File

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

View File

@@ -1,4 +1,5 @@
import {
ItemDefinition,
JSONObject,
QueryMetrics,
Resource,
@@ -30,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;
@@ -46,6 +50,7 @@ export interface QueryResults extends QueryResultsMetadata {
roundTrips?: number;
headers?: any;
queryMetrics?: QueryMetrics;
ruThresholdExceeded?: boolean;
}
export interface Button {
@@ -438,6 +443,7 @@ export interface DataExplorerInputsFrame {
[key: string]: string;
};
feedbackPolicies?: any;
aadToken?: string;
}
export interface SelfServeFrameInputs {

View File

@@ -1,11 +0,0 @@
<!doctype html>
<html class="no-js" lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="data:," />
</head>
<body>
<div id="heatmap"></div>
</body>
</html>

View File

@@ -1,55 +0,0 @@
@import "../../../less/Common/Constants";
html {
font-family: @DataExplorerFont;
padding: 0px;
margin: 0px;
border: 0px;
overflow: hidden;
position: fixed;
width: 100%;
height: 100%;
}
body {
font-family: @DataExplorerFont;
padding: 0px;
margin: 0px;
border: 0px;
overflow: hidden;
}
#heatmap {
.dark-theme {
color: @BaseLight;
}
.chartTitle {
position: absolute;
top: 5px;
left: 3px;
font-size: 13px;
}
.noDataMessage {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
z-index: 10000;
height: 100%;
width: 100%;
top: 0;
left: 0;
opacity: 0.97;
div {
border-color: rgba(204, 204, 204, 0.8);
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.12);
padding: 15px 10px;
width: calc(55% - 40px);
font-size: 13px;
text-align: center;
border-width: 1px;
border-style: solid;
}
}
}

View File

@@ -1,143 +0,0 @@
import dayjs from "dayjs";
import { handleMessage, Heatmap, isDarkTheme } from "./Heatmap";
import { PortalTheme } from "./HeatmapDatatypes";
describe("The Heatmap Control", () => {
const dataPoints = {
"1": {
"2019-06-19T00:59:10Z": {
"Normalized Throughput": 0.35,
},
"2019-06-19T00:48:10Z": {
"Normalized Throughput": 0.25,
},
},
};
const chartCaptions = {
chartTitle: "chart title",
yAxisTitle: "YAxisTitle",
tooltipText: "Tooltip text",
timeWindow: 123456789,
};
let heatmap: Heatmap;
const theme: PortalTheme = 1;
const divElement = `<div id="${Heatmap.elementId}"></div>`;
describe("drawHeatmap rendering", () => {
beforeEach(() => {
heatmap = new Heatmap(dataPoints, chartCaptions, theme);
document.body.innerHTML = divElement;
});
afterEach(() => {
document.body.innerHTML = ``;
});
it("should call _getChartSettings when drawHeatmap is invoked", () => {
const _getChartSettings = jest.spyOn(heatmap, "_getChartSettings");
heatmap.drawHeatmap();
expect(_getChartSettings).toHaveBeenCalled();
});
it("should call _getLayoutSettings when drawHeatmap is invoked", () => {
const _getLayoutSettings = jest.spyOn(heatmap, "_getLayoutSettings");
heatmap.drawHeatmap();
expect(_getLayoutSettings).toHaveBeenCalled();
});
it("should call _getChartDisplaySettings when drawHeatmap is invoked", () => {
const _getChartDisplaySettings = jest.spyOn(heatmap, "_getChartDisplaySettings");
heatmap.drawHeatmap();
expect(_getChartDisplaySettings).toHaveBeenCalled();
});
it("drawHeatmap should render a Heatmap inside the div element", () => {
heatmap.drawHeatmap();
expect(document.body.innerHTML).not.toEqual(divElement);
});
});
describe("generateMatrixFromMap", () => {
it("should massage input data to match output expected", () => {
expect(heatmap.generateMatrixFromMap(dataPoints).yAxisPoints).toEqual(["1"]);
expect(heatmap.generateMatrixFromMap(dataPoints).dataPoints).toEqual([[0.25, 0.35]]);
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints.length).toEqual(2);
});
it("should output the date format to ISO8601 string format", () => {
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints[0].slice(10, 11)).toEqual("T");
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints[0].slice(-1)).toEqual("Z");
});
it("should convert the time to the user's local time", () => {
if (dayjs().utcOffset()) {
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).not.toEqual([
"2019-06-19T00:48:10Z",
"2019-06-19T00:59:10Z",
]);
} else {
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).toEqual([
"2019-06-19T00:48:10Z",
"2019-06-19T00:59:10Z",
]);
}
});
});
describe("isDarkTheme", () => {
it("isDarkTheme should return the correct result", () => {
expect(isDarkTheme(PortalTheme.dark)).toEqual(true);
expect(isDarkTheme(PortalTheme.azure)).not.toEqual(true);
});
});
});
describe("iframe rendering when there is no data", () => {
afterEach(() => {
document.body.innerHTML = ``;
});
it("should show a no data message with a dark theme", () => {
const data = {
data: {
signature: "pcIframe",
data: {
chartData: {},
chartSettings: {},
theme: 4,
},
},
origin: "http://localhost",
};
const divElement = `<div id="${Heatmap.elementId}"></div>`;
document.body.innerHTML = divElement;
handleMessage(data as MessageEvent);
expect(document.body.innerHTML).toContain("dark-theme");
expect(document.body.innerHTML).toContain("noDataMessage");
});
it("should show a no data message with a white theme", () => {
const data = {
data: {
signature: "pcIframe",
data: {
chartData: {},
chartSettings: {},
theme: 2,
},
},
origin: "http://localhost",
};
const divElement = `<div id="${Heatmap.elementId}"></div>`;
document.body.innerHTML = divElement;
handleMessage(data as MessageEvent);
expect(document.body.innerHTML).not.toContain("dark-theme");
expect(document.body.innerHTML).toContain("noDataMessage");
});
});

View File

@@ -1,272 +0,0 @@
import dayjs from "dayjs";
import * as Plotly from "plotly.js-cartesian-dist-min";
import { sendCachedDataMessage, sendReadyMessage } from "../../Common/MessageHandler";
import { StyleConstants } from "../../Common/StyleConstants";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import "./Heatmap.less";
import {
ChartSettings,
DataPayload,
DisplaySettings,
FontSettings,
HeatmapCaptions,
HeatmapData,
LayoutSettings,
PartitionTimeStampToData,
PortalTheme,
} from "./HeatmapDatatypes";
export class Heatmap {
public static readonly elementId: string = "heatmap";
private _chartData: HeatmapData;
private _heatmapCaptions: HeatmapCaptions;
private _theme: PortalTheme;
private _defaultFontColor: string;
constructor(data: DataPayload, heatmapCaptions: HeatmapCaptions, theme: PortalTheme) {
this._theme = theme;
this._defaultFontColor = StyleConstants.BaseDark;
this._setThemeColorForChart();
this._chartData = this.generateMatrixFromMap(data);
this._heatmapCaptions = heatmapCaptions;
}
private _setThemeColorForChart() {
if (isDarkTheme(this._theme)) {
this._defaultFontColor = StyleConstants.BaseLight;
}
}
private _getFontStyles(size: number = StyleConstants.MediumFontSize, color = "#838383"): FontSettings {
return {
family: StyleConstants.DataExplorerFont,
size,
color,
};
}
public generateMatrixFromMap(data: DataPayload): HeatmapData {
// all keys in data payload, sorted...
const rows: string[] = Object.keys(data).sort((a: string, b: string) => {
if (parseInt(a) < parseInt(b)) {
return -1;
} else {
if (parseInt(a) > parseInt(b)) {
return 1;
} else {
return 0;
}
}
});
const output: HeatmapData = {
yAxisPoints: [],
dataPoints: [],
xAxisPoints: Object.keys(data[rows[0]]).sort((a: string, b: string) => {
if (a < b) {
return -1;
} else {
if (a > b) {
return 1;
} else {
return 0;
}
}
}),
};
// go thru all rows and create 2d matrix for heatmap...
for (let i = 0; i < rows.length; i++) {
output.yAxisPoints.push(rows[i]);
const dataPoints: number[] = [];
for (let a = 0; a < output.xAxisPoints.length; a++) {
const row: PartitionTimeStampToData = data[rows[i]];
dataPoints.push(row[output.xAxisPoints[a]]["Normalized Throughput"]);
}
output.dataPoints.push(dataPoints);
}
for (let a = 0; a < output.xAxisPoints.length; a++) {
const dateTime = output.xAxisPoints[a];
// convert to local users timezone...
const day = dayjs(new Date(dateTime)).format("YYYY-MM-DD");
const hour = dayjs(new Date(dateTime)).format("HH:mm:ss");
// coerce to ISOString format since that is what plotly wants...
output.xAxisPoints[a] = `${day}T${hour}Z`;
}
return output;
}
// public for testing purposes
public _getChartSettings(): ChartSettings[] {
return [
{
z: this._chartData.dataPoints,
type: "heatmap",
zmin: 0,
zmid: 50,
zmax: 100,
colorscale: [
[0.0, "#1FD338"],
[0.1, "#1CAD2F"],
[0.2, "#50A527"],
[0.3, "#719F21"],
[0.4, "#95991B"],
[0.5, "#CE8F11"],
[0.6, "#E27F0F"],
[0.7, "#E46612"],
[0.8, "#E64914"],
[0.9, "#B80016"],
[1.0, "#B80016"],
],
name: "",
hovertemplate: this._heatmapCaptions.tooltipText,
colorbar: {
thickness: 15,
outlinewidth: 0,
tickcolor: StyleConstants.BaseDark,
tickfont: this._getFontStyles(10, this._defaultFontColor),
},
y: this._chartData.yAxisPoints,
x: this._chartData.xAxisPoints,
},
];
}
// public for testing purposes
public _getLayoutSettings(): LayoutSettings {
return {
margin: {
l: 40,
r: 10,
b: 35,
t: 30,
pad: 0,
},
paper_bgcolor: "transparent",
plot_bgcolor: "transparent",
width: 462,
height: 240,
yaxis: {
title: this._heatmapCaptions.yAxisTitle,
titlefont: this._getFontStyles(11),
autorange: true,
showgrid: false,
zeroline: false,
showline: false,
autotick: true,
fixedrange: true,
ticks: "",
showticklabels: false,
},
xaxis: {
fixedrange: true,
title: "*White area in heatmap indicates there is no available data",
titlefont: this._getFontStyles(11),
autorange: true,
showgrid: false,
zeroline: false,
showline: false,
autotick: true,
tickformat: this._heatmapCaptions.timeWindow > 7 ? "%I:%M %p" : "%b %e",
showticklabels: true,
tickfont: this._getFontStyles(10),
},
title: {
text: this._heatmapCaptions.chartTitle,
x: 0.01,
font: this._getFontStyles(13, this._defaultFontColor),
},
};
}
// public for testing purposes
public _getChartDisplaySettings(): DisplaySettings {
return {
/* heatmap can be fully responsive however the min-height needed in that case is greater than the iframe portal height, hence explicit width + height have been set in _getLayoutSettings
responsive: true,*/
displayModeBar: false,
};
}
public drawHeatmap(): void {
// todo - create random elementId generator so multiple heatmaps can be created - ticket # 431469
Plotly.plot(
Heatmap.elementId,
this._getChartSettings(),
this._getLayoutSettings(),
this._getChartDisplaySettings(),
);
const plotDiv: any = document.getElementById(Heatmap.elementId);
plotDiv.on("plotly_click", (data: any) => {
let timeSelected: string = data.points[0].x;
timeSelected = timeSelected.replace(" ", "T");
timeSelected = `${timeSelected}Z`;
let xAxisIndex = 0;
for (let i = 0; i < this._chartData.xAxisPoints.length; i++) {
if (this._chartData.xAxisPoints[i] === timeSelected) {
xAxisIndex = i;
break;
}
}
const output = [];
for (let i = 0; i < this._chartData.dataPoints.length; i++) {
output.push(this._chartData.dataPoints[i][xAxisIndex]);
}
sendCachedDataMessage(MessageTypes.LogInfo, output);
});
}
}
export function isDarkTheme(theme: PortalTheme) {
return theme === PortalTheme.dark;
}
export function handleMessage(event: MessageEvent) {
if (isInvalidParentFrameOrigin(event)) {
return;
}
if (typeof event.data !== "object" || event.data["signature"] !== "pcIframe") {
return;
}
if (
typeof event.data.data !== "object" ||
!("chartData" in event.data.data) ||
!("chartSettings" in event.data.data)
) {
return;
}
Plotly.purge(Heatmap.elementId);
document.getElementById(Heatmap.elementId)!.innerHTML = "";
const data = event.data.data;
const chartData: DataPayload = data.chartData;
const chartSettings: HeatmapCaptions = data.chartSettings;
const chartTheme: PortalTheme = data.theme;
if (Object.keys(chartData).length) {
new Heatmap(chartData, chartSettings, chartTheme).drawHeatmap();
} else {
const chartTitleElement = document.createElement("div");
chartTitleElement.innerHTML = data.chartSettings.chartTitle;
chartTitleElement.classList.add("chartTitle");
const noDataMessageElement = document.createElement("div");
noDataMessageElement.classList.add("noDataMessage");
const noDataMessageContent = document.createElement("div");
noDataMessageContent.innerHTML = data.errorMessage;
noDataMessageElement.appendChild(noDataMessageContent);
if (isDarkTheme(chartTheme)) {
chartTitleElement.classList.add("dark-theme");
noDataMessageElement.classList.add("dark-theme");
noDataMessageContent.classList.add("dark-theme");
}
document.getElementById(Heatmap.elementId)!.appendChild(chartTitleElement);
document.getElementById(Heatmap.elementId)!.appendChild(noDataMessageElement);
}
}
window.addEventListener("message", handleMessage, false);
sendReadyMessage();

View File

@@ -1,106 +0,0 @@
type dataPoint = string | number;
export interface DataPayload {
[id: string]: PartitionTimeStampToData;
}
export enum PortalTheme {
blue = 1,
azure,
light,
dark,
}
export interface HeatmapData {
yAxisPoints: string[];
xAxisPoints: string[];
dataPoints: dataPoint[][];
}
export interface HeatmapCaptions {
chartTitle: string;
yAxisTitle: string;
tooltipText: string;
timeWindow: number;
}
export interface FontSettings {
family: string;
size: number;
color: string;
}
export interface LayoutSettings {
paper_bgcolor?: string;
plot_bgcolor?: string;
margin?: {
l: number;
r: number;
b: number;
t: number;
pad: number;
};
width?: number;
height?: number;
yaxis?: {
fixedrange: boolean;
title: HeatmapCaptions["yAxisTitle"];
titlefont: FontSettings;
autorange: boolean;
showgrid: boolean;
zeroline: boolean;
showline: boolean;
autotick: boolean;
ticks: "";
showticklabels: boolean;
};
xaxis?: {
fixedrange: boolean;
title: string;
titlefont: FontSettings;
autorange: boolean;
showgrid: boolean;
zeroline: boolean;
showline: boolean;
autotick: boolean;
showticklabels: boolean;
tickformat: string;
tickfont: FontSettings;
};
title?: {
text: HeatmapCaptions["chartTitle"];
x: number;
font?: FontSettings;
};
font?: FontSettings;
}
export interface ChartSettings {
z: HeatmapData["dataPoints"];
type: "heatmap";
zmin: number;
zmid: number;
zmax: number;
colorscale: [number, string][];
name: string;
hovertemplate: HeatmapCaptions["tooltipText"];
colorbar: {
thickness: number;
outlinewidth: number;
tickcolor: string;
tickfont: FontSettings;
};
y: HeatmapData["yAxisPoints"];
x: HeatmapData["xAxisPoints"];
}
export interface DisplaySettings {
displayModeBar: boolean;
responsive?: boolean;
}
export interface PartitionTimeStampToData {
[timeSeriesDates: string]: {
[NormalizedThroughput: string]: number;
};
}

View File

@@ -103,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: () => {

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

@@ -13,7 +13,7 @@ import {
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
import { useDatabases } from "Explorer/useDatabases";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
import * as React from "react";
import DiscardIcon from "../../../../images/discard.svg";
@@ -188,13 +188,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
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 =
@@ -1074,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,
});
@@ -1094,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) {
@@ -1149,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,
@@ -1214,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 = {
@@ -1342,7 +1342,7 @@ 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} />,

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

@@ -161,7 +161,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
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>

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,22 +376,26 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
toolTipElement={getToolTipContainer(this.props.infoBubbleText)}
/>
</Label>
{this.overrideWithProvisionedThroughputSettings() && (
<MessageBar
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
styles={messageBarStyles}
>
{manualToAutoscaleDisclaimerElement}
</MessageBar>
{!this.props.isGlobalSecondaryIndex && (
<>
{this.overrideWithProvisionedThroughputSettings() && (
<MessageBar
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
styles={messageBarStyles}
>
{manualToAutoscaleDisclaimerElement}
</MessageBar>
)}
<ChoiceGroup
selectedKey={this.props.isAutoPilotSelected.toString()}
options={this.options}
onChange={this.onChoiceGroupChange}
required={this.props.showAsMandatory}
ariaLabelledBy={labelId}
styles={getChoiceGroupStyles(this.props.wasAutopilotOriginallySet, this.props.isAutoPilotSelected, true)}
/>
</>
)}
<ChoiceGroup
selectedKey={this.props.isAutoPilotSelected.toString()}
options={this.options}
onChange={this.onChoiceGroupChange}
required={this.props.showAsMandatory}
ariaLabelledBy={labelId}
styles={getChoiceGroupStyles(this.props.wasAutopilotOriginallySet, this.props.isAutoPilotSelected, true)}
/>
</Stack>
);
};
@@ -554,26 +559,81 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
private getThroughputTextField = (): JSX.Element => (
<>
{this.props.isAutoPilotSelected ? (
<TextField
label="Maximum RU/s required by this resource"
required
type="number"
id="autopilotInput"
key="auto pilot throughput input"
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
disabled={this.overrideWithProvisionedThroughputSettings()}
step={AutoPilotUtils.autoPilotIncrementStep}
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
onChange={this.onAutoPilotThroughputChange}
min={autoPilotThroughput1K}
onGetErrorMessage={(value: string) => {
const sanitizedValue = getSanitizedInputValue(value);
return sanitizedValue % 1000
? "Throughput value must be in increments of 1000"
: this.props.throughputError;
}}
validateOnLoad={false}
/>
<Stack horizontal verticalAlign="end" tokens={{ childrenGap: 8 }}>
{/* Column 1: Minimum RU/s */}
<Stack tokens={{ childrenGap: 4 }}>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
Minimum RU/s
</Text>
<FontIcon iconName="Info" style={{ fontSize: 12, color: "#666" }} />
</Stack>
<Text
style={{
fontFamily: "Segoe UI",
width: 70,
height: 28,
border: "none",
fontSize: 14,
backgroundColor: "transparent",
fontWeight: 400,
display: "flex",
alignItems: "center",
justifyContent: "center",
boxSizing: "border-box",
}}
>
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)}
</Text>
</Stack>
{/* Column 2: "x 10 =" Text */}
<Text
style={{
fontFamily: "Segoe UI",
fontSize: 12,
fontWeight: 400,
paddingBottom: 6,
}}
>
x 10 =
</Text>
{/* Column 3: Maximum 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>
<FontIcon iconName="Info" style={{ fontSize: 12, color: "#666" }} />
</Stack>
<TextField
required
type="number"
id="autopilotInput"
key="auto pilot throughput input"
styles={{
...getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline),
fieldGroup: { width: 100, height: 28 },
field: { fontSize: 14, fontWeight: 400 },
}}
disabled={this.overrideWithProvisionedThroughputSettings()}
step={AutoPilotUtils.autoPilotIncrementStep}
value={
this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()
}
onChange={this.onAutoPilotThroughputChange}
min={autoPilotThroughput1K}
onGetErrorMessage={(value: string) => {
const sanitizedValue = getSanitizedInputValue(value);
return sanitizedValue % 1000
? "Throughput value must be in increments of 1000"
: this.props.throughputError;
}}
validateOnLoad={false}
/>
</Stack>
</Stack>
) : (
<TextField
required

View File

@@ -157,35 +157,148 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
}
}
>
<StyledTextFieldBase
disabled={true}
id="autopilotInput"
key="auto pilot throughput input"
label="Maximum RU/s required by this resource"
min={1000}
onChange={[Function]}
onGetErrorMessage={[Function]}
required={true}
step={1000}
styles={
<Stack
horizontal={true}
tokens={
{
"fieldGroup": {
"borderColor": "",
"height": 25,
"selectors": {
":disabled": {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
"childrenGap": 8,
}
}
type="number"
validateOnLoad={false}
value=""
/>
verticalAlign="end"
>
<Stack
tokens={
{
"childrenGap": 4,
}
}
>
<Stack
horizontal={true}
tokens={
{
"childrenGap": 4,
}
}
verticalAlign="center"
>
<Text
style={
{
"fontWeight": 600,
"lineHeight": "20px",
}
}
variant="small"
>
Minimum RU/s
</Text>
<FontIcon
iconName="Info"
style={
{
"color": "#666",
"fontSize": 12,
}
}
/>
</Stack>
<Text
style={
{
"alignItems": "center",
"backgroundColor": "transparent",
"border": "none",
"boxSizing": "border-box",
"display": "flex",
"fontFamily": "Segoe UI",
"fontSize": 14,
"fontWeight": 400,
"height": 28,
"justifyContent": "center",
"width": 70,
}
}
>
400
</Text>
</Stack>
<Text
style={
{
"fontFamily": "Segoe UI",
"fontSize": 12,
"fontWeight": 400,
"paddingBottom": 6,
}
}
>
x 10 =
</Text>
<Stack
tokens={
{
"childrenGap": 4,
}
}
>
<Stack
horizontal={true}
tokens={
{
"childrenGap": 4,
}
}
verticalAlign="center"
>
<Text
style={
{
"fontWeight": 600,
"lineHeight": "20px",
}
}
variant="small"
>
Maximum RU/s
</Text>
<FontIcon
iconName="Info"
style={
{
"color": "#666",
"fontSize": 12,
}
}
/>
</Stack>
<StyledTextFieldBase
disabled={true}
id="autopilotInput"
key="auto pilot throughput input"
min={1000}
onChange={[Function]}
onGetErrorMessage={[Function]}
required={true}
step={1000}
styles={
{
"field": {
"fontSize": 14,
"fontWeight": 400,
},
"fieldGroup": {
"height": 28,
"width": 100,
},
}
}
type="number"
validateOnLoad={false}
value=""
/>
</Stack>
</Stack>
<Stack>
<Stack>
<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;
@@ -165,7 +166,7 @@ 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:

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,

View File

@@ -71,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]}
@@ -152,8 +163,18 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyProperties": [
"partitionKey",
],
"rawDataModel": {
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
}
@@ -177,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"
@@ -273,8 +320,18 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyProperties": [
"partitionKey",
],
"rawDataModel": {
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
}
@@ -403,8 +460,18 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyProperties": [
"partitionKey",
],
"rawDataModel": {
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
}

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,33 +44,42 @@ 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 (
<Text variant="small">
Estimated monthly cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
<b>
{currencySign + calculateEstimateNumber(monthlyPrice / 10)} -{" "}
{currencySign + calculateEstimateNumber(monthlyPrice)}{" "}
</b>
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits / 10} - {requestUnits}{" "}
RU/s, {currencySign + pricePerRu}/RU)
</Text>
<Stack style={{ marginBottom: 6 }}>
<Text variant="small">
{estimatedMonthlyCost} ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
<b>
{currencySign + calculateEstimateNumber(monthlyPrice / 10)} -{" "}
{currencySign + calculateEstimateNumber(monthlyPrice)}{" "}
</b>
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits / 10} - {requestUnits}{" "}
RU/s, {currencySign + pricePerRu}/RU)
</Text>
</Stack>
);
}
return (
<Text variant="small">
Estimated cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
<b>
{currencySign + calculateEstimateNumber(hourlyPrice)} hourly /{" "}
{currencySign + calculateEstimateNumber(dailyPrice)} daily /{" "}
{currencySign + calculateEstimateNumber(monthlyPrice)} monthly{" "}
</b>
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "}
{currencySign + pricePerRu}/RU)
</Text>
<Stack style={{ marginBottom: 8 }}>
<Text variant="small">
Estimated cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
<b>
{currencySign + calculateEstimateNumber(hourlyPrice)} hourly /{" "}
{currencySign + calculateEstimateNumber(dailyPrice)} daily /{" "}
{currencySign + calculateEstimateNumber(monthlyPrice)} monthly{" "}
</b>
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "}
{currencySign + pricePerRu}/RU)
</Text>
</Stack>
);
};

View File

@@ -1,15 +1,16 @@
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";
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
import * as SharedConstants from "../../../Shared/Constants";
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";
export interface ThroughputInputProps {
@@ -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,
@@ -38,7 +41,9 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
let defaultThroughput: number;
const workloadType: Constants.WorkloadType = getWorkloadType();
if (
if (isFabricNative()) {
defaultThroughput = AutoPilotUtils.autoPilotThroughput5K;
} else if (
isFreeTier ||
isQuickstart ||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
@@ -193,88 +198,127 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
</Text>
<InfoTooltip>{PricingUtils.getRuToolTipText()}</InfoTooltip>
</Stack>
{!isGlobalSecondaryIndex && (
<Stack horizontal verticalAlign="center">
<div role="radiogroup">
<input
id="Autoscale-input"
className="throughputInputRadioBtn"
aria-label={`${getThroughputLabelText()} Autoscale`}
aria-required={true}
checked={isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Autoscale")}
/>
<label htmlFor="Autoscale-input" className="throughputInputRadioBtnLabel">
Autoscale
</label>
<Stack horizontal verticalAlign="center">
<div role="radiogroup">
<input
id="Autoscale-input"
className="throughputInputRadioBtn"
aria-label={`${getThroughputLabelText()} Autoscale`}
aria-required={true}
checked={isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Autoscale")}
/>
<label htmlFor="Autoscale-input" className="throughputInputRadioBtnLabel">
Autoscale
</label>
<input
id="Manual-input"
className="throughputInputRadioBtn"
aria-label={`${getThroughputLabelText()} Manual`}
checked={!isAutoscaleSelected}
type="radio"
aria-required={true}
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Manual")}
/>
<label className="throughputInputRadioBtnLabel" htmlFor="Manual-input">
Manual
</label>
</div>
</Stack>
<input
id="Manual-input"
className="throughputInputRadioBtn"
aria-label={`${getThroughputLabelText()} Manual`}
checked={!isAutoscaleSelected}
type="radio"
aria-required={true}
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Manual")}
/>
<label className="throughputInputRadioBtnLabel" htmlFor="Manual-input">
Manual
</label>
</div>
</Stack>
)}
{isAutoscaleSelected && (
<Stack className="throughputInputSpacing">
<Text variant="small" aria-label="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"
>
capacity calculator
</Link>
.
<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",
}}
>
{Math.round(throughput / 10).toString()}
</Text>
</Stack>
<Stack horizontal>
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }} aria-label="maxRUDescription">
{isDatabase ? "Database" : getCollectionName()} Max RU/s
<Text
style={{
fontFamily: "Segoe UI",
fontSize: 12,
fontWeight: 400,
paddingBottom: 6,
}}
>
x 10 =
</Text>
<InfoTooltip>{getAutoScaleTooltip()}</InfoTooltip>
<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: 100, height: 27, flexShrink: 0 },
field: { fontSize: 14, fontWeight: 400 },
}}
onChange={(_event, newInput?: string) => onThroughputValueChange(newInput)}
step={AutoPilotUtils.autoPilotIncrementStep}
min={AutoPilotUtils.autoPilotThroughput1K}
max={isSharded ? Number.MAX_SAFE_INTEGER.toString() : "10000"}
value={throughput.toString()}
ariaLabel={`${isDatabase ? "Database" : getCollectionName()} max RU/s`}
required={true}
errorMessage={throughputError}
/>
</Stack>
</Stack>
<TextField
id="autoscaleRUValueField"
type="number"
styles={{
fieldGroup: { width: 300, height: 27 },
field: { fontSize: 12 },
}}
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
step={AutoPilotUtils.autoPilotIncrementStep}
min={AutoPilotUtils.autoPilotThroughput1K}
max={isSharded ? Number.MAX_SAFE_INTEGER.toString() : "10000"}
value={throughput.toString()}
ariaLabel={`${isDatabase ? "Database" : getCollectionName()} max RU/s`}
required={true}
errorMessage={throughputError}
/>
<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.
</Text>
<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>
)}
@@ -298,7 +342,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
</Text>
<InfoTooltip>{getAutoScaleTooltip()}</InfoTooltip>
</Stack>
<TooltipHost
directionalHint={DirectionalHint.topLeftEdge}
content={
@@ -323,11 +366,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

@@ -1,3 +1,4 @@
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
import { createCollection } from "../../Common/dataAccess/createCollection";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { createDocument as createMongoDocument } from "../../Common/MongoProxyClient";
@@ -90,12 +91,13 @@ export class ContainerSampleGenerator {
}
const { databaseAccount: account } = userContext;
const databaseId = collection.databaseId;
const gremlinClient = new GremlinClient();
gremlinClient.initialize({
endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`,
databaseId: databaseId,
collectionId: collection.id(),
masterKey: userContext.masterKey || "",
password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
maxResultSize: 100,
});

View File

@@ -8,10 +8,17 @@ import { MessageTypes } from "Contracts/ExplorerContracts";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { IGalleryItem } from "Juno/JunoClient";
import { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil";
import {
isFabricMirrored,
isFabricMirroredKey,
isFabricNative,
scheduleRefreshFabricToken,
} from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
import { featureRegistered } from "Utils/FeatureRegistrationUtils";
import { getVSCodeUrl } from "Utils/VSCodeExtensionUtils";
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as ko from "knockout";
@@ -30,6 +37,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";
@@ -282,6 +290,36 @@ export default class Explorer {
}
}
public openInVsCode(): void {
const vscodeUrl = getVSCodeUrl();
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 (
<>
@@ -1109,7 +1149,10 @@ export default class Explorer {
? this.refreshDatabaseForResourceToken()
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow
}
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
if (!isFabricNative()) {
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
}
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
const isNotebookEnabled =
@@ -1131,6 +1174,11 @@ export default class Explorer {
await this.initNotebooks(userContext.databaseAccount);
}
if (userContext.authType === AuthType.AAD && userContext.apiType === "SQL" && !isFabricNative()) {
const throughputBucketsEnabled = await featureRegistered(userContext.subscriptionId, "ThroughputBucketing");
updateUserContext({ throughputBucketsEnabled });
}
this.refreshSampleData();
}

View File

@@ -163,8 +163,7 @@ describe("GraphExplorer", () => {
graphBackendEndpoint: "graphBackendEndpoint",
databaseId: "databaseId",
collectionId: "collectionId",
masterKey: "masterKey",
password: "password",
onLoadStartKey: 0,
onLoadStartKeyChange: (newKey: number): void => {},
resourceId: "resourceId",

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";
@@ -54,7 +59,7 @@ export interface GraphExplorerProps {
graphBackendEndpoint: string;
databaseId: string;
collectionId: string;
masterKey: string;
password: string;
onLoadStartKey: number;
onLoadStartKeyChange: (newKey: number) => void;
@@ -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);
}
}
@@ -1292,7 +1300,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
endpoint: `wss://${this.props.graphBackendEndpoint}`,
databaseId: this.props.databaseId,
collectionId: this.props.collectionId,
masterKey: this.props.masterKey,
password: this.props.password,
maxResultSize: GraphExplorer.MAX_RESULT_SIZE,
});
}

View File

@@ -8,28 +8,28 @@ describe("Gremlin Client", () => {
endpoint: null,
collectionId: null,
databaseId: null,
masterKey: null,
maxResultSize: 10000,
password: null,
};
it("should use databaseId, collectionId and masterKey to authenticate", () => {
it("should use databaseId, collectionId and password to authenticate", () => {
const collectionId = "collectionId";
const databaseId = "databaseId";
const masterKey = "masterKey";
const testPassword = "password";
const gremlinClient = new GremlinClient();
gremlinClient.initialize({
endpoint: null,
collectionId,
databaseId,
masterKey,
maxResultSize: 0,
password: testPassword,
});
// User must includes these values
expect(gremlinClient.client.params.user.indexOf(collectionId)).not.toBe(-1);
expect(gremlinClient.client.params.user.indexOf(databaseId)).not.toBe(-1);
expect(gremlinClient.client.params.password).toEqual(masterKey);
expect(gremlinClient.client.params.password).toEqual(testPassword);
});
it("should aggregate RU charges across multiple responses", (done) => {

View File

@@ -11,8 +11,8 @@ export interface GremlinClientParameters {
endpoint: string;
databaseId: string;
collectionId: string;
masterKey: string;
maxResultSize: number;
password: string;
}
export interface GremlinRequestResult {
@@ -43,7 +43,7 @@ export class GremlinClient {
this.client = new GremlinSimpleClient({
endpoint: params.endpoint,
user: `/dbs/${params.databaseId}/colls/${params.collectionId}`,
password: params.masterKey,
password: params.password,
successCallback: (result: Result) => {
this.storePendingResult(result);
this.flushResult(result.requestId);

View File

@@ -5,11 +5,11 @@
import * as sinon from "sinon";
import {
GremlinRequestMessage,
GremlinResponseMessage,
GremlinSimpleClient,
GremlinSimpleClientParameters,
Result,
GremlinRequestMessage,
GremlinResponseMessage,
} from "./GremlinSimpleClient";
describe("Gremlin Simple Client", () => {

View File

@@ -95,3 +95,10 @@
white-space: nowrap;
}
}
@media (max-width: 768px) {
.newVertexComponent {
padding: 0;
width: 100%;
}
}

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

@@ -188,6 +188,11 @@ function openPane(action: ActionContracts.OpenPane, explorer: Explorer) {
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection]
) {
explorer.onNewCollectionClicked();
} else if (
action.paneKind === ActionContracts.PaneKind.QuickStart ||
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.QuickStart]
) {
explorer.onNewCollectionClicked({ isQuickstart: true });
} else if (
action.paneKind === ActionContracts.PaneKind.CassandraAddCollection ||
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection]

View File

@@ -25,7 +25,7 @@ import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullT
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
import {
AllPropertiesIndexed,
AnalyticalStorageContent,
AnalyticalStoreHeader,
ContainerVectorPolicyTooltipContent,
FullTextPolicyDefault,
getPartitionKey,
@@ -49,14 +49,10 @@ 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 { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { CollapsibleSectionComponent } from "../../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import { ContainerSampleGenerator } from "../../DataSamples/ContainerSampleGenerator";
@@ -110,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);
@@ -126,7 +123,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
isSharded: userContext.apiType !== "Tables",
partitionKey: getPartitionKey(props.isQuickstart),
subPartitionKeys: [],
enableDedicatedThroughput: false,
enableDedicatedThroughput: isFabricNative(), // Dedicated throughput is only enabled in Fabric Native by default
createMongoWildCardIndex:
isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport"),
useHashV1: false,
@@ -144,6 +141,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
fullTextIndexes: [],
fullTextPolicyValidated: true,
};
this.showFullTextSearch = userContext.apiType === "SQL";
}
componentDidMount(): void {
@@ -266,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">
@@ -337,7 +336,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
size={40}
className="panelTextField"
aria-label="New database id, Type a new database id"
autoFocus
tabIndex={0}
value={this.state.newDatabaseId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
@@ -408,12 +406,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
responsiveMode={999}
/>
)}
<Separator className="panelSeparator" />
<Separator className="panelSeparator" style={{ marginTop: -4, marginBottom: -4 }} />
</Stack>
)}
<Stack>
<Stack horizontal>
<Stack horizontal style={{ marginTop: -5, marginBottom: 1 }}>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{`${getCollectionName()} id`}
@@ -450,11 +448,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
this.setState({ collectionId: event.target.value })
}
/>
<Separator className="panelSeparator" style={{ marginTop: -5, marginBottom: -5 }} />
</Stack>
{this.shouldShowIndexingOptionsForFreeTierAccount() && (
<Stack>
<Stack horizontal>
<Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Indexing
@@ -500,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
@@ -556,7 +555,7 @@ 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">
{getPartitionKeyName()}
@@ -600,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",
@@ -646,7 +645,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack>
);
})}
{!isFabricNative() && userContext.apiType === "SQL" && (
{userContext.apiType === "SQL" && (
<Stack className="panelGroupSpacing">
<DefaultButton
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
@@ -668,6 +667,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)}
</Stack>
)}
<Separator className="panelSeparator" style={{ marginTop: 2, marginBottom: -4 }} />
</Stack>
)}
@@ -709,7 +709,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack>
)}
{this.shouldShowCollectionThroughputInput() && (
{this.shouldShowCollectionThroughputInput() && !isFabricNative() && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={false}
@@ -728,7 +728,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)}
{!isFabricNative() && userContext.apiType === "SQL" && (
<Stack>
<Stack style={{ marginTop: -2, marginBottom: -4 }}>
{UniqueKeysHeader()}
{this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => {
return (
@@ -742,7 +742,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
: "Comma separated paths e.g. /firstName,/address/zipCode"
}
className="panelTextField"
autoFocus
value={uniqueKey}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const uniqueKeys = this.state.uniqueKeys.map((uniqueKey: string, j: number) => {
@@ -777,10 +776,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack>
)}
{!isFabricNative() && userContext.apiType === "SQL" && (
<Separator className="panelSeparator" style={{ marginTop: -15, marginBottom: -4 }} />
)}
{shouldShowAnalyticalStoreOptions() && (
<Stack className="panelGroupSpacing">
<Stack className="panelGroupSpacing" style={{ marginTop: -4 }}>
<Text className="panelTextBold" variant="small">
{AnalyticalStorageContent()}
{AnalyticalStoreHeader()}
</Text>
<Stack horizontal verticalAlign="center">
@@ -821,7 +824,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<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"
@@ -1131,7 +1134,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
// }
private shouldShowCollectionThroughputInput(): boolean {
if (isFabricNative() || isServerlessAccount()) {
if (isServerlessAccount()) {
return false;
}
@@ -1161,7 +1164,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
private shouldShowFullTextSearchParameters() {
return isFullTextSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
return !isFabricNative() && this.showFullTextSearch;
}
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
@@ -1316,7 +1319,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
};
}
if (this.shouldShowFullTextSearchParameters()) {
if (this.showFullTextSearch) {
indexingPolicy.fullTextIndexes = this.state.fullTextIndexes;
}
@@ -1350,7 +1353,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 5K
autoPilotMaxThroughput = AutoPilotUtils.autoPilotThroughput5K;
offerThroughput = undefined;
} else if (databaseLevelThroughput) {
if (this.state.createNewDatabase) {
if (this.isNewDatabaseAutoscale) {
autoPilotMaxThroughput = this.newDatabaseThroughput;

View File

@@ -73,7 +73,7 @@ export function UniqueKeysHeader(): JSX.Element {
"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>
<Stack horizontal style={{ marginBottom: -2 }}>
<Text className="panelTextBold" variant="small">
Unique keys
</Text>
@@ -98,6 +98,21 @@ export function shouldShowAnalyticalStoreOptions(): boolean {
}
}
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">

View File

@@ -11,6 +11,11 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
>
<Stack
hidden={false}
style={
{
"marginBottom": -2,
}
}
>
<Stack
horizontal={true}
@@ -88,7 +93,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
aria-label="New database id, Type a new database id"
aria-required={true}
autoComplete="off"
autoFocus={true}
className="panelTextField"
id="newDatabaseId"
name="newDatabaseId"
@@ -140,11 +144,23 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
</Stack>
<Separator
className="panelSeparator"
style={
{
"marginBottom": -4,
"marginTop": -4,
}
}
/>
</Stack>
<Stack>
<Stack
horizontal={true}
style={
{
"marginBottom": 1,
"marginTop": -5,
}
}
>
<span
className="mandatoryStar"
@@ -186,10 +202,25 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
type="text"
value=""
/>
<Separator
className="panelSeparator"
style={
{
"marginBottom": -5,
"marginTop": -5,
}
}
/>
</Stack>
<Stack>
<Stack
horizontal={true}
style={
{
"marginBottom": -4,
"marginTop": -5,
}
}
>
<span
className="mandatoryStar"
@@ -254,6 +285,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 +303,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 +358,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"
>
<Text
variant="small"
<Stack
horizontal={true}
style={
{
"marginBottom": -2,
}
}
>
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"
<Text
className="panelTextBold"
variant="small"
>
Learn more
</StyledLinkBase>
</Text>
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}
@@ -381,8 +460,8 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
Azure Synapse Link is required for creating an analytical store
container
. Enable Synapse Link for this Cosmos DB account.
. Enable Synapse Link for this Cosmos DB account.
<br />
<StyledLinkBase
aria-label="Learn more about Azure Synapse Link."
className="capacitycalculator-link"
@@ -411,6 +490,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

@@ -22,7 +22,6 @@ import {
FullTextPolicyDefault,
getPartitionKey,
isSynapseLinkEnabled,
parseUniqueKeys,
scrollToSection,
shouldShowAnalyticalStoreOptions,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
@@ -35,19 +34,18 @@ import { AnalyticalStoreComponent } from "Explorer/Panes/AddGlobalSecondaryIndex
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 { UniqueKeysComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/UniqueKeysComponent";
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, { useEffect, useState } from "react";
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 { isFullTextSearchEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
export interface AddGlobalSecondaryIndexPanelProps {
@@ -66,18 +64,19 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
const [useHashV1, setUseHashV1] = useState<boolean>();
const [enableDedicatedThroughput, setEnabledDedicatedThroughput] = useState<boolean>();
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>();
const [uniqueKeys, setUniqueKeys] = useState<string[]>([]);
const [enableAnalyticalStore, setEnableAnalyticalStore] = useState<boolean>();
const [vectorEmbeddingPolicy, setVectorEmbeddingPolicy] = useState<VectorEmbedding[]>();
const [vectorIndexingPolicy, setVectorIndexingPolicy] = useState<VectorIndex[]>();
const [vectorPolicyValidated, setVectorPolicyValidated] = 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>();
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) => {
@@ -109,17 +108,12 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
}, [errorMessage]);
let globalSecondaryIndexThroughput: number;
let isGlobalSecondaryIndexAutoscale: boolean;
let isCostAcknowledged: boolean;
const globalSecondaryIndexThroughputOnChange = (globalSecondaryIndexThroughputValue: number): void => {
globalSecondaryIndexThroughput = globalSecondaryIndexThroughputValue;
};
const isGlobalSecondaryIndexAutoscaleOnChange = (isGlobalSecondaryIndexAutoscaleValue: boolean): void => {
isGlobalSecondaryIndexAutoscale = isGlobalSecondaryIndexAutoscaleValue;
};
const isCostAknowledgedOnChange = (isCostAcknowledgedValue: boolean): void => {
isCostAcknowledged = isCostAcknowledgedValue;
};
@@ -148,10 +142,6 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
return isVectorSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput());
};
const showFullTextSearchParameters = (): boolean => {
return isFullTextSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput());
};
const getAnalyticalStorageTtl = (): number => {
if (!isSynapseLinkEnabled()) {
return undefined;
@@ -178,18 +168,11 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
}
if (globalSecondaryIndexThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
const errorMessage = isGlobalSecondaryIndexAutoscale
? "Please acknowledge the estimated monthly spend."
: "Please acknowledge the estimated daily spend.";
const errorMessage: string = "Please acknowledge the estimated monthly spend.";
setErrorMessage(errorMessage);
return false;
}
if (globalSecondaryIndexThroughput > CollectionCreation.MaxRUPerPartition) {
setErrorMessage("Unsharded collections support up to 10,000 RUs");
return false;
}
if (showVectorSearchParameters()) {
if (!vectorPolicyValidated) {
setErrorMessage("Please fix errors in container vector policy");
@@ -221,7 +204,6 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
const partitionKeyTrimmed: string = partitionKey.trim();
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(uniqueKeys);
const partitionKeyVersion = useHashV1 ? undefined : 2;
const partitionKeyPaths: DataModels.PartitionKey = partitionKeyTrimmed
? {
@@ -244,7 +226,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
};
}
if (showFullTextSearchParameters()) {
if (showFullTextSearch) {
indexingPolicy.fullTextIndexes = fullTextIndexes;
}
@@ -256,9 +238,8 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
collection: {
id: globalSecondaryIdTrimmed,
throughput: globalSecondaryIndexThroughput,
isAutoscale: isGlobalSecondaryIndexAutoscale,
isAutoscale: true,
partitionKeyPaths,
uniqueKeyPolicy,
collectionWithDedicatedThroughput: enableDedicatedThroughput,
},
subscriptionQuotaId: userContext.quotaId,
@@ -268,28 +249,17 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, telemetryData);
const databaseLevelThroughput: boolean = isSelectedSourceContainerSharedThroughput() && !enableDedicatedThroughput;
let offerThroughput: number;
let autoPilotMaxThroughput: number;
if (!databaseLevelThroughput) {
if (isGlobalSecondaryIndexAutoscale) {
autoPilotMaxThroughput = globalSecondaryIndexThroughput;
} else {
offerThroughput = globalSecondaryIndexThroughput;
}
}
const createGlobalSecondaryIndexParams: DataModels.CreateMaterializedViewsParams = {
materializedViewId: globalSecondaryIdTrimmed,
materializedViewDefinition: globalSecondaryIndexDefinition,
databaseId: selectedSourceContainer.databaseId,
databaseLevelThroughput: databaseLevelThroughput,
offerThroughput: offerThroughput,
autoPilotMaxThroughput: autoPilotMaxThroughput,
...(!databaseLevelThroughput && {
autoPilotMaxThroughput: globalSecondaryIndexThroughput,
}),
analyticalStorageTtl: getAnalyticalStorageTtl(),
indexingPolicy: indexingPolicy,
partitionKey: partitionKeyPaths,
uniqueKeyPolicy: uniqueKeyPolicy,
vectorEmbeddingPolicy: vectorEmbeddingPolicyFinal,
fullTextPolicy: fullTextPolicy,
};
@@ -395,12 +365,10 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
isSelectedSourceContainerSharedThroughput,
showCollectionThroughputInput,
globalSecondaryIndexThroughputOnChange,
isGlobalSecondaryIndexAutoscaleOnChange,
setIsThroughputCapExceeded,
isCostAknowledgedOnChange,
}}
/>
<UniqueKeysComponent {...{ uniqueKeys, setUniqueKeys }} />
{shouldShowAnalyticalStoreOptions() && (
<AnalyticalStoreComponent {...{ explorer, enableAnalyticalStore, setEnableAnalyticalStore }} />
)}
@@ -413,10 +381,11 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
setVectorIndexingPolicy,
vectorPolicyValidated,
setVectorPolicyValidated,
isGlobalSecondaryIndex: true,
}}
/>
)}
{showFullTextSearchParameters() && (
{showFullTextSearch && (
<FullTextSearchComponent
{...{ fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated }}
/>

View File

@@ -12,7 +12,6 @@ export interface ThroughputComponentProps {
isSelectedSourceContainerSharedThroughput: () => boolean;
showCollectionThroughputInput: () => boolean;
globalSecondaryIndexThroughputOnChange: (globalSecondaryIndexThroughputValue: number) => void;
isGlobalSecondaryIndexAutoscaleOnChange: (isGlobalSecondaryIndexAutoscaleValue: boolean) => void;
setIsThroughputCapExceeded: React.Dispatch<React.SetStateAction<boolean>>;
isCostAknowledgedOnChange: (isCostAknowledgedValue: boolean) => void;
}
@@ -24,7 +23,6 @@ export const ThroughputComponent = (props: ThroughputComponentProps): JSX.Elemen
isSelectedSourceContainerSharedThroughput,
showCollectionThroughputInput,
globalSecondaryIndexThroughputOnChange,
isGlobalSecondaryIndexAutoscaleOnChange,
setIsThroughputCapExceeded,
isCostAknowledgedOnChange,
} = props;
@@ -49,15 +47,14 @@ export const ThroughputComponent = (props: ThroughputComponentProps): JSX.Elemen
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !useDatabases.getState().isFirstResourceCreated()}
isDatabase={false}
isSharded={false}
isSharded={true}
isFreeTier={isFreeTierAccount()}
isQuickstart={false}
isGlobalSecondaryIndex={true}
setThroughputValue={(throughput: number) => {
globalSecondaryIndexThroughputOnChange(throughput);
}}
setIsAutoscale={(isAutoscale: boolean) => {
isGlobalSecondaryIndexAutoscaleOnChange(isAutoscale);
}}
setIsAutoscale={() => {}}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => {
setIsThroughputCapExceeded(isThroughputCapExceeded);
}}

View File

@@ -1,78 +0,0 @@
import { ActionButton, IconButton, Stack } from "@fluentui/react";
import { UniqueKeysHeader } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
import { userContext } from "UserContext";
export interface UniqueKeysComponentProps {
uniqueKeys: string[];
setUniqueKeys: React.Dispatch<React.SetStateAction<string[]>>;
}
export const UniqueKeysComponent = (props: UniqueKeysComponentProps): JSX.Element => {
const { uniqueKeys, setUniqueKeys } = props;
const updateUniqueKeysOnChange = (value: string, uniqueKeyToReplaceIndex: number): void => {
const updatedUniqueKeys = uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number) => {
if (uniqueKeyToReplaceIndex === uniqueKeyIndex) {
return value;
}
return uniqueKey;
});
setUniqueKeys(updatedUniqueKeys);
};
const deleteUniqueKeyOnClick = (uniqueKeyToDeleteIndex: number): void => {
const updatedUniqueKeys = uniqueKeys.filter((_, uniqueKeyIndex) => uniqueKeyToDeleteIndex !== uniqueKeyIndex);
setUniqueKeys(updatedUniqueKeys);
};
const addUniqueKeyOnClick = (): void => {
setUniqueKeys([...uniqueKeys, ""]);
};
return (
<Stack>
{UniqueKeysHeader()}
{uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number): JSX.Element => {
return (
<Stack style={{ marginBottom: 8 }} key={`uniqueKey-${uniqueKeyIndex}`} horizontal>
<input
type="text"
autoComplete="off"
placeholder={
userContext.apiType === "Mongo"
? "Comma separated paths e.g. firstName,address.zipCode"
: "Comma separated paths e.g. /firstName,/address/zipCode"
}
className="panelTextField"
autoFocus
value={uniqueKey}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
updateUniqueKeysOnChange(event.target.value, uniqueKeyIndex);
}}
/>
<IconButton
iconProps={{ iconName: "Delete" }}
style={{ height: 27 }}
onClick={() => {
deleteUniqueKeyOnClick(uniqueKeyIndex);
}}
/>
</Stack>
);
})}
<ActionButton
iconProps={{ iconName: "Add" }}
styles={{ root: { padding: 0 }, label: { fontSize: 12 } }}
onClick={() => {
addUniqueKeyOnClick();
}}
>
Add unique key
</ActionButton>
</Stack>
);
};

View File

@@ -14,6 +14,7 @@ export interface VectorSearchComponentProps {
vectorIndexingPolicy: VectorIndex[];
setVectorIndexingPolicy: React.Dispatch<React.SetStateAction<VectorIndex[]>>;
setVectorPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
isGlobalSecondaryIndex?: boolean;
}
export const VectorSearchComponent = (props: VectorSearchComponentProps): JSX.Element => {
@@ -23,6 +24,7 @@ export const VectorSearchComponent = (props: VectorSearchComponentProps): JSX.El
vectorIndexingPolicy,
setVectorIndexingPolicy,
setVectorPolicyValidated,
isGlobalSecondaryIndex,
} = props;
return (
@@ -49,6 +51,7 @@ export const VectorSearchComponent = (props: VectorSearchComponentProps): JSX.El
setVectorIndexingPolicy(vectorIndexingPolicy);
setVectorPolicyValidated(vectorPolicyValidated);
}}
isGlobalSecondaryIndex={isGlobalSecondaryIndex}
/>
</Stack>
</Stack>

View File

@@ -137,16 +137,11 @@ exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
<ThroughputComponent
globalSecondaryIndexThroughputOnChange={[Function]}
isCostAknowledgedOnChange={[Function]}
isGlobalSecondaryIndexAutoscaleOnChange={[Function]}
isSelectedSourceContainerSharedThroughput={[Function]}
setEnabledDedicatedThroughput={[Function]}
setIsThroughputCapExceeded={[Function]}
showCollectionThroughputInput={[Function]}
/>
<UniqueKeysComponent
setUniqueKeys={[Function]}
uniqueKeys={[]}
/>
<AnalyticalStoreComponent
explorer={
Explorer {
@@ -177,6 +172,17 @@ exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
}
setEnableAnalyticalStore={[Function]}
/>
<FullTextSearchComponent
fullTextPolicy={
{
"defaultLanguage": "en-US",
"fullTextPaths": [],
}
}
setFullTextIndexes={[Function]}
setFullTextPolicy={[Function]}
setFullTextPolicyValidated={[Function]}
/>
<AdvancedComponent
setSubPartitionKeys={[Function]}
setUseHashV1={[Function]}

View File

@@ -11,10 +11,10 @@
margin: 20px 0;
overflow-x: hidden;
& > :not(.collapsibleSection) {
&> :not(.collapsibleSection) {
margin-bottom: @DefaultSpace;
& > :not(:last-child) {
&> :not(:last-child) {
margin-bottom: @DefaultSpace;
}
}
@@ -56,6 +56,14 @@
transform: translate(-50%, -50%);
}
}
@media (max-width: 768px) {
.panelMainContent {
padding: 0 24px;
margin: 0;
overflow-x: auto;
}
}
}
.panelHeader {
@@ -113,70 +121,87 @@
.deleteCollectionFeedback {
margin-top: 12px;
}
.addRemoveIcon {
margin-left: 4px !important;
}
.addRemoveIconLabel {
margin-top: 28px;
margin-left: 4px !important;
}
.addRemoveIcon [alt="editEntity"]:focus,
.addRemoveIconLabel [alt="editEntity"]:focus {
border: 1px dashed #605e5c;
}
.addNewParamStyle {
margin-top: 5px;
margin-left: 5px !important;
cursor: pointer;
}
.panelGroupSpacing > :not(:last-child) {
.panelGroupSpacing> :not(:last-child) {
margin-bottom: @DefaultSpace;
}
.fileUpload {
display: none !important;
}
.customFileUpload {
padding: 25px 0px 0px 10px;
cursor: pointer;
display: flex;
}
.fileIcon {
align-self: center;
}
.panelAddIconLabel {
font-size: 20px;
width: 20px;
margin: 30px 0 0 10px;
cursor: default;
}
.panelAddIcon {
font-size: 20px;
width: 20px;
margin: 30px 0 0 10px;
cursor: default;
}
.removeIcon {
color: @InfoIconColor;
}
.backImageIcon {
margin-top: 8px;
}
[alt="back"]:focus {
border: 1px solid #605e5c;
}
.addEntityDatePicker {
max-width: 145px;
}
.addEntityTextField {
width: 237px;
}
.addButtonEntiy {
width: 25%;
}
.column-select-view {
margin: 20px 0px 0px 0px;
}
.panelSeparator::before {
background-color: #edebe9;
}
}

File diff suppressed because it is too large Load Diff

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"
@@ -494,6 +495,51 @@ exports[`Settings Pane should render Default properly 1`] = `
</div>
</AccordionPanel>
</AccordionItem>
<AccordionItem
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"
>
@@ -573,6 +619,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
>
<Accordion
className="customAccordion ___1uf6361_0000000 fz7g6wx"
collapsible={true}
>
<AccordionItem
value="7"

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,44 +100,94 @@ 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,
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
const fieldContent = `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
return (
<TooltipHost
content={fieldContent}
id={`tooltip-${index}-${column.key}`}
directionalHint={DirectionalHint.bottomAutoEdge}
>
{fieldContent}
</TooltipHost>
);
},
},
];
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;
}
return (
<TooltipHost content={fieldContent} id={tooltipId} directionalHint={DirectionalHint.rightCenter}>
{fieldContent}
</TooltipHost>
);
};
return (
<RightPaneForm {...props}>
<div className="paneMainContent">
@@ -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

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

@@ -1,11 +1,9 @@
/* eslint-disable no-console */
import { Stack } from "@fluentui/react";
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
@@ -13,7 +11,6 @@ import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotRe
import { userContext } from "UserContext";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel";
import { ReactTabKind, TabsState, useTabs } from "hooks/useTabs";
import React, { useState } from "react";
import SplitterLayout from "react-splitter-layout";
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
@@ -26,7 +23,8 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
const [copilotActive, setCopilotActive] = useState<boolean>(() =>
readCopilotToggleStatus(userContext.databaseAccount),
);
const [tabActive, setTabActive] = useState<boolean>(true);
//TODO: Uncomment this useState when query copilot is reinstated in DE
// const [tabActive, setTabActive] = useState<boolean>(true);
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
@@ -70,17 +68,18 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
useCommandBar.getState().setContextButtons(getCommandbarButtons());
}, [query, selectedQuery, copilotActive]);
React.useEffect(() => {
return () => {
useTabs.subscribe((state: TabsState) => {
if (state.activeReactTab === ReactTabKind.QueryCopilot) {
setTabActive(true);
} else {
setTabActive(false);
}
});
};
}, []);
//TODO: Uncomment this effect when query copilot is reinstated in DE
// React.useEffect(() => {
// return () => {
// useTabs.subscribe((state: TabsState) => {
// if (state.activeReactTab === ReactTabKind.QueryCopilot) {
// setTabActive(true);
// } else {
// setTabActive(false);
// }
// });
// };
// }, []);
const toggleCopilot = (toggle: boolean) => {
setCopilotActive(toggle);
@@ -90,6 +89,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
return (
<Stack className="tab-pane" style={{ width: "100%" }}>
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
{/*TODO: Uncomment this section when query copilot is reinstated in DE
{tabActive && copilotActive && (
<QueryCopilotPromptbar
explorer={explorer}
@@ -97,7 +97,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
databaseId={QueryCopilotSampleDatabaseId}
containerId={QueryCopilotSampleContainerId}
></QueryCopilotPromptbar>
)}
)} */}
<Stack className="tabPaneContentContainer">
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>
<EditorReact

View File

@@ -27,11 +27,13 @@ import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/T
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";
@@ -104,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 {
@@ -275,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) {
@@ -318,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 && (
@@ -384,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

@@ -2,14 +2,13 @@
* Accordion top class
*/
import { Link, makeStyles, tokens } from "@fluentui/react-components";
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
import { DocumentAddRegular, LinkMultipleRegular, OpenRegular } from "@fluentui/react-icons";
import { SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
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 {
@@ -62,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",
@@ -111,7 +119,7 @@ const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className
}) => {
const styles = useStyles();
return (
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick}>
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick} tabIndex={0}>
<div className={styles.buttonUpperPart}>{icon}</div>
<div aria-label={title} className={styles.buttonLowerPart}>
<div>{title}</div>
@@ -139,7 +147,7 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
{
title: "Sample data",
description: "Automatically load sample data in your database",
icon: <img src={CosmosDbBlackIcon} />,
icon: <img src={CosmosDbBlackIcon} alt={"Azure Cosmos DB icon"} aria-hidden="true" />,
onClick: () => setOpenSampleDataImportDialog(true),
},
{
@@ -150,7 +158,11 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
},
];
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]} />
@@ -159,7 +171,7 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
);
};
const title = "Build your database";
const title = isFabricNativeReadOnly() ? "Use your database" : "Build your database";
return (
<>
<CosmosFluentProvider className={styles.homeContainer}>
@@ -169,16 +181,18 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
explorer={props.explorer}
databaseName={userContext.fabricContext?.databaseName}
/>
<div className={styles.title} role="heading" aria-label={title}>
<div className={styles.title} role="heading" aria-label={title} aria-level={1}>
{title}
</div>
{getSplashScreenButtons()}
<div className={styles.footer}>
Need help?{" "}
<Link href="https://aka.ms/cosmosdbfabricdocs" target="_blank">
Learn more <img src={LinkIcon} alt="Learn more" />
</Link>
</div>
{
<div className={styles.footer}>
Need help?{" "}
<Link href="https://learn.microsoft.com/fabric/database/cosmos-db/overview" target="_blank">
Learn more <OpenRegular />
</Link>
</div>
}
</CosmosFluentProvider>
</>
);

View File

@@ -1,3 +1,4 @@
import { BackendDefaults } from "Common/Constants";
import { createCollection } from "Common/dataAccess/createCollection";
import Explorer from "Explorer/Explorer";
import { useDatabases } from "Explorer/useDatabases";
@@ -35,6 +36,11 @@ export const createContainer = async (
collectionId: containerName,
databaseId: databaseName,
databaseLevelThroughput: false,
partitionKey: {
paths: [`/${SAMPLE_DATA_PARTITION_KEY}`],
kind: "Hash",
version: BackendDefaults.partitionKeyVersion,
},
};
await createCollection(createRequest);
await explorer.refreshAllDatabases();
@@ -47,6 +53,8 @@ export const createContainer = async (
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(

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