Compare commits

...

81 Commits

Author SHA1 Message Date
Ashley Stanton-Nurse
48950613a7 some wordsmithing 2024-06-07 13:53:21 -07:00
Ashley Stanton-Nurse
2bd913acbb when opening hosted explorer from portal, jump to selected sub/account 2024-06-07 12:05:46 -07:00
Ashley Stanton-Nurse
4866d3c902 stuck in a redirect loop, need to work that out 2024-06-07 11:03:20 -07:00
Laurent Nguyen
06d4829422 Fix click to show document issue. Table doesn't auto-select first document anymore. (#1864) 2024-06-07 16:28:08 +02:00
Ashley Stanton-Nurse
416743c548 stop tree nodes from expanding to full height, causing overlap (#1861) 2024-06-06 09:33:25 -07:00
Ashley Stanton-Nurse
b5d4509d49 fix a broken test snapshot (#1863) 2024-06-05 13:40:51 -07:00
Ashley Stanton-Nurse
417ef899f0 Update Playwright, improve E2E test reliability, add scripts to deploy test resources (#1857) 2024-06-05 12:46:32 -07:00
Ashley Stanton-Nurse
736731474f show an expand icon for nodes with non-null children arrray (#1862) 2024-06-05 12:27:29 -07:00
Ashley Stanton-Nurse
9b12775151 Allow query result view to be toggled from command bar (#1833)
* allow query result view to be toggled from command bar

also provides a default results view option that's stored in the
browser's local storage

* update SettingsPane test snapshot
2024-06-05 12:16:28 -07:00
Laurent Nguyen
7002da0b51 Implement ctrl-shift click to select multiple documents (#1851)
* Initial implementation of shift and ctrl click to select

* Implement shift-ctrl selection

* Fix snapshot, update selectionHelper comment

* Fix missing type

* Properly disable cursor selection

* Update snapshots

* Do not enable (multiselect) if readonly

* Consider meta key for mac and ctrl for everything else
2024-06-05 17:47:27 +02:00
SATYA SB
7c5fb1b697 [accessibility-3102730]:[Visual Requirement - Azure Cosmos DB - Data Explorer]: Luminosity ratio of 'Learn More' link with surrounding text is less than required 3:1 under 'Data Explorer' pane. (#1847) 2024-06-05 20:46:35 +05:30
SATYA SB
06e28ae3e7 [Visual Requirement - Azure Cosmos DB - Add Table]: Luminosity ratio of links with surrounding text is less than required 3:1 under 'Add Table' pane. (#1840) 2024-06-05 20:45:55 +05:30
SATYA SB
52c2cfe419 [Accessibility-3100036]: Close button is nested under 'Home' tab control under 'Data Explorer' pane. (#1818)
* [3100036]: [Programmatic Access - Azure Cosmos DB - Data Explorer]: Close button is nested under 'Home' tab control under 'Data Explorer' pane.

* Fabric-Less updated.

* Added specific width for contentWrapper.

* less update.

* Fixed out-scope space issue

---------

Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2024-06-05 20:42:48 +05:30
Laurent Nguyen
b76d83d8e1 Disable table selection for Fabric/read-only (#1855)
* Disable table selection for Fabric/read-only

* Update unit tests

* Fix format

---------

Co-authored-by: Laurent Nguyen <languye@microsoft.com>
2024-06-03 19:11:16 +02:00
Laurent Nguyen
495296602a Fix delete documents on mongo bug (#1852) 2024-06-03 15:23:09 +02:00
Asier Isayas
96ba0a9729 Reactivate Mongo Proxy (#1850)
* Reactivate Mongo Proxy

* Reactivate Mongo Proxy

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-05-31 13:31:24 -04:00
Laurent Nguyen
6276464e0d Fix refreshgrid behaving like load more (#1853) 2024-05-31 17:57:11 +02:00
Ashley Stanton-Nurse
98c5fe65e6 Use new Fluent-based Resource Tree for all environments (#1841)
Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com>
2024-05-29 09:56:27 -07:00
Laurent Nguyen
cebf044803 Fix typo (#1849) 2024-05-29 18:20:07 +02:00
Laurent Nguyen
f669a99228 Separate Fabric-specific message types (#1848)
* Update message de->fabric to v3

* Reinstate get authorization token path which doesn't get called every 5 minutes anymore

* Remove obsolete comment

* Add missing types

* Fix format

* Fix build issue

* Revert "Reinstate get authorization token path which doesn't get called every 5 minutes anymore"

This reverts commit a3f3511043.

* Keep 3 old fabric message types enums for compatibility with the portal

* Re-add warning comment about not changing existing message type enums

---------

Co-authored-by: Laurent Nguyen <languye@microsoft.com>
2024-05-29 16:03:51 +02:00
Laurent Nguyen
36736882ee Migrate DocumentsTab to React and add bulk delete and column resize (#1770)
* Document page now loads list of docs and displays selection

* DocumentsTabV2 now properly loads documents, show partition keys and display first doc with proper selection behavior. Move it to its own folder.

* Extract table in a separate component

* Resizable columns on the document table

* Fix selection behavior and some layout issue

* Adding table scrolling

* Fix NaN height issue

* Fix NaN height issue

* Fix column sizing + cell selection

* Improvement in width size. Add Load More

* Add react editor and pass column headers

* Dynamic columns for pk

* Fix initial columns size

* Add nav buttons

* Editing content updates buttons state

* Discard and save buttons working

* Fix save new document. Implement delete.

* Remove debug display

* Fix unexpand filter and reformat

* Fix compil issues

* Add refresh button

* Update column header placeholder style

* Implement delete multiple docs

* Fix multi delete

* Fix show/hide delete button

* Fix selection behavior

* Fix UX with buttons behavior and editor display

* Fix UX issue with not discarding edit changes

* Add some TODO's

* Remove debugging info and reformat

* Add mongo support

* Fix build issues

* Fix table header. Remove debug statement

* Restore broken nosql

* Fix mongo save new document/update document

* Fix bugs with clicking on newly created documents

* Fix comment

* Fix double fetch issue when clicking on an item

* Auto-select last document when saving new document

* Fix resourceTokenPartitionKey code

* Fix format

* Fix isQueryCopilotSampleContainer flag

* Fix unused code

* Call tab when updating error flag

* Destructure props to make useEffect dependencies work

* Fix loadStartKey

* minor update

* Fix format

* Add title to table

* Fix table coming off its container with unwanted horizontal scrollbar

* Increase table width. Fix eslint issue.

* Move refresh documents button from table back to DocumentsTabV2

* Fix load more text centering

* Don't show Load More if nothing to show

* Fix columns min width

* Add keyboard shortcuts

* Add keyboard handlers to load more and refresh button

* Add keyboard support to select table row

* Disable eslint issue from fluent library

* Connect cancel query button

* Add Fluent V9 theme for Fabric (#1821)

* Clean up dependencies and memoize internal functions and object. Move methods and object that don't depend on state outside of component.

* Fix filter disappearing when clicking Apply Filter

* Fix typo and format

* Implement bulk delete for nosql

* Replace filter ui components with fluent ui

* Remove jquery calls

* Migrate unit test to DocumentsTabV2

* Remove DocumentsTab and MongoDocumentsTab. Fix build issues.

* Properly handle activetab

* Remove comments and unused code

* Port keyboard shortcuts from commitId 1f4d0f2

* Port item editor shortcuts to improved Items tab branch (#1831)

* set filter focus on Ctrl+Shift+F

* implement filter enter/esc keybinds

* remove debugging

* Collapse filter when query is executed

* Fix monaco editor not happy when parent is null

* Fix how bulk delete operation gets called when no partition key

* Fix update id list after delete

* Fix deleteDocuments

* Fix build issue

* Fix bug in mongo delete

* Fix mongo delete flow

* Proper error handling in mongo

* Handle >100 bulk delete operations

* Add unit tests for DocumentsTableComponent

* More improvements to table unit tests

* Fix import. Disable selection test for now

* Add more DocumentsTab unit react tests

* Remove selection test

* Add more unit tests. Add lcov coverage report to display in vscode

* Move unit tests to correct file

* Add unit test on command bar

* Fix build issues

* Add more unit tests

* Remove unneeded call

* Add DocumentsTab for Mongo API

* Fix linting errors

* Update fluent ui v9 dependency. Color columns separation. Fix refresh button placement to not interfere with header cell width.

* Revert @fluentui/react-components to a safe version that compiles

* Add confirmation window when documents have been deleted

* Fix mongo unit tests

* Fix format

* Update src/Common/dataAccess/deleteDocument.ts

Co-authored-by: Ashley Stanton-Nurse <ashleyst@microsoft.com>

* Update src/Common/dataAccess/deleteDocument.ts

Co-authored-by: Ashley Stanton-Nurse <ashleyst@microsoft.com>

* Update src/Common/dataAccess/deleteDocument.ts

Co-authored-by: Ashley Stanton-Nurse <ashleyst@microsoft.com>

* Fix bug with markup. Simplify code.

* Protect against creating React editor without parent node

* Replace rendering tests with snapshot match

* Add test screenshot to troubleshoot e2e test

* Revert "Add test screenshot to troubleshoot e2e test"

This reverts commit 1b8138ade0.

* Attempt 2 at troubleshooting failing test

* Revert "Attempt 2 at troubleshooting failing test"

This reverts commit 3e51a593bf.

* Delete button now shows if one or more rows are selected

---------

Co-authored-by: Vsevolod Kukol <sevoku@microsoft.com>
Co-authored-by: Ashley Stanton-Nurse <ashleyst@microsoft.com>
2024-05-29 09:09:13 +02:00
sunghyunkang1111
19d1e0d1df allow serverless accounts to have vector search embeddings (#1844) 2024-05-20 17:24:04 -07:00
sunghyunkang1111
ceeead8458 Vector search for NoSQL accounts (#1843)
* Add container vector policy and indexing policy support

* Add vector search capability

* hide vector settings for shared throughput DB

* update package-lock

* fix pipeline

* remove comments

* Address comments

* Address comments
2024-05-20 13:30:30 -05:00
sunghyunkang1111
4da3363cf7 add capacityMode (#1826)
* add capacityMode

* add check for capacityMode for serverless
2024-05-17 12:19:23 -05:00
jawelton74
ff4bc78d6c Remove preview label from Computed Properties. (#1842) 2024-05-17 06:57:35 -07:00
SATYA SB
b6e3e5ea1c Screen Reader does not announce status message after invoking 'Add Row' control under 'Add Table Row' pane. (#1837)
* [accessibility-3100026]: [Screen Reader - Azure Cosmos DB - Add Table Row]: Screen Reader does not announce status message after invoking 'Add Row' control under 'Add Table Row' pane.

* Fixed format.

* Snap update.

---------

Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2024-05-14 09:57:50 +05:30
SATYA SB
9e9d270b65 [accessibility-3102877]:[Programmatic Access - Azure CosmosDB – Data Explorer]: Ensures every ARIA input field has an accessible name. (#1835)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2024-05-14 09:56:53 +05:30
SATYA SB
f56e5e64b9 [accessibility-3102916]:[Keyboard Navigation - Azure CosmosDB - Data … (#1834)
* [accessibility-3102916]:[Keyboard Navigation - Azure CosmosDB - Data Explorer]: Keyboard focus is moving to non-interactive control after checkbox control of Advanced button.

* Updated Snap.

---------

Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2024-05-14 09:56:16 +05:30
Asier Isayas
14e5efcebf Point Mongo requests to old backend (#1838)
* point mongo requests to old backend

* point mongo requests to old backend

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-05-09 13:42:59 -04:00
Ashley Stanton-Nurse
5c3f18f5f8 add link to keyboard shortcuts doc to home tab (#1836) 2024-05-07 12:30:46 -07:00
jawelton74
6ebc48ad28 Remove some Notebooks code (#1832)
* Remove onNewNotebookClicked, openUploadFilePanel functions and
UploadFilePane.

* Remove resetNotebookWorkspace function.

* Remove Notebooks related resource tree node generation.

* Fix test snapshots.
2024-05-02 07:14:31 -07:00
jawelton74
298197b1b8 Revert "First set of changes for Notebooks removal. (#1816)" (#1830)
This reverts commit b023250e67.
2024-05-01 07:21:50 -07:00
Ashley Stanton-Nurse
81a5b7cb6d add shortcuts for the Items tab (#1827)
* add shortcuts for the Items tab

* Add shortcut to clear Items tab filter.
2024-04-30 10:03:27 -07:00
jawelton74
b023250e67 First set of changes for Notebooks removal. (#1816)
* First set of changes for Notebooks removal.

* Fix unit test snapshots.
2024-04-29 15:46:24 -07:00
Asier Isayas
92246144f7 Enable Legacy Mongo Shell in Fairfax (#1829)
* enable Mongo Proxy and LMS in sovereign clouds

* remove mooncake

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-04-29 16:25:58 -04:00
SATYA SB
a08415e7bc [3100018:[Programmatic Access - Azure Cosmos DB - Edit Property]: Text Area edit field does not have a label under 'Edit Property' pane. (#1819)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2024-04-29 22:26:27 +05:30
SATYA SB
b94ce28e96 [accessibility-2724013]:[Screen reader - Cosmos DB - Data Explorer -> Entities -> Add entity]: Screen reader announces incorrect role when focus lands on the "Edit" and "Delete" buttons. (#1822)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2024-04-29 22:23:49 +05:30
sunghyunkang1111
f8f7ea34bd Copilot rewording (#1824)
* Copilot rebranding to query advisor

* fix the subquery link
2024-04-26 14:09:55 -05:00
Asier Isayas
cbd5e6bf76 open Legacy Mongo SHell with correct base URL in sovereign clouds (#1823)
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-04-26 14:55:47 -04:00
Ashley Stanton-Nurse
618c5ec0fe Add button (and keyboard shortcut) to download query (#1817) 2024-04-24 15:11:51 -07:00
Asier Isayas
afc82845b5 activate Token Controller (#1820)
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-04-24 15:04:01 -04:00
Ashley Stanton-Nurse
f4bcee5461 initialize new documents with their partition key (#1815)
* initialize new documents with their partition key

* refmt
2024-04-23 15:47:04 -07:00
Ashley Stanton-Nurse
17207624a9 add more intl-friendly tab nav shortcuts (#1814) 2024-04-23 15:46:41 -07:00
jawelton74
d36e511b18 Update d3, webpack-dev-server, typedoc dependencies. (#1812)
* Update d3, webpack-dev-server, typedoc dependencies.

* Fix unit test failures.

* Revert change to snapshot as it doesn't seem required when running in
github.
2024-04-23 10:15:48 -07:00
Ashley Stanton-Nurse
c1a28793ba bind F5 to execute query (#1813) 2024-04-23 09:08:29 -07:00
Asier Isayas
acf5acfdb4 Remove Legacy Mongo Shell feature flag (#1810)
* LMS Mongo Proxy support

* change stirng to url for get mongo shell url

* fix tests

* enable feature flag

* fixed unit test

* add mongoshell to path

* remove LMS feature flag

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-04-23 08:20:27 -04:00
jawelton74
7b81767ded Enable new backend for Settings API in Prod. (#1791) 2024-04-22 14:34:20 -07:00
jawelton74
c12eced120 Update node-fetch, react-dev-utils and azure/identity dependencies. (#1809) 2024-04-22 07:10:16 -07:00
jawelton74
2b15a4d43d Update package.json (#1807) 2024-04-19 15:03:11 -07:00
Ashley Stanton-Nurse
c220a8b070 [Task 3071878] Tab Navigation Keyboard Shortcuts (#1808)
* [Task 3071878] Tab Navigation Keyboard Shortcuts

* throw in development on duplicate handlers

* refmt
2024-04-19 13:44:30 -07:00
Ashley Stanton-Nurse
a5a5a95973 [Task 3061766] Additional Keyboard Shortcuts (#1805)
* [Task 3061766] Additional Keyboard Shortcuts

refmt and fix lints

shortcuts for: discard, new item/sproc/udf/trigger, delete
item/sproc/udf/trigger

copilot shortcut

* remove 'Ctrl+I' due to conflict with Monaco Autocomplete
2024-04-19 09:43:27 -07:00
Asier Isayas
e3fab9b5bf Add 'mongoshell' to Legacy Mongo Shell path (#1806)
* LMS Mongo Proxy support

* change stirng to url for get mongo shell url

* fix tests

* enable feature flag

* fixed unit test

* add mongoshell to path

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-04-18 15:39:13 -04:00
Asier Isayas
98000a27f0 Legacy Mongo Shell Mongo Proxy support (#1802)
* LMS Mongo Proxy support

* change stirng to url for get mongo shell url

* fix tests

* enable feature flag

* fixed unit test

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-04-17 19:01:12 -04:00
Ashley Stanton-Nurse
af664326ea Fix issues with the command bar when switching through React and Trigger tabs (#1804)
* fix bug in trigger tab that takes over the command bar while open

* clear context buttons when a react tab is active

* restore unintentionally removed code

* reformat
2024-04-17 15:57:29 -07:00
Ashley Stanton-Nurse
a44ed1f45c [Task 3061766] Global Keyboard Shortcuts, implemented through the Command Bar (#1789)
* keyboard shortcuts using tinykeys

* refmt and fix lints

* retarget keyboard shortcuts to the body instead of the root element of the React component tree

* refmt

* Update src/Explorer/Menus/CommandBar/CommandBarUtil.tsx

Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com>

* add Save binding to New Item command bar

---------

Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com>
2024-04-17 11:19:09 -07:00
JustinKol
e0cb3da6aa Is executing is false (#1801) 2024-04-16 18:47:43 -04:00
JustinKol
6c9673975a Added hyphen to prohibited characters in keyspace name title (#1800) 2024-04-16 09:24:14 -04:00
JustinKol
d35e2a325e Cassandra API create table error messages swallowed by the Portal and shown as "undefined". (#1790)
* changed error message variable

* changed other error messages

* Added check in case responseJSON is missing

* created error const
2024-04-15 15:47:58 -04:00
sunghyunkang1111
00a816c488 set the value in the editor for results (#1799) 2024-04-13 15:19:56 -05:00
jawelton74
953bef404b Set backend endpoints in testExplorer to use MPAC. (#1797) 2024-04-11 15:43:46 -07:00
Asier Isayas
dfcb771939 Activate Mongo Proxy and Cassandra Proxy in Prod (#1794)
* activate Mongo Proxy and Cassandra Proxy in Prod

* fix bug that blocked local mongo proxy and cassandra proxy development

* fix pr check tests

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-04-09 13:45:36 -07:00
jawelton74
6925fa8e4e Replace Entra app client secret auth with OpenID Connect in E2E tests. (#1792)
* Use Az login with OpenID connection to get test credentials.

* Set subscription id environment variable.

* Update testExplorer and cleanup job.

* Retrieve access token in test case and pass to testExplorer.

* Add debug tracing for tests.

* Set up other mongo test to use Az CLI creds.

* Revert subscription id retrieval.

* Add CLI credentials retrieval to rest of tests.

* Fix missing imports.

* Clean up redundant code.

* Remove commented import statement.
2024-04-09 10:55:08 -07:00
jawelton74
7f6338b68b Change copilot settings call to use new backend endpoint. (#1781)
* Change copilot settings call to use new backend endpoint.

* Refactor EndpointUtils function for new backend enablement.
2024-04-04 10:18:50 -07:00
Ashley Stanton-Nurse
db50f42832 [Task #3061771] Correct render order issues on undo (#1785)
* fix #3061771 by correcting render order issues on undo

* clarifying comment

* fix lints

* push an undo stop before executing edits

* tidy up some unnecessary comments
2024-04-04 09:17:09 -07:00
Ashley Stanton-Nurse
f533eeb0fc add support for react dev tools in the cosmos explorer (#1788) 2024-04-04 09:16:23 -07:00
sunghyunkang1111
3c5d899e47 add the new message to the bottom to avoid contract breaking (#1786) 2024-04-02 17:54:53 -05:00
Ashley Stanton-Nurse
b44778b00a fix #3061738 by unclobbering some Monaco styles we clobber (#1784) 2024-04-02 10:51:19 -07:00
sunghyunkang1111
1464745659 Add activate/close tab contracts and add to queryTab (#1783) 2024-04-02 11:49:33 -05:00
Asier Isayas
18cc2a4195 Activate Mongo and Cassandra Proxies in MPAC (#1776)
* Fix API endpoint for CassandraProxy query API

* activated mongo proxy

* added mpac

* Activate CassandraProxy API endpoints for MPAC

* Run npm format

* Set CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED when we detect new
Cassandra Proxy endpoints in IP rules.

* query documents API fix

* simplify ip check

---------

Co-authored-by: Senthamil Sindhu <sindhuba@microsoft.com>
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
Co-authored-by: Jade Welton <jawelton@microsoft.com>
2024-04-02 09:34:58 -07:00
sindhuba
86f2bc171f Remove notebooks UI (#1779)
* Fix API endpoint for CassandraProxy query API

* Remove notebooks UI components

* Fix tests

* Address comments

* Fix unit tests

* Remove commented code
2024-04-01 08:44:42 -07:00
jawelton74
cabedf4a29 Enable new backend endpoint to be passed to Data Explorer via message. (#1782) 2024-04-01 07:54:04 -07:00
sunghyunkang1111
5aa6b0abe1 fix opening collections (#1780)
* fix opening collections

* fix opening collections

* fix opening collections

* fix opening collections
2024-03-28 12:10:32 -05:00
Vsevolod Kukol
f24b0bcf1b Show the Feedback button in Portal only (#1775)
This feature is not supported on any other platform incl. Hosted.
2024-03-28 16:05:38 +01:00
JustinKol
56408a97d7 Add container ids to tabs (#1772)
* Added container ids to tabs

* prettier run

* Updated for undefined scenarios

* prettier

* added ellipsis to long container names

* added slice

* prettier

* Added ellipsis to long DB names in tabs

* Added undefined DB case

* Replaced dots with ellipsis character

* corrected undefined return value
2024-03-26 12:36:04 -04:00
Vsevolod Kukol
0df68c4967 Fix initial container loading in Fabric (#1771)
* Fix initial container loading in Fabric

There is a rendering issue where the documents table doesn't resize properly if explorer is loaded in the beackground (invisible).
To workaround this, track DE visibility from Fabric RPC and open the first container only once DE becomes visible. If DE has been already shown, open the container right away.

* Preserve glitchy behavior if Fabric UX doesn't send isVisible

* Preserve Fabric visibility in global status
and fix a race condition where visibility might change during initialization.
2024-03-26 17:22:15 +01:00
Asier Isayas
e09930d9d0 Support Token API in new Portal Backend (#1773)
* added support for generate token

* fix checks

* fix checks

* deactivate mongo proxy

* fix tests

* remove mongo proxy from mpac

* change endpoints to prod

* npm run format

* add await

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-03-22 13:18:02 -04:00
sunghyunkang1111
da2e874ae6 Fix bugs on data transfer and bring back query explanation and remove query prompt from editor (#1777)
* Fix minor issues

* add back preview tag

* bring back query explanation and remove prompt in editor
2024-03-21 11:23:42 -05:00
Vsevolod Kukol
a524138ac9 Don't show the new Home button in Fabric (#1774)
as the whole Home tab feature is not supported there.
2024-03-20 00:28:24 +01:00
sindhuba
39b0fb9e2c Fix API endpoint for CassandraProxy query API (#1769) 2024-03-18 10:49:33 -07:00
sunghyunkang1111
ac22e88d9c rebranding of inline copilot (#1767)
* rebranding of inline copilot

* rebranding of inline copilot

* rebranding of inline copilot

* fix styling
2024-03-18 12:15:24 -05:00
Laurent Nguyen
91d9e27049 Turn off fetching authorization token (#1766) 2024-03-14 21:56:26 +01:00
202 changed files with 16685 additions and 8335 deletions

10
.editorconfig Normal file
View File

@@ -0,0 +1,10 @@
# NOTE: Prettier reads EditorConfig settings, so be careful adjusting settings here and assuming they'll only affect your editor ;).
# top-most EditorConfig file
root = true
[*.yml]
indent_size = 2
[*.{js,jsx,ts,tsx}]
indent_size = 2

View File

@@ -1,3 +1,5 @@
playwright.config.ts
**/node_modules/
src/**/__mocks__/**/*
dist/
@@ -89,10 +91,7 @@ src/Explorer/Tables/TableEntityProcessor.ts
src/Explorer/Tables/Utilities.ts
src/Explorer/Tabs/ConflictsTab.ts
src/Explorer/Tabs/DatabaseSettingsTab.ts
src/Explorer/Tabs/DocumentsTab.test.ts
src/Explorer/Tabs/DocumentsTab.ts
src/Explorer/Tabs/GraphTab.ts
src/Explorer/Tabs/MongoDocumentsTab.ts
src/Explorer/Tabs/NotebookV2Tab.ts
src/Explorer/Tabs/ScriptTabBase.ts
src/Explorer/Tabs/TabComponents.ts
@@ -128,7 +127,7 @@ src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx
src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx
src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
src/Explorer/Controls/TreeComponent/TreeComponent.tsx
src/Explorer/Controls/TreeComponent/LegacyTreeComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx

View File

@@ -8,6 +8,9 @@ on:
pull_request:
branches:
- master
permissions:
id-token: write
contents: read
jobs:
codemetrics:
runs-on: ubuntu-latest
@@ -101,72 +104,6 @@ jobs:
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
env:
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
endtoendemulator:
name: "End To End Emulator Tests"
# Temporarily disabled. This test needs to be rewritten in playwright
if: false
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js 18.x
uses: actions/setup-node@v4
with:
node-version: 18.x
- uses: southpolesteve/cosmos-emulator-github-action@v1
- name: End to End Tests
run: |
npm ci
npm start &
npm run wait-for-server
npx jest -c ./jest.config.e2e.js --detectOpenHandles test/sql/container.spec.ts
shell: bash
env:
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/explorer.html?platform=Emulator"
PLATFORM: "Emulator"
NODE_TLS_REJECT_UNAUTHORIZED: 0
- uses: actions/upload-artifact@v3
if: failure()
with:
name: screenshots
path: failed-*
endtoend:
name: "E2E"
runs-on: ubuntu-latest
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
strategy:
fail-fast: false
matrix:
test-file:
- ./test/cassandra/container.spec.ts
- ./test/graph/container.spec.ts
- ./test/sql/container.spec.ts
- ./test/mongo/container.spec.ts
- ./test/mongo/container32.spec.ts
- ./test/selfServe/selfServeExample.spec.ts
# - ./test/notebooks/upload.spec.ts // TEMP disabled since notebooks service is off
- ./test/sql/resourceToken.spec.ts
- ./test/tables/container.spec.ts
steps:
- uses: actions/checkout@v4
- name: Use Node.js 18.x
uses: actions/setup-node@v4
with:
node-version: 18.x
- run: npm ci
- run: npm start &
- run: npm run wait-for-server
- name: ${{ matrix['test-file'] }}
run: |
# Run tests up to three times
for i in $(seq 1 3); do npx jest -c ./jest.config.playwright.js ${{ matrix['test-file'] }} && s=0 && break || s=$? && sleep 1; done; (exit $s)
shell: bash
- uses: actions/upload-artifact@v3
if: failure()
with:
name: screenshots
path: screenshots/
nuget:
name: Publish Nuget
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
@@ -216,3 +153,70 @@ jobs:
name: packages
with:
path: "*.nupkg"
playwright-tests:
name: "Run Playwright Tests (Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }})"
runs-on: ubuntu-latest
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
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: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- name: Upload blob report to GitHub Actions Artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: blob-report-${{ matrix.shardIndex }}
path: blob-report
retention-days: 1
merge-playwright-reports:
name: "Merge Playwright Reports"
# Merge reports after playwright-tests, even if some shards have failed
if: ${{ !cancelled() }}
needs: [playwright-tests]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Download blob reports from GitHub Actions Artifacts
uses: actions/download-artifact@v4
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: Merge into HTML Report
run: npx playwright merge-reports --reporter html ./all-blob-reports
- name: Upload HTML report
uses: actions/upload-artifact@v4
with:
name: html-report--attempt-${{ github.run_attempt }}
path: playwright-report
retention-days: 14

View File

@@ -9,6 +9,10 @@ on:
# Once every hour
- cron: "0 15 * * *"
permissions:
id-token: write
contents: read
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
@@ -16,10 +20,17 @@ jobs:
name: "Cleanup Test Database Accounts"
runs-on: ubuntu-latest
env:
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
steps:
- uses: actions/checkout@v2
- 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@v1
with:

4
.gitignore vendored
View File

@@ -17,3 +17,7 @@ Contracts/*
failure.png
screenshots/*
GettingStarted-ignore*.ipynb
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View File

@@ -1,13 +0,0 @@
const isCI = require("is-ci");
module.exports = {
exitOnPageError: false,
launchOptions: {
headless: isCI,
slowMo: 10,
timeout: 60000,
},
contextOptions: {
ignoreHTTPSErrors: true,
},
};

View File

@@ -31,7 +31,7 @@ module.exports = {
coveragePathIgnorePatterns: ["/node_modules/"],
// A list of reporter names that Jest uses when writing coverage reports
coverageReporters: ["json", "text", "cobertura"],
coverageReporters: ["json", "text", "cobertura", "lcov"],
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
@@ -76,6 +76,10 @@ module.exports = {
"^dnd-core$": "dnd-core/dist/cjs",
"^react-dnd$": "react-dnd/dist/cjs",
"^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs",
"d3-force": "<rootDir>/node_modules/d3-force/dist/d3-force.min.js",
"d3-quadtree": "<rootDir>/node_modules/d3-quadtree/dist/d3-quadtree.min.js",
"d3-scale-chromatic": "<rootDir>/node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.min.js",
"d3-zoom": "<rootDir>/node_modules/d3-zoom/dist/d3-zoom.min.js",
},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
@@ -130,7 +134,6 @@ module.exports = {
// The test environment that will be used for testing
// testEnvironment: "jest-environment-jsdom",
modulePaths: ["node_modules", "<rootDir>/src"],
// Options that will be passed to the testEnvironment

View File

@@ -1,7 +0,0 @@
module.exports = {
preset: "jest-playwright-preset",
testMatch: ["<rootDir>/test/**/*.spec.[jt]s?(x)"],
setupFiles: ["dotenv/config"],
testEnvironment: "./test/playwrightEnv.js",
setupFilesAfterEnv: ["expect-playwright"],
};

View File

@@ -130,6 +130,7 @@
@ActiveTabWidth: 141px;
@TabsHeight: 30px;
@TabsWidth: 140px;
@ContentWrapper: 111px;
@StatusIconContainerSize: 18px;
@LoadingErrorIconSize: 14px;
@ErrorIconContainer: 16px;
@@ -336,3 +337,10 @@
height: 0;
border-color: @InfoPointerColor transparent;
}
/*********************************************************************************************************
Screen Reader Only
**********************************************************************************************************/
.screenReaderOnly {
position: absolute;
left: -9999px;
}

View File

@@ -2264,38 +2264,49 @@ a:link {
width: 82px;
}
.tabdocuments .scrollable {
height: 100%;
overflow-y: auto;
overflow-x: hidden;
padding-left: 5px;
padding-right: 5px;
width: 100%;
}
// .tabdocuments .scrollable {
// height: 100%;
// overflow-y: auto;
// overflow-x: hidden;
// padding-left: 5px;
// padding-right: 5px;
// width: 100%;
// }
.tabdocuments > .tabdocumentsGridElement {
width: 50%;
}
// .tabdocuments > .tabdocumentsGridElement {
// width: 50%;
// }
.tabdocuments > .evenlySpacedHeader {
width: 30%;
}
// .tabdocuments > .evenlySpacedHeader {
// width: 30%;
// }
.tabdocuments.scrollable:focus,
.tabdocuments.scrollable:active {
outline: 1px dotted;
}
// .tabdocuments.scrollable:focus,
// .tabdocuments.scrollable:active {
// outline: 1px dotted;
// }
.tabdocuments .scrollable table td {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// .tabdocuments .scrollable table td {
// white-space: nowrap;
// overflow: hidden;
// text-overflow: ellipsis;
// }
.mongoDocumentEditor .monaco-editor.vs .redsquiggly {
display: none !important;
}
.monaco-editor .quick-input-list-label {
/* Restore some of Monaco's default styles that are clobbered by our global styles */
padding: 0;
line-height: 22px;
}
.monaco-editor .quick-input-list .highlight {
/* Padding in highlighted text within the quick input list breaks the flow of the text */
padding: 0;
}
td a {
color: #393939;
}
@@ -2305,10 +2316,9 @@ td a:hover {
}
.loadMore {
display: block;
width: 100%;
padding-left: 30%;
padding-top: 2px;
cursor: pointer;
text-align: center;
}
.loadMore > a:focus {
@@ -2547,10 +2557,12 @@ a:link {
}
.filterdivs {
padding-top: 15px;
height: 45px;
margin-bottom: 8px;
margin: 10px 0px;
white-space: nowrap;
input {
line-height: 14px; // To correct vertical position of the down arrow of the input
outline: none; // Remove the dotted border on focus, because fluent has its own focus style (underlined)
}
}
.editFilterContainer {
@@ -2567,6 +2579,18 @@ a:link {
cursor: pointer;
}
.documentsTab {
.documentsTable {
.documentsTableCell {
border-left: 1px solid @BaseMedium;
height: 100%;
}
.documentsTableHeader {
border-bottom: 1px solid @BaseMedium;
}
}
}
.querydropdown {
border: 1px solid @BaseMedium;
font-style: normal;
@@ -2647,7 +2671,7 @@ a:link {
width: @ActiveTabWidth;
}
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .tabNavText {
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper> .tabNavText {
font-weight: bolder;
border-bottom: 2px solid rgba(0, 120, 212, 1);
}
@@ -2683,6 +2707,9 @@ a:link {
width: @TabsWidth;
border-right: @ButtonBorderWidth solid @BaseMedium;
white-space: nowrap;
.contentWrapper {
.flex-display();
width: @ContentWrapper;
.statusIconContainer {
width: @StatusIconContainerSize;
@@ -2741,9 +2768,10 @@ a:link {
white-space: nowrap;
flex-grow: 1;
}
}
.tabIconSection {
width: 30px;
width: 29px;
position: relative;
padding-top: 2px;

View File

@@ -75,7 +75,7 @@ a:focus {
border-bottom: 2px solid @FabricAccentMedium;
}
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content>.tabNavText {
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content>.contentWrapper>.tabNavText {
border-bottom: 0px none transparent;
}
@@ -93,9 +93,11 @@ a:focus {
width: calc(@TabsWidth - (@SmallSpace * 2));
padding-bottom: @SmallSpace;
.contentWrapper {
.statusIconContainer {
margin-left: 0px;
}
}
.tabIconSection {
.cancelButton {

3985
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,10 +5,10 @@
"main": "index.js",
"dependencies": {
"@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.0.1-beta.2",
"@azure/cosmos": "4.0.1-beta.3",
"@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.2.1",
"@azure/ms-rest-nodeauth": "3.0.7",
"@azure/identity": "1.5.2",
"@azure/ms-rest-nodeauth": "3.1.1",
"@azure/msal-browser": "2.14.2",
"@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12",
@@ -46,6 +46,8 @@
"@types/lodash": "4.14.171",
"@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7",
"@uiw/react-split": "5.9.3",
"@xmldom/xmldom": "0.7.13",
"applicationinsights": "1.8.0",
"bootstrap": "3.4.1",
"canvas": "file:./canvas",
@@ -54,7 +56,7 @@
"copy-webpack-plugin": "11.0.0",
"crossroads": "0.12.2",
"css-element-queries": "1.1.1",
"d3": "6.1.1",
"d3": "7.8.5",
"datatables.net-colreorder-dt": "1.7.0",
"datatables.net-dt": "1.13.8",
"date-fns": "1.29.0",
@@ -69,12 +71,14 @@
"i18next-browser-languagedetector": "6.0.1",
"i18next-http-backend": "1.0.23",
"iframe-resizer-react": "1.1.0",
"immer": "9.0.6",
"immutable": "4.0.0-rc.12",
"is-ci": "2.0.0",
"jquery": "3.7.1",
"jquery-typeahead": "2.11.1",
"jquery-ui-dist": "1.13.2",
"knockout": "3.5.1",
"loader-utils": "2.0.3",
"mkdirp": "1.0.4",
"monaco-editor": "0.44.0",
"ms": "2.1.3",
@@ -95,13 +99,16 @@
"react-splitter-layout": "4.0.0",
"react-string-format": "1.0.1",
"react-youtube": "9.0.1",
"react-window": "1.8.10",
"reflect-metadata": "0.1.13",
"rx-jupyter": "5.5.12",
"sanitize-html": "2.3.3",
"shell-quote": "1.7.3",
"styled-components": "5.0.1",
"swr": "0.4.0",
"terser-webpack-plugin": "5.3.9",
"underscore": "1.9.1",
"tinykeys": "2.1.0",
"underscore": "1.12.1",
"utility-types": "3.10.0",
"zustand": "3.5.0"
},
@@ -110,6 +117,7 @@
"@babel/preset-env": "7.9.0",
"@babel/preset-react": "7.9.4",
"@babel/preset-typescript": "7.9.0",
"@playwright/test": "1.44.0",
"@testing-library/react": "11.2.3",
"@types/applicationinsights-js": "1.0.7",
"@types/codemirror": "0.0.56",
@@ -118,8 +126,8 @@
"@types/datatables.net": "1.10.28",
"@types/datatables.net-colreorder": "1.4.5",
"@types/dom-to-image": "2.6.2",
"@types/enzyme": "3.10.7",
"@types/enzyme-adapter-react-16": "1.0.6",
"@types/enzyme": "3.10.12",
"@types/enzyme-adapter-react-16": "1.0.9",
"@types/hasher": "0.0.31",
"@types/jest": "26.0.20",
"@types/jquery": "3.5.29",
@@ -131,6 +139,7 @@
"@types/react-notification-system": "0.2.39",
"@types/react-redux": "7.1.7",
"@types/react-splitter-layout": "3.0.1",
"@types/react-window": "1.8.8",
"@types/sanitize-html": "1.27.2",
"@types/sinon": "2.3.3",
"@types/styled-components": "5.1.1",
@@ -146,14 +155,13 @@
"create-file-webpack": "1.0.2",
"css-loader": "6.8.1",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.5",
"enzyme-to-json": "3.6.1",
"enzyme-adapter-react-16": "1.15.8",
"enzyme-to-json": "3.6.2",
"eslint": "8.50.0",
"eslint-cli": "1.1.1",
"eslint-plugin-no-null": "1.0.2",
"eslint-plugin-prefer-arrow": "1.2.3",
"eslint-plugin-react-hooks": "4.6.0",
"expect-playwright": "0.3.3",
"fast-glob": "3.2.5",
"fs-extra": "7.0.0",
"html-inline-css-webpack-plugin": "1.11.2",
@@ -162,7 +170,6 @@
"html-webpack-plugin": "5.5.3",
"jest": "26.6.3",
"jest-canvas-mock": "2.3.1",
"jest-playwright-preset": "1.5.1",
"jest-react-hooks-shallow": "1.5.1",
"jest-trx-results-processor": "0.0.7",
"less": "3.8.1",
@@ -170,25 +177,24 @@
"less-vars-loader": "1.1.0",
"mini-css-extract-plugin": "2.1.0",
"monaco-editor-webpack-plugin": "7.1.0",
"node-fetch": "2.6.1",
"playwright": "1.13.0",
"node-fetch": "2.6.7",
"prettier": "3.0.3",
"process": "0.11.10",
"querystring-es3": "0.2.1",
"raw-loader": "0.5.1",
"react-dev-utils": "11.0.4",
"react-dev-utils": "12.0.1",
"rimraf": "3.0.0",
"sinon": "3.2.1",
"style-loader": "0.23.0",
"ts-loader": "9.2.4",
"typedoc": "0.21.5",
"typedoc": "0.22.15",
"typescript": "4.3.5",
"url-loader": "4.1.1",
"wait-on": "4.0.2",
"webpack": "5.88.2",
"webpack-bundle-analyzer": "4.9.1",
"webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.1"
"webpack-dev-server": "4.15.2"
},
"scripts": {
"postinstall": "patch-package",

View File

@@ -0,0 +1,11 @@
diff --git a/node_modules/@uiw/react-split/cjs/index.d.ts b/node_modules/@uiw/react-split/cjs/index.d.ts
index 644bcc3..f794760 100644
--- a/node_modules/@uiw/react-split/cjs/index.d.ts
+++ b/node_modules/@uiw/react-split/cjs/index.d.ts
@@ -56,5 +56,5 @@ export default class Split extends React.Component<SplitProps, SplitState> {
onMouseDown(paneNumber: number, env: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
onDragging(env: Event): void;
onDragEnd(): void;
- render(): import("react/jsx-runtime").JSX.Element;
+ render(): JSX.Element;
}

53
playwright.config.ts Normal file
View File

@@ -0,0 +1,53 @@
import { defineConfig, devices } from '@playwright/test';
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: 'test',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 3 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? 'blob' : 'html',
timeout: 10 * 60 * 1000,
use: {
actionTimeout: 5 * 60 * 1000,
trace: 'off',
video: 'off',
screenshot: 'on',
testIdAttribute: 'data-test',
contextOptions: {
ignoreHTTPSErrors: true,
},
},
expect: {
timeout: 5 * 60 * 1000,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
webServer: {
command: 'npm run start',
url: 'https://127.0.0.1:1234/_ready',
timeout: 120 * 1000,
ignoreHTTPSErrors: true,
reuseExistingServer: !process.env.CI,
},
});

View File

@@ -88,6 +88,12 @@ export class CapabilityNames {
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
public static readonly EnableMongo: string = "EnableMongo";
public static readonly EnableServerless: string = "EnableServerless";
public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch";
}
export enum CapacityMode {
Provisioned = "Provisioned",
Serverless = "Serverless",
}
// flight names returned from the portal are always lowercase
@@ -124,6 +130,35 @@ export enum MongoBackendEndpointType {
remote,
}
export class BackendApi {
public static readonly GenerateToken: string = "GenerateToken";
public static readonly PortalSettings: string = "PortalSettings";
}
export class PortalBackendEndpoints {
public static readonly Development: string = "https://localhost:7235";
public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
public static readonly Prod: string = "https://cdb-ms-prod-pbe.cosmos.azure.com";
public static readonly Fairfax: string = "https://cdb-ff-prod-pbe.cosmos.azure.us";
public static readonly Mooncake: string = "https://cdb-mc-prod-pbe.cosmos.azure.cn";
}
export class MongoProxyEndpoints {
public static readonly Local: string = "https://localhost:7238";
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn";
}
export class CassandraProxyEndpoints {
public static readonly Development: string = "https://localhost:7240";
public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com";
public static readonly Prod: string = "https://cdb-ms-prod-cp.cosmos.azure.com";
public static readonly Fairfax: string = "https://cdb-ff-prod-cp.cosmos.azure.us";
public static readonly Mooncake: string = "https://cdb-mc-prod-cp.cosmos.azure.cn";
}
//TODO: Remove this when new backend is migrated over
export class CassandraBackend {
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
@@ -139,7 +174,7 @@ export class CassandraBackend {
export class CassandraProxyAPIs {
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
public static readonly connectionStringCreateOrDeleteApi: string = "api/connectionstring/cassandra/createordelete";
public static readonly queryApi: string = "api/cassandra/postquery";
public static readonly queryApi: string = "api/cassandra";
public static readonly connectionStringQueryApi: string = "api/connectionstring/cassandra";
public static readonly keysApi: string = "api/cassandra/keys";
public static readonly connectionStringKeysApi: string = "api/connectionstring/cassandra/keys";
@@ -446,22 +481,6 @@ export class JunoEndpoints {
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
}
export class MongoProxyEndpoints {
public static readonly Development: string = "https://localhost:7238";
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn";
}
export class CassandraProxyEndpoints {
public static readonly Development: string = "https://localhost:7240";
public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com";
public static readonly Prod: string = "https://cdb-ms-prod-cp.cosmos.azure.com";
public static readonly Fairfax: string = "https://cdb-ff-prod-cp.cosmos.azure.us";
public static readonly Mooncake: string = "https://cdb-mc-prod-cp.cosmos.azure.cn";
}
export class PriorityLevel {
public static readonly High = "high";
public static readonly Low = "low";

View File

@@ -1,7 +1,6 @@
import * as Cosmos from "@azure/cosmos";
import { sendCachedDataMessage } from "Common/MessageHandler";
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
import { AuthorizationToken, MessageTypes } from "Contracts/MessageTypes";
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { AuthType } from "../AuthType";
@@ -51,15 +50,23 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
case Cosmos.ResourceType.offer:
case Cosmos.ResourceType.user:
case Cosmos.ResourceType.permission:
// For now, these operations aren't used, so fetching the authorization token is commented out.
// This provider must return a real token to pass validation by the client, so we return the cached resource token
// (which is a valid token, but won't work for these operations).
const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
/* ************** TODO: Uncomment this code if we need to support these operations **************
// User master tokens
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(
MessageTypes.GetAuthorizationToken,
FabricMessageTypes.GetAuthorizationToken,
[requestInfo],
userContext.fabricContext.connectionId,
);
console.log("Response from Fabric: ", authorizationToken);
headers[HttpHeaders.msDate] = authorizationToken.XDate;
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);
***********************************************************************************************/
}
}

View File

@@ -1,9 +1,9 @@
import { userContext } from "../UserContext";
export const getEntityName = (): string => {
export const getEntityName = (multiple?: boolean): string => {
if (userContext.apiType === "Mongo") {
return "document";
return multiple ? "documents" : "document";
}
return "item";
return multiple ? "items" : "item";
};

View File

@@ -1,3 +1,4 @@
import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
import Q from "q";
import * as _ from "underscore";
import { MessageTypes } from "../Contracts/ExplorerContracts";
@@ -36,7 +37,7 @@ export function handleCachedDataMessage(message: any): void {
* @returns
*/
export function sendCachedDataMessage<TResponseDataModel>(
messageType: MessageTypes,
messageType: MessageTypes | FabricMessageTypes,
params: Object[],
scope?: string,
timeoutInMs?: number,

View File

@@ -67,7 +67,7 @@ export function queryDocuments(
query: string,
continuationToken?: string,
): Promise<QueryResponse> {
if (!useMongoProxyEndpoint("resourcelist")) {
if (!useMongoProxyEndpoint("resourcelist") || !useMongoProxyEndpoint("queryDocuments")) {
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
}
@@ -106,7 +106,7 @@ export function queryDocuments(
headers[CosmosSDKConstants.HttpHeaders.Continuation] = continuationToken;
}
const path = isResourceList ? "/resourcelist" : "";
const path = isResourceList ? "/resourcelist" : "/queryDocuments";
return window
.fetch(`${endpoint}${path}`, {
@@ -672,6 +672,28 @@ export function getEndpoint(endpoint: string): string {
return url;
}
export function useMongoProxyEndpoint(api: string): boolean {
const activeMongoProxyEndpoints: string[] = [
MongoProxyEndpoints.Local,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
];
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
if (
configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local &&
userContext.databaseAccount.properties.ipRules?.length > 0
) {
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
}
return (
canAccessMongoProxy &&
configContext.NEW_MONGO_APIS?.includes(api) &&
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
);
}
// TODO: This function throws most of the time except on Forbidden which is a bit strange
// It causes problems for TypeScript understanding the types
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
@@ -688,16 +710,3 @@ async function errorHandling(response: Response, action: string, params: unknown
export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string {
return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`;
}
function useMongoProxyEndpoint(api: string): boolean {
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
if (userContext.databaseAccount.properties.ipRules?.length > 0) {
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
}
return (
canAccessMongoProxy &&
configContext.NEW_MONGO_APIS?.includes(api) &&
[MongoProxyEndpoints.Development, MongoProxyEndpoints.Mpac].includes(configContext.MONGO_PROXY_ENDPOINT)
);
}

View File

@@ -3,8 +3,7 @@ import * as _ from "underscore";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import Explorer from "../Explorer/Explorer";
import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
import DocumentId from "../Explorer/Tree/DocumentId";
import DocumentId, { IDocumentIdContainer } from "../Explorer/Tree/DocumentId";
import { useDatabases } from "../Explorer/useDatabases";
import { userContext } from "../UserContext";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
@@ -162,10 +161,10 @@ export class QueriesClient {
{
partitionKey: QueriesClient.PartitionKey,
partitionKeyProperties: ["id"],
} as DocumentsTab,
} as IDocumentIdContainer,
query,
[query.queryName],
); // TODO: Remove DocumentId's dependency on DocumentsTab
);
const options: any = { partitionKey: query.resourceId };
return deleteDocument(queriesCollection, documentId)
.then(

View File

@@ -1,14 +1,10 @@
import { ResourceTree } from "Explorer/Tree/ResourceTree";
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
import refreshImg from "../../images/refresh-cosmos.svg";
import { AuthType } from "../AuthType";
import Explorer from "../Explorer/Explorer";
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
import { ResourceTree2 } from "../Explorer/Tree2/ResourceTree";
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
import { userContext } from "../UserContext";
import { getApiShortDisplayName } from "../Utils/APITypeUtils";
import { Platform, configContext } from "./../ConfigContext";
import { NormalizedEventKey } from "./Constants";
export interface ResourceTreeContainerProps {
@@ -74,12 +70,8 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
</div>
</div>
</div>
{userContext.authType === AuthType.ResourceToken ? (
<ResourceTokenTree />
) : userContext.features.enableKoResourceTree ? (
{userContext.features.enableKoResourceTree ? (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
) : configContext.platform === Platform.Fabric ? (
<ResourceTree2 container={container} />
) : (
<ResourceTree container={container} />
)}

View File

@@ -142,7 +142,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
<Image
{...imageProps}
src={EditIcon}
alt="editEntity"
alt={`Edit ${entityProperty} entity`}
onClick={onEditEntity}
tabIndex={0}
onKeyPress={handleKeyPress}
@@ -156,7 +156,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
<Image
{...imageProps}
src={DeleteIcon}
alt="delete entity"
alt={`Delete ${entityProperty} entity`}
id="deleteEntity"
onClick={onDeleteEntity}
tabIndex={0}

View File

@@ -2,6 +2,7 @@
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
Object {
"disableNonStreamingOrderByQuery": true,
"enableScanInQuery": true,
"forceQueryPlan": true,
"maxDegreeOfParallelism": 0,
@@ -12,6 +13,7 @@ Object {
exports[`getCommonQueryOptions reads from localStorage 1`] = `
Object {
"disableNonStreamingOrderByQuery": true,
"enableScanInQuery": true,
"forceQueryPlan": true,
"maxDegreeOfParallelism": 17,

View File

@@ -6,13 +6,13 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { getCollectionName } from "../../Utils/APITypeUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { createMongoCollectionWithProxy } from "../MongoProxyClient";
@@ -96,6 +96,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
if (params.uniqueKeyPolicy) {
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
}
if (params.vectorEmbeddingPolicy) {
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
}
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
properties: {

View File

@@ -122,14 +122,21 @@ const pollDataTransferJobOperation = async (
updateDataTransferJob(body);
if (status === "Cancelled" || status === "Failed" || status === "Faulted") {
if (status === "Cancelled") {
removeFromPolling(jobName);
clearMessage && clearMessage();
const cancelMessage = `Data transfer job ${jobName} cancelled`;
NotificationConsoleUtils.logConsoleError(cancelMessage);
throw new AbortError(cancelMessage);
}
if (status === "Failed" || status === "Faulted") {
removeFromPolling(jobName);
const errorMessage = body?.properties?.error
? JSON.stringify(body?.properties?.error)
: "Operation could not be completed";
const error = new Error(errorMessage);
clearMessage && clearMessage();
NotificationConsoleUtils.logConsoleError(`Data transfer job ${jobName} Failed`);
NotificationConsoleUtils.logConsoleError(`Data transfer job ${jobName} failed: ${errorMessage}`);
throw new AbortError(error);
}
if (status === "Completed") {

View File

@@ -1,3 +1,4 @@
import { BulkOperationType, OperationInput } from "@azure/cosmos";
import { CollectionBase } from "../../Contracts/ViewModels";
import DocumentId from "../../Explorer/Tree/DocumentId";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
@@ -24,3 +25,58 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
clearMessage();
}
};
/**
* Bulk delete documents
* @param collection
* @param documentId
* @returns array of ids that were successfully deleted
*/
export const deleteDocuments = async (collection: CollectionBase, documentIds: DocumentId[]): Promise<DocumentId[]> => {
const nbDocuments = documentIds.length;
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
try {
const v2Container = await client().database(collection.databaseId).container(collection.id());
// Bulk can only delete 100 documents at a time
const BULK_DELETE_LIMIT = 100;
const promiseArray = [];
while (documentIds.length > 0) {
const documentIdsChunk = documentIds.splice(0, BULK_DELETE_LIMIT);
const operations: OperationInput[] = documentIdsChunk.map((documentId) => ({
id: documentId.id(),
// bulk delete: if not partition key is specified, do not pass empty array, but undefined
partitionKey:
documentId.partitionKeyValue &&
Array.isArray(documentId.partitionKeyValue) &&
documentId.partitionKeyValue.length === 0
? undefined
: documentId.partitionKeyValue,
operationType: BulkOperationType.Delete,
}));
const promise = v2Container.items.bulk(operations).then((bulkResult) => {
return documentIdsChunk.filter((_, index) => bulkResult[index].statusCode === 204);
});
promiseArray.push(promise);
}
const allResult = await Promise.all(promiseArray);
const flatAllResult = Array.prototype.concat.apply([], allResult);
logConsoleInfo(
`Successfully deleted ${getEntityName(flatAllResult.length > 1)}: ${flatAllResult.length} out of ${nbDocuments}`,
);
// TODO: handle case result.length != nbDocuments
return flatAllResult;
} catch (error) {
handleError(
error,
"DeleteDocuments",
`Error while deleting ${documentIds.length} ${getEntityName(documentIds.length > 1)}`,
);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -1,4 +1,5 @@
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,5 +27,6 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
Queries.itemsPerPage;
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled();
return options;
};

View File

@@ -1,4 +1,10 @@
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints } from "Common/Constants";
import {
BackendApi,
CassandraProxyEndpoints,
JunoEndpoints,
MongoProxyEndpoints,
PortalBackendEndpoints,
} from "Common/Constants";
import {
allowedAadEndpoints,
allowedArcadiaEndpoints,
@@ -39,6 +45,8 @@ export interface ConfigContext {
ARCADIA_ENDPOINT: string;
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
BACKEND_ENDPOINT?: string;
PORTAL_BACKEND_ENDPOINT?: string;
NEW_BACKEND_APIS?: BackendApi[];
MONGO_BACKEND_ENDPOINT?: string;
MONGO_PROXY_ENDPOINT?: string;
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED?: boolean;
@@ -75,6 +83,7 @@ let configContext: Readonly<ConfigContext> = {
`^https:\\/\\/.*\\.analysis-df\\.net$`,
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
`^https:\\/\\/.*\\.azure-test\\.net$`,
`^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net`,
], // Webpack injects this at build time
gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/",
@@ -90,23 +99,21 @@ let configContext: Readonly<ConfigContext> = {
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
JUNO_ENDPOINT: JunoEndpoints.Prod,
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
NEW_MONGO_APIS: [
// "resourcelist",
// "createDocument",
// "readDocument",
// "updateDocument",
// "deleteDocument",
// "createCollectionWithProxy",
"resourcelist",
"queryDocuments",
"createDocument",
"readDocument",
"updateDocument",
"deleteDocument",
"createCollectionWithProxy",
"legacyMongoShell",
],
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
NEW_CASSANDRA_APIS: [
// "postQuery",
// "createOrDelete",
// "getKeys",
// "getSchema",
],
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
isTerminalEnabled: false,
isPhoenixEnabled: false,
@@ -231,6 +238,15 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
updateConfigContext({ platform });
}
}
if (window.location.origin !== configContext.hostedExplorerURL) {
if (window.location.origin === "https://localhost:1234") {
// Special case for localhost, we need to send them to 'hostedExplorer.html'.
updateConfigContext({ hostedExplorerURL: "https://localhost:1234/hostedExplorer.html" });
} else {
const newOrigin = window.location.origin.endsWith("/") ? window.location.origin : `${window.location.origin}/`;
updateConfigContext({ hostedExplorerURL: newOrigin });
}
}
} catch (error) {
console.error("No configuration file found using defaults");
}
@@ -238,3 +254,4 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
}
export { configContext };

View File

@@ -1,37 +1,22 @@
import { MessageTypes } from "./MessageTypes";
import { FabricMessageTypes } from "./FabricMessageTypes";
// This is the current version of these messages
export const DATA_EXPLORER_RPC_VERSION = "2";
export const DATA_EXPLORER_RPC_VERSION = "3";
// Data Explorer to Fabric
// TODO Remove when upgrading to Fabric v2
export type DataExploreMessageV1 =
| "ready"
export type DataExploreMessageV3 =
| {
type: MessageTypes.GetAuthorizationToken;
id: string;
params: GetCosmosTokenMessageOptions[];
}
| {
type: MessageTypes.GetAllResourceTokens;
id: string;
};
// -----------------------------
export type DataExploreMessageV2 =
| {
type: MessageTypes.Ready;
type: FabricMessageTypes.Ready;
id: string;
params: [string]; // version
}
| {
type: MessageTypes.GetAuthorizationToken;
type: FabricMessageTypes.GetAuthorizationToken;
id: string;
params: GetCosmosTokenMessageOptions[];
}
| {
type: MessageTypes.GetAllResourceTokens;
type: FabricMessageTypes.GetAllResourceTokens;
id: string;
};

View File

@@ -1,4 +1,4 @@
import { ConnectionStatusType, ContainerStatusType } from "../Common/Constants";
import { CapacityMode, ConnectionStatusType, ContainerStatusType } from "../Common/Constants";
export interface ArmEntity {
id: string;
@@ -35,6 +35,7 @@ export interface DatabaseAccountExtendedProperties {
ipRules?: IpRule[];
privateEndpointConnections?: unknown[];
capacity?: { totalThroughputLimit: number };
capacityMode?: CapacityMode;
locations?: DatabaseAccountResponseLocation[];
postgresqlEndpoint?: string;
publicNetworkAccess?: string;
@@ -157,6 +158,7 @@ export interface Collection extends Resource {
changeFeedPolicy?: ChangeFeedPolicy;
analyticalStorageTtl?: number;
geospatialConfig?: GeospatialConfig;
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
schema?: ISchema;
requestSchema?: () => void;
computedProperties?: ComputedProperties;
@@ -194,8 +196,14 @@ export interface IndexingPolicy {
indexingMode: "consistent" | "lazy" | "none";
includedPaths: any;
excludedPaths: any;
compositeIndexes?: any;
spatialIndexes?: any;
compositeIndexes?: any[];
spatialIndexes?: any[];
vectorIndexes?: VectorIndex[];
}
export interface VectorIndex {
path: string;
type: "flat" | "diskANN" | "quantizedFlat";
}
export interface ComputedProperty {
@@ -333,6 +341,18 @@ export interface CreateCollectionParams {
partitionKey?: PartitionKey;
uniqueKeyPolicy?: UniqueKeyPolicy;
createMongoWildcardIndex?: boolean;
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
}
export interface VectorEmbeddingPolicy {
vectorEmbeddings: VectorEmbedding[];
}
export interface VectorEmbedding {
dataType: "float16" | "float32" | "uint8" | "int8";
dimensions: number;
distanceFunction: "euclidean" | "cosine" | "dotproduct";
path: string;
}
export interface ReadDatabaseOfferParams {

View File

@@ -0,0 +1,13 @@
/**
* Data Explorer -> Fabric communication.
*/
export enum FabricMessageTypes {
GetAuthorizationToken = "GetAuthorizationToken",
GetAllResourceTokens = "GetAllResourceTokens",
Ready = "Ready",
}
export interface AuthorizationToken {
XDate: string;
PrimaryReadWriteToken: string;
}

View File

@@ -1,4 +1,4 @@
import { AuthorizationToken } from "./MessageTypes";
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
// This is the version of these messages
export const FABRIC_RPC_VERSION = "2";
@@ -53,6 +53,7 @@ export type FabricMessageV2 =
id: string;
message: {
connectionId: string;
isVisible: boolean;
};
}
| {
@@ -72,7 +73,7 @@ export type FabricMessageV2 =
};
}
| {
type: "setToolbarStatus";
type: "explorerVisible";
message: {
visible: boolean;
};

View File

@@ -1,12 +1,13 @@
/**
* Messaging types used with Data Explorer <-> Portal communication,
* Hosted <-> Explorer communication and Data Explorer -> Fabric communication.
* Hosted <-> Explorer communication
*
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
* WARNING: !!!!!!! YOU CAN ONLY ADD NEW TYPES TO THE END OF THIS ENUM !!!!!!!
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
*
* Enum are integers, so inserting or deleting a type will break the communication.
*
*/
export enum MessageTypes {
TelemetryInfo,
@@ -43,13 +44,9 @@ export enum MessageTypes {
DisplayNPSSurvey,
OpenVCoreMongoNetworkingBlade,
OpenVCoreMongoConnectionStringsBlade,
GetAuthorizationToken, // Data Explorer -> Fabric
GetAllResourceTokens, // Data Explorer -> Fabric
Ready, // Data Explorer -> Fabric
GetAuthorizationToken, // unused. Can be removed if the portal uses the same list of enums.
GetAllResourceTokens, // unused. Can be removed if the portal uses the same list of enums.
Ready, // unused. Can be removed if the portal uses the same list of enums.
OpenCESCVAFeedbackBlade,
}
export interface AuthorizationToken {
XDate: string;
PrimaryReadWriteToken: string;
ActivateTab,
}

View File

@@ -176,6 +176,11 @@ export interface Collection extends CollectionBase {
loadTriggers(): Promise<any>;
loadOffer(): Promise<void>;
showStoredProcedures: ko.Observable<boolean>;
showTriggers: ko.Observable<boolean>;
showUserDefinedFunctions: ko.Observable<boolean>;
showConflicts: ko.Observable<boolean>;
createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure;
createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction;
createTriggerNode(data: TriggerDefinition | SqlTriggerResource): Trigger;
@@ -324,9 +329,9 @@ export enum DocumentExplorerState {
noDocumentSelected,
newDocumentValid,
newDocumentInvalid,
exisitingDocumentNoEdits,
exisitingDocumentDirtyValid,
exisitingDocumentDirtyInvalid,
existingDocumentNoEdits,
existingDocumentDirtyValid,
existingDocumentDirtyInvalid,
}
export enum IndexingPolicyEditorState {
@@ -339,9 +344,9 @@ export enum IndexingPolicyEditorState {
export enum ScriptEditorState {
newInvalid,
newValid,
exisitingNoEdits,
exisitingDirtyValid,
exisitingDirtyInvalid,
existingNoEdits,
existingDirtyValid,
existingDirtyInvalid,
}
export enum CollectionTabKind {
@@ -387,6 +392,7 @@ export interface DataExplorerInputsFrame {
dnsSuffix?: string;
serverId?: string;
extensionEndpoint?: string;
portalBackendEndpoint?: string;
mongoProxyEndpoint?: string;
cassandraProxyEndpoint?: string;
subscriptionType?: SubscriptionType;

View File

@@ -1,3 +1,4 @@
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import { useDatabases } from "Explorer/useDatabases";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
@@ -19,7 +20,6 @@ import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { useSidePanel } from "../hooks/useSidePanel";
import { Platform, configContext } from "./../ConfigContext";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import Explorer from "./Explorer";
import { useNotebook } from "./Notebook/useNotebook";
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";

View File

@@ -26,8 +26,8 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
this.setState({ isExpanded: !this.state.isExpanded });
};
public componentDidUpdate(): void {
if (this.state.isExpanded && this.props.onExpand) {
public componentDidUpdate(_prevProps: CollapsibleSectionProps, prevState: CollapsibleSectionState): void {
if (!prevState.isExpanded && this.state.isExpanded && this.props.onExpand) {
this.props.onExpand();
}
}
@@ -43,7 +43,7 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
return (
<>
<Stack
className="collapsibleSection"
className={"collapsibleSection"}
horizontal
verticalAlign="center"
tokens={accordionStackTokens}

View File

@@ -1,6 +1,7 @@
/**
* React component for Command button component.
*/
import { KeyboardAction } from "KeyboardShortcuts";
import * as React from "react";
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
import { KeyCodes } from "../../../Common/Constants";
@@ -30,7 +31,7 @@ export interface CommandButtonComponentProps {
/**
* Click handler for command button click
*/
onCommandClick: (e: React.SyntheticEvent) => void;
onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent) => void;
/**
* Label for the button
@@ -107,10 +108,17 @@ export interface CommandButtonComponentProps {
* Vertical bar to divide buttons
*/
isDivider?: boolean;
/**
* Aria-label for the button
*/
ariaLabel: string;
/**
* If specified, a keyboard action that should trigger this button's onCommandClick handler when activated.
* If not specified, the button will not be triggerable by keyboard shortcuts.
*/
keyboardAction?: KeyboardAction;
}
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {

View File

@@ -20,7 +20,10 @@ export interface EditorReactProps {
lineDecorationsWidth?: monaco.editor.IEditorOptions["lineDecorationsWidth"];
minimap?: monaco.editor.IEditorOptions["minimap"];
scrollBeyondLastLine?: monaco.editor.IEditorOptions["scrollBeyondLastLine"];
fontSize?: monaco.editor.IEditorOptions["fontSize"];
monacoContainerStyles?: React.CSSProperties;
className?: string;
spinnerClassName?: string;
}
export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> {
@@ -46,9 +49,25 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
}, 100);
}
public componentDidUpdate(previous: EditorReactProps) {
if (this.props.content !== previous.content) {
this.editor?.setValue(this.props.content);
public componentDidUpdate() {
if (!this.editor) {
return;
}
const existingContent = this.editor.getModel().getValue();
if (this.props.content !== existingContent) {
if (this.props.isReadOnly) {
this.editor.setValue(this.props.content);
} else {
this.editor.pushUndoStop();
this.editor.executeEdits("", [
{
range: this.editor.getModel().getFullModelRange(),
text: this.props.content,
},
]);
}
}
}
@@ -59,9 +78,11 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
public render(): JSX.Element {
return (
<React.Fragment>
{!this.state.showEditor && <Spinner size={SpinnerSize.large} className="spinner" />}
{!this.state.showEditor && (
<Spinner size={SpinnerSize.large} className={this.props.spinnerClassName || "spinner"} />
)}
<div
className="jsonEditor"
className={this.props.className || "jsonEditor"}
style={this.props.monacoContainerStyles}
ref={(elt: HTMLElement) => this.setRef(elt)}
/>
@@ -71,9 +92,14 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
this.editor = editor;
const queryEditorModel = this.editor.getModel();
if (!this.props.isReadOnly && this.props.onContentChanged) {
queryEditorModel.onDidChangeContent(() => {
// Hooking the model's onDidChangeContent event because of some event ordering issues.
// If a single user input causes BOTH the editor content to change AND the cursor selection to change (which is likely),
// then there are some inconsistencies as to which event fires first.
// But the editor.onDidChangeModelContent event seems to always fire before the cursor selection event.
// (This is NOT true for the model's onDidChangeContent event, which sometimes fires after the cursor selection event.)
// If the cursor selection event fires first, then the calling component may re-render the component with old content, so we want to ensure the model content changed event always fires first.
this.editor.onDidChangeModelContent(() => {
const queryEditorModel = this.editor.getModel();
this.props.onContentChanged(queryEditorModel.getValue());
});
@@ -98,7 +124,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
value: this.props.content,
readOnly: this.props.isReadOnly,
ariaLabel: this.props.ariaLabel,
fontSize: 12,
fontSize: this.props.fontSize || 12,
automaticLayout: true,
theme: this.props.theme,
wordWrap: this.props.wordWrap || "off",
@@ -111,7 +137,13 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
this.rootNode.innerHTML = "";
const monaco = await loadMonaco();
try {
createCallback(monaco?.editor?.create(this.rootNode, options));
} catch (error) {
// This could happen if the parent node suddenly disappears during create()
console.error("Unable to create EditorReact", error);
return;
}
if (this.rootNode.innerHTML) {
this.setState({

View File

@@ -25,7 +25,14 @@
font-family: @DataExplorerFont;
}
.settingsV2IndexingPolicyEditor {
.settingsV2Editor {
width: 100%;
height: 60vh;
}
.settingsV2EditorSpinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

View File

@@ -3,7 +3,12 @@ import {
ComputedPropertiesComponent,
ComputedPropertiesComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent";
import {
ContainerVectorPolicyComponent,
ContainerVectorPolicyComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent";
import { useDatabases } from "Explorer/useDatabases";
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
import * as React from "react";
import DiscardIcon from "../../../../images/discard.svg";
@@ -144,6 +149,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private shouldShowComputedPropertiesEditor: boolean;
private shouldShowIndexingPolicyEditor: boolean;
private shouldShowPartitionKeyEditor: boolean;
private isVectorSearchEnabled: boolean;
private totalThroughputUsed: number;
public mongoDBCollectionResource: MongoDBCollectionResource;
@@ -158,6 +164,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
@@ -1097,6 +1104,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
indexTransformationProgress: this.state.indexTransformationProgress,
refreshIndexTransformationProgress: this.refreshIndexTransformationProgress,
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange,
isVectorSearchEnabled: this.isVectorSearchEnabled,
};
const mongoIndexingPolicyComponentProps: MongoIndexingPolicyComponentProps = {
@@ -1143,6 +1151,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
explorer: this.props.settingsTab.getContainer(),
};
const containerVectorPolicyProps: ContainerVectorPolicyComponentProps = {
vectorEmbeddingPolicy: this.collection.rawDataModel?.vectorEmbeddingPolicy,
};
const tabs: SettingsV2TabInfo[] = [];
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
tabs.push({
@@ -1156,6 +1168,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
content: <SubSettingsComponent {...subSettingsComponentProps} />,
});
if (this.isVectorSearchEnabled) {
tabs.push({
tab: SettingsV2TabTypes.ContainerVectorPolicyTab,
content: <ContainerVectorPolicyComponent {...containerVectorPolicyProps} />,
});
}
if (this.shouldShowIndexingPolicyEditor) {
tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab,

View File

@@ -121,7 +121,7 @@ export class ComputedPropertiesComponent extends React.Component<
</Link>
&#160; about how to define computed properties and how to use them.
</Text>
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.computedPropertiesDiv}></div>
<div className="settingsV2Editor" tabIndex={0} ref={this.computedPropertiesDiv}></div>
</Stack>
);
}

View File

@@ -0,0 +1,30 @@
import { Stack } from "@fluentui/react";
import { VectorEmbeddingPolicy } from "Contracts/DataModels";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils";
import React from "react";
export interface ContainerVectorPolicyComponentProps {
vectorEmbeddingPolicy: VectorEmbeddingPolicy;
}
export const ContainerVectorPolicyComponent: React.FC<ContainerVectorPolicyComponentProps> = ({
vectorEmbeddingPolicy,
}) => {
return (
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative" } }}>
<EditorReact
language={"json"}
content={JSON.stringify(vectorEmbeddingPolicy || {}, null, 4)}
isReadOnly={true}
wordWrap={"on"}
ariaLabel={"Container vector policy"}
lineNumbers={"on"}
scrollBeyondLastLine={false}
className={"settingsV2Editor"}
spinnerClassName={"settingsV2EditorSpinner"}
fontSize={14}
/>
</Stack>
);
};

View File

@@ -16,6 +16,7 @@ export interface IndexingPolicyComponentProps {
logIndexingPolicySuccessMessage: () => void;
indexTransformationProgress: number;
refreshIndexTransformationProgress: () => Promise<void>;
isVectorSearchEnabled?: boolean;
onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void;
}
@@ -119,10 +120,15 @@ export class IndexingPolicyComponent extends React.Component<
indexTransformationProgress={this.props.indexTransformationProgress}
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
/>
{this.props.isVectorSearchEnabled && (
<MessageBar messageBarType={MessageBarType.severeWarning}>
Container vector policies and vector indexes are not modifiable after container creation
</MessageBar>
)}
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
)}
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
<div className="settingsV2Editor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
</Stack>
);
}

View File

@@ -136,15 +136,15 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ da
};
const getPercentageComplete = () => {
const jobStatus = portalDataTransferJob?.properties?.status;
const isCompleted = jobStatus === "Completed";
if (isCompleted) {
return 1;
}
const processedCount = portalDataTransferJob?.properties?.processedCount;
const totalCount = portalDataTransferJob?.properties?.totalCount;
const jobStatus = portalDataTransferJob?.properties?.status;
const isCancelled = jobStatus === "Cancelled";
const isCompleted = jobStatus === "Completed";
if (totalCount <= 0 && !isCompleted) {
return isCancelled ? 0 : null;
}
return isCompleted ? 1 : processedCount / totalCount;
const isJobInProgress = isCurrentJobInProgress(portalDataTransferJob);
return isJobInProgress ? (totalCount > 0 ? processedCount / totalCount : null) : 0;
};
return (

View File

@@ -29,7 +29,7 @@ exports[`ComputedPropertiesComponent renders 1`] = `
  about how to define computed properties and how to use them.
</Text>
<div
className="settingsV2IndexingPolicyEditor"
className="settingsV2Editor"
tabIndex={0}
/>
</Stack>

View File

@@ -12,7 +12,7 @@ exports[`IndexingPolicyComponent renders 1`] = `
refreshIndexTransformationProgress={[Function]}
/>
<div
className="settingsV2IndexingPolicyEditor"
className="settingsV2Editor"
tabIndex={0}
/>
</Stack>

View File

@@ -47,6 +47,7 @@ export enum SettingsV2TabTypes {
IndexingPolicyTab,
PartitionKeyTab,
ComputedPropertiesTab,
ContainerVectorPolicyTab,
}
export interface IsComponentDirtyResult {
@@ -149,9 +150,11 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
case SettingsV2TabTypes.IndexingPolicyTab:
return "Indexing Policy";
case SettingsV2TabTypes.PartitionKeyTab:
return "Partition Keys";
return "Partition Keys (preview)";
case SettingsV2TabTypes.ComputedPropertiesTab:
return "Computed Properties (preview)";
return "Computed Properties";
case SettingsV2TabTypes.ContainerVectorPolicyTab:
return "Container Vector Policy (preview)";
default:
throw new Error(`Unknown tab ${tab}`);
}

View File

@@ -30,7 +30,6 @@ exports[`SettingsComponent renders 1`] = `
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
@@ -108,7 +107,6 @@ exports[`SettingsComponent renders 1`] = `
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
@@ -198,6 +196,7 @@ exports[`SettingsComponent renders 1`] = `
"indexingMode": "consistent",
}
}
isVectorSearchEnabled={false}
logIndexingPolicySuccessMessage={[Function]}
onIndexingPolicyContentChange={[Function]}
onIndexingPolicyDirtyChange={[Function]}
@@ -207,7 +206,7 @@ exports[`SettingsComponent renders 1`] = `
/>
</PivotItem>
<PivotItem
headerText="Partition Keys"
headerText="Partition Keys (preview)"
itemKey="PartitionKeyTab"
key="PartitionKeyTab"
style={
@@ -225,7 +224,6 @@ exports[`SettingsComponent renders 1`] = `
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
@@ -272,7 +270,6 @@ exports[`SettingsComponent renders 1`] = `
explorer={
Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
@@ -300,7 +297,7 @@ exports[`SettingsComponent renders 1`] = `
/>
</PivotItem>
<PivotItem
headerText="Computed Properties (preview)"
headerText="Computed Properties"
itemKey="ComputedPropertiesTab"
key="ComputedPropertiesTab"
style={

View File

@@ -223,6 +223,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
<Text variant="small" aria-label="capacity calculator of azure cosmos db">
Estimate your required RU/s with{" "}
<Link
className="underlinedLink"
target="_blank"
href="https://cosmos.azure.com/capacitycalculator/"
aria-label="capacity calculator of azure cosmos db"
@@ -271,7 +272,12 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
<Stack className="throughputInputSpacing">
<Text variant="small" aria-label="ruDescription">
Estimate your required RU/s with&nbsp;
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/" aria-label="Capacity calculator">
<Link
className="underlinedLink"
target="_blank"
href="https://cosmos.azure.com/capacitycalculator/"
aria-label="Capacity calculator"
>
capacity calculator
</Link>
.

View File

@@ -733,11 +733,13 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
<StyledLinkBase
aria-label="capacity calculator of azure cosmos db"
className="underlinedLink"
href="https://cosmos.azure.com/capacitycalculator/"
target="_blank"
>
<LinkBase
aria-label="capacity calculator of azure cosmos db"
className="underlinedLink"
href="https://cosmos.azure.com/capacitycalculator/"
styles={[Function]}
target="_blank"
@@ -1017,7 +1019,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
>
<a
aria-label="capacity calculator of azure cosmos db"
className="ms-Link root-117"
className="ms-Link underlinedLink root-117"
href="https://cosmos.azure.com/capacitycalculator/"
onClick={[Function]}
target="_blank"

View File

@@ -1,48 +1,48 @@
import React from "react";
import { shallow } from "enzyme";
import { TreeComponent, TreeNode, TreeNodeComponent } from "./TreeComponent";
import React from "react";
import { LegacyTreeComponent, LegacyTreeNode, LegacyTreeNodeComponent } from "./LegacyTreeComponent";
const buildChildren = (): TreeNode[] => {
const grandChild11: TreeNode = {
const buildChildren = (): LegacyTreeNode[] => {
const grandChild11: LegacyTreeNode = {
label: "ZgrandChild11",
};
const grandChild12: TreeNode = {
const grandChild12: LegacyTreeNode = {
label: "AgrandChild12",
};
const child1: TreeNode = {
const child1: LegacyTreeNode = {
label: "Bchild1",
children: [grandChild11, grandChild12],
};
const child2: TreeNode = {
const child2: LegacyTreeNode = {
label: "2child2",
};
return [child1, child2];
};
const buildChildren2 = (): TreeNode[] => {
const grandChild11: TreeNode = {
const buildChildren2 = (): LegacyTreeNode[] => {
const grandChild11: LegacyTreeNode = {
label: "ZgrandChild11",
};
const grandChild12: TreeNode = {
const grandChild12: LegacyTreeNode = {
label: "AgrandChild12",
};
const child1: TreeNode = {
const child1: LegacyTreeNode = {
label: "aChild",
};
const child2: TreeNode = {
const child2: LegacyTreeNode = {
label: "bchild",
children: [grandChild11, grandChild12],
};
const child3: TreeNode = {
const child3: LegacyTreeNode = {
label: "cchild",
};
const child4: TreeNode = {
const child4: LegacyTreeNode = {
label: "dchild",
children: [grandChild11, grandChild12],
};
@@ -50,7 +50,7 @@ const buildChildren2 = (): TreeNode[] => {
return [child1, child2, child3, child4];
};
describe("TreeComponent", () => {
describe("LegacyTreeComponent", () => {
it("renders a simple tree", () => {
const root = {
label: "root",
@@ -62,14 +62,14 @@ describe("TreeComponent", () => {
className: "tree",
};
const wrapper = shallow(<TreeComponent {...props} />);
const wrapper = shallow(<LegacyTreeComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
describe("TreeNodeComponent", () => {
describe("LegacyTreeNodeComponent", () => {
it("renders a simple node (sorted children, expanded)", () => {
const node: TreeNode = {
const node: LegacyTreeNode = {
label: "label",
id: "id",
children: buildChildren(),
@@ -98,12 +98,12 @@ describe("TreeNodeComponent", () => {
generation: 12,
paddingLeft: 23,
};
const wrapper = shallow(<TreeNodeComponent {...props} />);
const wrapper = shallow(<LegacyTreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders unsorted children by default", () => {
const node: TreeNode = {
const node: LegacyTreeNode = {
label: "label",
children: buildChildren(),
isExpanded: true,
@@ -113,12 +113,12 @@ describe("TreeNodeComponent", () => {
generation: 2,
paddingLeft: 9,
};
const wrapper = shallow(<TreeNodeComponent {...props} />);
const wrapper = shallow(<LegacyTreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("does not render children by default", () => {
const node: TreeNode = {
const node: LegacyTreeNode = {
label: "label",
children: buildChildren(),
isAlphaSorted: false,
@@ -128,12 +128,12 @@ describe("TreeNodeComponent", () => {
generation: 2,
paddingLeft: 9,
};
const wrapper = shallow(<TreeNodeComponent {...props} />);
const wrapper = shallow(<LegacyTreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders sorted children, expanded, leaves and parents separated", () => {
const node: TreeNode = {
const node: LegacyTreeNode = {
label: "label",
id: "id",
children: buildChildren2(),
@@ -156,12 +156,12 @@ describe("TreeNodeComponent", () => {
generation: 12,
paddingLeft: 23,
};
const wrapper = shallow(<TreeNodeComponent {...props} />);
const wrapper = shallow(<LegacyTreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders loading icon", () => {
const node: TreeNode = {
const node: LegacyTreeNode = {
label: "label",
children: [],
isExpanded: true,
@@ -172,7 +172,7 @@ describe("TreeNodeComponent", () => {
generation: 2,
paddingLeft: 9,
};
const wrapper = shallow(<TreeNodeComponent {...props} />);
const wrapper = shallow(<LegacyTreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -12,6 +12,7 @@ import {
IContextualMenuItemProps,
IContextualMenuProps,
} from "@fluentui/react";
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import * as React from "react";
import AnimateHeight from "react-animate-height";
import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
@@ -22,18 +23,10 @@ import { StyleConstants } from "../../../Common/StyleConstants";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
export interface TreeNodeMenuItem {
label: string;
onClick: () => void;
iconSrc?: string;
isDisabled?: boolean;
styleClass?: string;
}
export interface TreeNode {
export interface LegacyTreeNode {
label: string;
id?: string;
children?: TreeNode[];
children?: LegacyTreeNode[];
contextMenu?: TreeNodeMenuItem[];
iconSrc?: string;
isExpanded?: boolean;
@@ -50,34 +43,37 @@ export interface TreeNode {
onContextMenuOpen?: () => void;
}
export interface TreeComponentProps {
rootNode: TreeNode;
export interface LegacyTreeComponentProps {
rootNode: LegacyTreeNode;
style?: any;
className?: string;
}
export class TreeComponent extends React.Component<TreeComponentProps> {
export class LegacyTreeComponent extends React.Component<LegacyTreeComponentProps> {
public render(): JSX.Element {
return (
<div style={this.props.style} className={`treeComponent ${this.props.className}`} role="tree">
<TreeNodeComponent paddingLeft={0} node={this.props.rootNode} generation={0} />
<LegacyTreeNodeComponent paddingLeft={0} node={this.props.rootNode} generation={0} />
</div>
);
}
}
/* Tree node is a react component */
interface TreeNodeComponentProps {
node: TreeNode;
interface LegacyTreeNodeComponentProps {
node: LegacyTreeNode;
generation: number;
paddingLeft: number;
}
interface TreeNodeComponentState {
interface LegacyTreeNodeComponentState {
isExpanded: boolean;
isMenuShowing: boolean;
}
export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, TreeNodeComponentState> {
export class LegacyTreeNodeComponent extends React.Component<
LegacyTreeNodeComponentProps,
LegacyTreeNodeComponentState
> {
private static readonly paddingPerGenerationPx = 16;
private static readonly iconOffset = 22;
private static readonly transitionDurationMS = 200;
@@ -85,7 +81,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
private contextMenuRef = React.createRef<HTMLDivElement>();
private isExpanded: boolean;
constructor(props: TreeNodeComponentProps) {
constructor(props: LegacyTreeNodeComponentProps) {
super(props);
this.isExpanded = props.node.isExpanded;
this.state = {
@@ -94,13 +90,13 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
};
}
componentDidUpdate(prevProps: TreeNodeComponentProps, prevState: TreeNodeComponentState) {
componentDidUpdate(prevProps: LegacyTreeNodeComponentProps, prevState: LegacyTreeNodeComponentState) {
// Only call when expand has actually changed
if (this.state.isExpanded !== prevState.isExpanded) {
if (this.state.isExpanded) {
this.props.node.onExpanded && setTimeout(this.props.node.onExpanded, TreeNodeComponent.callbackDelayMS);
this.props.node.onExpanded && setTimeout(this.props.node.onExpanded, LegacyTreeNodeComponent.callbackDelayMS);
} else {
this.props.node.onCollapsed && setTimeout(this.props.node.onCollapsed, TreeNodeComponent.callbackDelayMS);
this.props.node.onCollapsed && setTimeout(this.props.node.onCollapsed, LegacyTreeNodeComponent.callbackDelayMS);
}
}
if (this.props.node.isExpanded !== this.isExpanded) {
@@ -115,18 +111,18 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
return this.renderNode(this.props.node, this.props.generation);
}
private static getSortedChildren(treeNode: TreeNode): TreeNode[] {
private static getSortedChildren(treeNode: LegacyTreeNode): LegacyTreeNode[] {
if (!treeNode || !treeNode.children) {
return undefined;
}
const compareFct = (a: TreeNode, b: TreeNode) => a.label.localeCompare(b.label);
const compareFct = (a: LegacyTreeNode, b: LegacyTreeNode) => a.label.localeCompare(b.label);
let unsortedChildren;
if (treeNode.isLeavesParentsSeparate) {
// Separate parents and leave
const parents: TreeNode[] = treeNode.children.filter((node) => node.children);
const leaves: TreeNode[] = treeNode.children.filter((node) => !node.children);
const parents: LegacyTreeNode[] = treeNode.children.filter((node) => node.children);
const leaves: LegacyTreeNode[] = treeNode.children.filter((node) => !node.children);
if (treeNode.isAlphaSorted) {
parents.sort(compareFct);
@@ -141,18 +137,18 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
return unsortedChildren;
}
private static isNodeHeaderBlank(node: TreeNode): boolean {
private static isNodeHeaderBlank(node: LegacyTreeNode): boolean {
return (node.label === undefined || node.label === null) && !node.contextMenu;
}
private renderNode(node: TreeNode, generation: number): JSX.Element {
let paddingLeft = generation * TreeNodeComponent.paddingPerGenerationPx;
private renderNode(node: LegacyTreeNode, generation: number): JSX.Element {
const paddingLeft = generation * LegacyTreeNodeComponent.paddingPerGenerationPx;
let additionalOffsetPx = 15;
if (node.children) {
const childrenWithSubChildren = node.children.filter((child: TreeNode) => !!child.children);
const childrenWithSubChildren = node.children.filter((child: LegacyTreeNode) => !!child.children);
if (childrenWithSubChildren.length > 0) {
additionalOffsetPx = TreeNodeComponent.iconOffset;
additionalOffsetPx = LegacyTreeNodeComponent.iconOffset;
}
}
@@ -160,16 +156,17 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
const showSelected =
this.props.node.isSelected &&
this.props.node.isSelected() &&
!TreeNodeComponent.isAnyDescendantSelected(this.props.node);
!LegacyTreeNodeComponent.isAnyDescendantSelected(this.props.node);
const headerStyle: React.CSSProperties = { paddingLeft: this.props.paddingLeft };
if (TreeNodeComponent.isNodeHeaderBlank(node)) {
if (LegacyTreeNodeComponent.isNodeHeaderBlank(node)) {
headerStyle.height = 0;
headerStyle.padding = 0;
}
return (
<div
data-test={`Tree/TreeNode:${node.label}`}
className={`${this.props.node.className || ""} main${generation} nodeItem ${showSelected ? "selected" : ""}`}
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.onNodeClick(event, node)}
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) => this.onNodeKeyPress(event, node)}
@@ -178,9 +175,9 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
>
<div
className={`treeNodeHeader ${this.state.isMenuShowing ? "showingMenu" : ""}`}
data-test={`Tree/TreeNode/Header:${node.label}`}
style={headerStyle}
tabIndex={node.children ? -1 : 0}
data-test={node.label}
>
{this.renderCollapseExpandIcon(node)}
{node.iconSrc && <img className="nodeIcon" src={node.iconSrc} alt="" />}
@@ -195,10 +192,13 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
<img className="loadingIcon" src={LoadingIndicator_3Squares} hidden={!this.props.node.isLoading} />
</div>
{node.children && (
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
<AnimateHeight
duration={LegacyTreeNodeComponent.transitionDurationMS}
height={this.state.isExpanded ? "auto" : 0}
>
<div className="nodeChildren" data-test={node.label} role="group">
{TreeNodeComponent.getSortedChildren(node).map((childNode: TreeNode) => (
<TreeNodeComponent
{LegacyTreeNodeComponent.getSortedChildren(node).map((childNode: LegacyTreeNode) => (
<LegacyTreeNodeComponent
key={`${childNode.label}-${generation + 1}-${childNode.timestamp}`}
node={childNode}
generation={generation + 1}
@@ -216,12 +216,14 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
* Recursive: is the node or any descendant selected
* @param node
*/
private static isAnyDescendantSelected(node: TreeNode): boolean {
private static isAnyDescendantSelected(node: LegacyTreeNode): boolean {
return (
node.children &&
node.children.reduce(
(previous: boolean, child: TreeNode) =>
previous || (child.isSelected && child.isSelected()) || TreeNodeComponent.isAnyDescendantSelected(child),
(previous: boolean, child: LegacyTreeNode) =>
previous ||
(child.isSelected && child.isSelected()) ||
LegacyTreeNodeComponent.isAnyDescendantSelected(child),
false,
)
);
@@ -232,10 +234,10 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
}
private onRightClick = (): void => {
this.contextMenuRef.current.firstChild.dispatchEvent(TreeNodeComponent.createClickEvent());
this.contextMenuRef.current.firstChild.dispatchEvent(LegacyTreeNodeComponent.createClickEvent());
};
private renderContextMenuButton(node: TreeNode): JSX.Element {
private renderContextMenuButton(node: LegacyTreeNode): JSX.Element {
const menuItemLabel = "More";
const buttonStyles: Partial<IButtonStyles> = {
rootFocused: { outline: `1px dashed ${StyleConstants.FocusColor}` },
@@ -263,9 +265,9 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
onMenuDismissed: (contextualMenu?: IContextualMenuProps) => this.setState({ isMenuShowing: false }),
contextualMenuItemAs: (props: IContextualMenuItemProps) => (
<div
data-test={`treeComponentMenuItemContainer`}
data-test={`Tree/TreeNode/MenuItem:${props.item.text}`}
className="treeComponentMenuItemContainer"
onContextMenu={(e) => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())}
onContextMenu={(e) => e.target.dispatchEvent(LegacyTreeNodeComponent.createClickEvent())}
>
{props.item.onRenderIcon()}
<span
@@ -297,7 +299,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
);
}
private renderCollapseExpandIcon(node: TreeNode): JSX.Element {
private renderCollapseExpandIcon(node: LegacyTreeNode): JSX.Element {
if (!node.children || !node.label) {
return <></>;
}
@@ -314,12 +316,12 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
);
}
private onNodeClick = (event: React.MouseEvent<HTMLDivElement>, node: TreeNode): void => {
private onNodeClick = (event: React.MouseEvent<HTMLDivElement>, node: LegacyTreeNode): void => {
event.stopPropagation();
if (node.children) {
const isExpanded = !this.state.isExpanded;
// Prevent collapsing if node header is blank
if (!(TreeNodeComponent.isNodeHeaderBlank(node) && !isExpanded)) {
if (!(LegacyTreeNodeComponent.isNodeHeaderBlank(node) && !isExpanded)) {
this.setState({ isExpanded });
}
}
@@ -327,14 +329,14 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
this.props.node.onClick && this.props.node.onClick(this.state.isExpanded);
};
private onNodeKeyPress = (event: React.KeyboardEvent<HTMLDivElement>, node: TreeNode): void => {
private onNodeKeyPress = (event: React.KeyboardEvent<HTMLDivElement>, node: LegacyTreeNode): void => {
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
event.stopPropagation();
this.props.node.onClick && this.props.node.onClick(this.state.isExpanded);
}
};
private onCollapseExpandIconKeyPress = (event: React.KeyboardEvent<HTMLDivElement>, node: TreeNode): void => {
private onCollapseExpandIconKeyPress = (event: React.KeyboardEvent<HTMLDivElement>, node: LegacyTreeNode): void => {
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
event.stopPropagation();
if (node.children) {

View File

@@ -0,0 +1,194 @@
import { TreeItem, TreeItemLayout, tokens } from "@fluentui/react-components";
import PromiseSource from "Utils/PromiseSource";
import { mount, shallow } from "enzyme";
import React from "react";
import { act } from "react-dom/test-utils";
import { TreeNode, TreeNodeComponent } from "./TreeNodeComponent";
function generateTestNode(id: string, additionalProps?: Partial<TreeNode>): TreeNode {
const node: TreeNode = {
id,
label: `${id}Label`,
className: `${id}Class`,
iconSrc: `${id}Icon`,
onClick: jest.fn().mockName(`${id}Click`),
...additionalProps,
};
return node;
}
describe("TreeNodeComponent", () => {
it("renders a single node", () => {
const node = generateTestNode("root");
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
expect(component).toMatchSnapshot();
// The "click" handler is actually attached to onOpenChange, with a type of "Click".
component
.find(TreeItem)
.props()
.onOpenChange(null!, { open: true, value: "borp", target: null!, event: null!, type: "Click" });
expect(node.onClick).toHaveBeenCalled();
});
it("renders a node with a menu", () => {
const node = generateTestNode("root", {
contextMenu: [
{
label: "enabledItem",
onClick: jest.fn().mockName("enabledItemClick"),
},
{
label: "disabledItem",
onClick: jest.fn().mockName("disabledItemClick"),
isDisabled: true,
},
],
});
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
expect(component).toMatchSnapshot();
});
it("renders a loading spinner if the node is loading", async () => {
const loading = new PromiseSource();
const node = generateTestNode("root", {
onExpanded: () => loading.promise,
});
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
act(() => {
component
.find(TreeItem)
.props()
.onOpenChange(null!, { open: true, value: "borp", target: null!, event: null!, type: "ExpandIconClick" });
});
expect(component).toMatchSnapshot("loading");
await loading.resolveAndWait();
expect(component).toMatchSnapshot("loaded");
});
it("renders single selected leaf node as selected", () => {
const node = generateTestNode("root", {
isSelected: () => true,
});
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
expect(component.find(TreeItemLayout).props().style?.backgroundColor).toStrictEqual(
tokens.colorNeutralBackground1Selected,
);
expect(component).toMatchSnapshot();
});
it("renders selected parent node as selected if no descendant nodes are selected", () => {
const node = generateTestNode("root", {
isSelected: () => true,
children: [
generateTestNode("child1", {
children: [generateTestNode("grandchild1"), generateTestNode("grandchild2")],
}),
generateTestNode("child2"),
],
});
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
expect(component.find(TreeItemLayout).props().style?.backgroundColor).toStrictEqual(
tokens.colorNeutralBackground1Selected,
);
expect(component).toMatchSnapshot();
});
it("renders selected parent node as unselected if any descendant node is selected", () => {
const node = generateTestNode("root", {
isSelected: () => true,
children: [
generateTestNode("child1", {
children: [
generateTestNode("grandchild1", {
isSelected: () => true,
}),
generateTestNode("grandchild2"),
],
}),
generateTestNode("child2"),
],
});
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
expect(component.find(TreeItemLayout).props().style?.backgroundColor).toBeUndefined();
expect(component).toMatchSnapshot();
});
it("renders an icon if the node has one", () => {
const node = generateTestNode("root", {
iconSrc: "the-icon.svg",
});
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
expect(component).toMatchSnapshot();
});
it("renders a node as expandable if it has empty, but defined, children array", () => {
const node = generateTestNode("root", {
isLoading: true,
children: [
generateTestNode("child1", {
children: [],
}),
generateTestNode("child2"),
],
});
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
expect(component).toMatchSnapshot();
});
it("does not render children if the node is loading", () => {
const node = generateTestNode("root", {
isLoading: true,
children: [
generateTestNode("child1", {
children: [generateTestNode("grandchild1"), generateTestNode("grandchild2")],
}),
generateTestNode("child2"),
],
});
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
expect(component).toMatchSnapshot();
});
it("fully renders a tree", () => {
const child3Loading = new PromiseSource();
const node = generateTestNode("root", {
isSelected: () => true,
children: [
generateTestNode("child1", {
children: [
generateTestNode("grandchild1", {
iconSrc: "grandchild1Icon.svg",
isSelected: () => true,
}),
generateTestNode("grandchild2"),
],
}),
generateTestNode("child2Loading", {
isLoading: true,
children: [generateTestNode("grandchild3NotRendered")],
}),
generateTestNode("child3Expanding", {
onExpanded: () => child3Loading.promise,
}),
],
});
const component = mount(<TreeNodeComponent node={node} treeNodeId={node.id} />);
// Find and expand the child3Expanding node
const expandingChild = component.find(TreeItem).filterWhere((n) => n.props().value === "root/child3ExpandingLabel");
act(() => {
expandingChild.props().onOpenChange(null!, {
open: true,
value: "root/child3ExpandingLabel",
target: null!,
event: null!,
type: "Click",
});
});
expect(component).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,207 @@
import {
Button,
Menu,
MenuItem,
MenuList,
MenuOpenChangeData,
MenuOpenEvent,
MenuPopover,
MenuTrigger,
Spinner,
Tree,
TreeItem,
TreeItemLayout,
TreeOpenChangeData,
TreeOpenChangeEvent,
} from "@fluentui/react-components";
import { MoreHorizontal20Regular } from "@fluentui/react-icons";
import { tokens } from "@fluentui/react-theme";
import * as React from "react";
import { useCallback } from "react";
export interface TreeNodeMenuItem {
label: string;
onClick: () => void;
iconSrc?: string;
isDisabled?: boolean;
styleClass?: string;
}
export interface TreeNode {
label: string;
id?: string;
children?: TreeNode[];
contextMenu?: TreeNodeMenuItem[];
iconSrc?: string;
isExpanded?: boolean;
className?: string;
isAlphaSorted?: boolean;
// data?: any; // Piece of data corresponding to this node
timestamp?: number;
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
isLoading?: boolean;
isScrollable?: boolean;
isSelected?: () => boolean;
onClick?: () => void; // Only if a leaf, other click will expand/collapse
onExpanded?: () => Promise<void>;
onCollapsed?: () => void;
onContextMenuOpen?: () => void;
}
export interface TreeNodeComponentProps {
node: TreeNode;
className?: string;
treeNodeId: string;
}
/** Function that returns true if any descendant (at any depth) of this node is selected. */
function isAnyDescendantSelected(node: TreeNode): boolean {
return (
node.children &&
node.children.reduce(
(previous: boolean, child: TreeNode) =>
previous || (child.isSelected && child.isSelected()) || isAnyDescendantSelected(child),
false,
)
);
}
const getTreeIcon = (iconSrc: string): JSX.Element => <img src={iconSrc} alt="" style={{ width: 16, height: 16 }} />;
export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
node,
treeNodeId,
}: TreeNodeComponentProps): JSX.Element => {
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const getSortedChildren = (treeNode: TreeNode): TreeNode[] => {
if (!treeNode || !treeNode.children) {
return undefined;
}
const compareFct = (a: TreeNode, b: TreeNode) => a.label.localeCompare(b.label);
let unsortedChildren;
if (treeNode.isLeavesParentsSeparate) {
// Separate parents and leave
const parents: TreeNode[] = treeNode.children.filter((node) => node.children);
const leaves: TreeNode[] = treeNode.children.filter((node) => !node.children);
if (treeNode.isAlphaSorted) {
parents.sort(compareFct);
leaves.sort(compareFct);
}
unsortedChildren = parents.concat(leaves);
} else {
unsortedChildren = treeNode.isAlphaSorted ? treeNode.children.sort(compareFct) : treeNode.children;
}
return unsortedChildren;
};
// A branch node is any node with a defined children array, even if the array is empty.
const isBranch = !!node.children;
const onOpenChange = useCallback(
(_: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
if (data.type === "Click" && !isBranch && node.onClick) {
node.onClick();
}
if (!node.isExpanded && data.open && node.onExpanded) {
// Catch the transition non-expanded to expanded
setIsLoading(true);
node.onExpanded?.().then(() => setIsLoading(false));
} else if (node.isExpanded && !data.open && node.onCollapsed) {
// Catch the transition expanded to non-expanded
node.onCollapsed?.();
}
},
[isBranch, node, setIsLoading],
);
const onMenuOpenChange = useCallback(
(e: MenuOpenEvent, data: MenuOpenChangeData) => {
if (data.open) {
node.onContextMenuOpen?.();
}
},
[node],
);
// We show a node as selected if it is selected AND no descendant is selected.
// We want to show only the deepest selected node as selected.
const isCurrentNodeSelected = node.isSelected && node.isSelected();
const shouldShowAsSelected = isCurrentNodeSelected && !isAnyDescendantSelected(node);
const contextMenuItems = (node.contextMenu ?? []).map((menuItem) => (
<MenuItem
data-test={`TreeNode/ContextMenuItem:${menuItem.label}`}
disabled={menuItem.isDisabled}
key={menuItem.label}
onClick={menuItem.onClick}
>
{menuItem.label}
</MenuItem>
));
const treeItem = (
<TreeItem
data-test={`TreeNodeContainer:${treeNodeId}`}
value={treeNodeId}
itemType={isBranch ? "branch" : "leaf"}
onOpenChange={onOpenChange}
>
<TreeItemLayout
className={node.className}
data-test={`TreeNode:${treeNodeId}`}
actions={
contextMenuItems.length > 0 && (
<Menu onOpenChange={onMenuOpenChange}>
<MenuTrigger disableButtonEnhancement>
<Button
aria-label="More options"
data-test="TreeNode/ContextMenuTrigger"
appearance="subtle"
icon={<MoreHorizontal20Regular />}
/>
</MenuTrigger>
<MenuPopover data-test={`TreeNode/ContextMenu:${treeNodeId}`}>
<MenuList>{contextMenuItems}</MenuList>
</MenuPopover>
</Menu>
)
}
expandIcon={isLoading ? <Spinner size="extra-tiny" /> : undefined}
iconBefore={node.iconSrc && getTreeIcon(node.iconSrc)}
style={{
backgroundColor: shouldShowAsSelected ? tokens.colorNeutralBackground1Selected : undefined,
}}
>
{node.label}
</TreeItemLayout>
{!node.isLoading && node.children?.length > 0 && (
<Tree style={{ overflow: node.isScrollable ? "auto" : undefined }}>
{getSortedChildren(node).map((childNode: TreeNode) => (
<TreeNodeComponent key={childNode.label} node={childNode} treeNodeId={`${treeNodeId}/${childNode.label}`} />
))}
</Tree>
)}
</TreeItem>
);
if (contextMenuItems.length === 0) {
return treeItem;
}
// For accessibility, it's highly recommended that any 'actions' also be available in the context menu.
// See https://react.fluentui.dev/?path=/docs/components-tree--default#actions
return (
<Menu positioning="below-end" openOnContext onOpenChange={onMenuOpenChange}>
<MenuTrigger disableButtonEnhancement>{treeItem}</MenuTrigger>
<MenuPopover>
<MenuList>{contextMenuItems}</MenuList>
</MenuPopover>
</Menu>
);
};

View File

@@ -1,11 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TreeComponent renders a simple tree 1`] = `
exports[`LegacyTreeComponent renders a simple tree 1`] = `
<div
className="treeComponent tree"
role="tree"
>
<TreeNodeComponent
<LegacyTreeNodeComponent
generation={0}
node={
Object {
@@ -33,16 +33,17 @@ exports[`TreeComponent renders a simple tree 1`] = `
</div>
`;
exports[`TreeNodeComponent does not render children by default 1`] = `
exports[`LegacyTreeNodeComponent does not render children by default 1`] = `
<div
className=" main2 nodeItem "
data-test="Tree/TreeNode:label"
onClick={[Function]}
onKeyPress={[Function]}
role="treeitem"
>
<div
className="treeNodeHeader "
data-test="label"
data-test="Tree/TreeNode/Header:label"
style={
Object {
"paddingLeft": 9,
@@ -102,7 +103,7 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
data-test="label"
role="group"
>
<TreeNodeComponent
<LegacyTreeNodeComponent
generation={3}
key="Bchild1-3-undefined"
node={
@@ -120,7 +121,7 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
}
paddingLeft={32}
/>
<TreeNodeComponent
<LegacyTreeNodeComponent
generation={3}
key="2child2-3-undefined"
node={
@@ -135,9 +136,10 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
</div>
`;
exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`] = `
exports[`LegacyTreeNodeComponent renders a simple node (sorted children, expanded) 1`] = `
<div
className="nodeClassname main12 nodeItem "
data-test="Tree/TreeNode:label"
id="id"
onClick={[Function]}
onKeyPress={[Function]}
@@ -145,7 +147,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
>
<div
className="treeNodeHeader "
data-test="label"
data-test="Tree/TreeNode/Header:label"
style={
Object {
"paddingLeft": 23,
@@ -254,7 +256,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
data-test="label"
role="group"
>
<TreeNodeComponent
<LegacyTreeNodeComponent
generation={13}
key="2child2-13-undefined"
node={
@@ -264,7 +266,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
}
paddingLeft={214}
/>
<TreeNodeComponent
<LegacyTreeNodeComponent
generation={13}
key="Bchild1-13-undefined"
node={
@@ -287,16 +289,17 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
</div>
`;
exports[`TreeNodeComponent renders loading icon 1`] = `
exports[`LegacyTreeNodeComponent renders loading icon 1`] = `
<div
className=" main2 nodeItem "
data-test="Tree/TreeNode:label"
onClick={[Function]}
onKeyPress={[Function]}
role="treeitem"
>
<div
className="treeNodeHeader "
data-test="label"
data-test="Tree/TreeNode/Header:label"
style={
Object {
"paddingLeft": 9,
@@ -360,9 +363,10 @@ exports[`TreeNodeComponent renders loading icon 1`] = `
</div>
`;
exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = `
exports[`LegacyTreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = `
<div
className="nodeClassname main12 nodeItem "
data-test="Tree/TreeNode:label"
id="id"
onClick={[Function]}
onKeyPress={[Function]}
@@ -370,7 +374,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
>
<div
className="treeNodeHeader "
data-test="label"
data-test="Tree/TreeNode/Header:label"
style={
Object {
"paddingLeft": 23,
@@ -470,7 +474,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
data-test="label"
role="group"
>
<TreeNodeComponent
<LegacyTreeNodeComponent
generation={13}
key="bchild-13-undefined"
node={
@@ -488,7 +492,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
}
paddingLeft={192}
/>
<TreeNodeComponent
<LegacyTreeNodeComponent
generation={13}
key="dchild-13-undefined"
node={
@@ -506,7 +510,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
}
paddingLeft={192}
/>
<TreeNodeComponent
<LegacyTreeNodeComponent
generation={13}
key="aChild-13-undefined"
node={
@@ -516,7 +520,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
}
paddingLeft={214}
/>
<TreeNodeComponent
<LegacyTreeNodeComponent
generation={13}
key="cchild-13-undefined"
node={
@@ -531,16 +535,17 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
</div>
`;
exports[`TreeNodeComponent renders unsorted children by default 1`] = `
exports[`LegacyTreeNodeComponent renders unsorted children by default 1`] = `
<div
className=" main2 nodeItem "
data-test="Tree/TreeNode:label"
onClick={[Function]}
onKeyPress={[Function]}
role="treeitem"
>
<div
className="treeNodeHeader "
data-test="label"
data-test="Tree/TreeNode/Header:label"
style={
Object {
"paddingLeft": 9,
@@ -600,7 +605,7 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
data-test="label"
role="group"
>
<TreeNodeComponent
<LegacyTreeNodeComponent
generation={3}
key="Bchild1-3-undefined"
node={
@@ -618,7 +623,7 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
}
paddingLeft={32}
/>
<TreeNodeComponent
<LegacyTreeNodeComponent
generation={3}
key="2child2-3-undefined"
node={

File diff suppressed because it is too large Load Diff

View File

@@ -1,147 +0,0 @@
import {
Button,
Menu,
MenuItem,
MenuList,
MenuPopover,
MenuTrigger,
Spinner,
Tree,
TreeItem,
TreeItemLayout,
TreeOpenChangeData,
TreeOpenChangeEvent,
} from "@fluentui/react-components";
import { MoreHorizontal20Regular } from "@fluentui/react-icons";
import { tokens } from "@fluentui/react-theme";
import * as React from "react";
export interface TreeNode2MenuItem {
label: string;
onClick: () => void;
iconSrc?: string;
isDisabled?: boolean;
styleClass?: string;
}
export interface TreeNode2 {
label: string;
id?: string;
children?: TreeNode2[];
contextMenu?: TreeNode2MenuItem[];
iconSrc?: string;
isExpanded?: boolean;
className?: string;
isAlphaSorted?: boolean;
// data?: any; // Piece of data corresponding to this node
timestamp?: number;
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
isLoading?: boolean;
isScrollable?: boolean;
isSelected?: () => boolean;
onClick?: () => void; // Only if a leaf, other click will expand/collapse
onExpanded?: () => Promise<void>;
onCollapsed?: () => void;
onContextMenuOpen?: () => void;
}
export interface TreeNode2ComponentProps {
node: TreeNode2;
className?: string;
treeNodeId: string;
}
const getTreeIcon = (iconSrc: string): JSX.Element => <img src={iconSrc} alt="" style={{ width: 20, height: 20 }} />;
export const TreeNode2Component: React.FC<TreeNode2ComponentProps> = ({
node,
treeNodeId,
}: TreeNode2ComponentProps): JSX.Element => {
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const getSortedChildren = (treeNode: TreeNode2): TreeNode2[] => {
if (!treeNode || !treeNode.children) {
return undefined;
}
const compareFct = (a: TreeNode2, b: TreeNode2) => a.label.localeCompare(b.label);
let unsortedChildren;
if (treeNode.isLeavesParentsSeparate) {
// Separate parents and leave
const parents: TreeNode2[] = treeNode.children.filter((node) => node.children);
const leaves: TreeNode2[] = treeNode.children.filter((node) => !node.children);
if (treeNode.isAlphaSorted) {
parents.sort(compareFct);
leaves.sort(compareFct);
}
unsortedChildren = parents.concat(leaves);
} else {
unsortedChildren = treeNode.isAlphaSorted ? treeNode.children.sort(compareFct) : treeNode.children;
}
return unsortedChildren;
};
const onOpenChange = (_: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
if (!node.isExpanded && data.open && node.onExpanded) {
// Catch the transition non-expanded to expanded
setIsLoading(true);
node.onExpanded?.().then(() => setIsLoading(false));
} else if (node.isExpanded && !data.open && node.onCollapsed) {
// Catch the transition expanded to non-expanded
node.onCollapsed?.();
}
};
return (
<TreeItem
value={treeNodeId}
itemType={node.children !== undefined ? "branch" : "leaf"}
style={{ height: "100%" }}
onOpenChange={onOpenChange}
>
<TreeItemLayout
className={node.className}
actions={
node.contextMenu && (
<Menu>
<MenuTrigger disableButtonEnhancement>
<Button aria-label="More options" appearance="subtle" icon={<MoreHorizontal20Regular />} />
</MenuTrigger>
<MenuPopover>
<MenuList>
{node.contextMenu.map((menuItem) => (
<MenuItem disabled={menuItem.isDisabled} key={menuItem.label} onClick={menuItem.onClick}>
{menuItem.label}
</MenuItem>
))}
</MenuList>
</MenuPopover>
</Menu>
)
}
expandIcon={isLoading ? <Spinner size="extra-tiny" /> : undefined}
iconBefore={node.iconSrc && getTreeIcon(node.iconSrc)}
style={{
backgroundColor: node.isSelected && node.isSelected() ? tokens.colorNeutralBackground1Selected : undefined,
}}
>
<span onClick={() => node.onClick?.()}>{node.label}</span>
</TreeItemLayout>
{!node.isLoading && node.children?.length > 0 && (
<Tree style={{ overflow: node.isScrollable ? "auto" : undefined }}>
{getSortedChildren(node).map((childNode: TreeNode2) => (
<TreeNode2Component
key={childNode.label}
node={childNode}
treeNodeId={`${treeNodeId}/${childNode.label}`}
/>
))}
</Tree>
)}
</TreeItem>
);
};

View File

@@ -38,7 +38,6 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { useSidePanel } from "../hooks/useSidePanel";
import { useTabs } from "../hooks/useTabs";
import "./ComponentRegisterer";
@@ -56,7 +55,6 @@ import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
@@ -138,14 +136,6 @@ export default class Explorer {
this.isTabsContentExpanded = ko.observable(false);
document.addEventListener(
"contextmenu",
(e) => {
e.preventDefault();
},
false,
);
$(() => {
$(document.body).click(() => $(".commandDropdownContainer").hide());
});
@@ -510,104 +500,6 @@ export default class Explorer {
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
}
public resetNotebookWorkspace(): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
handleError(
"Attempt to reset notebook workspace, but notebook is not enabled",
"Explorer/resetNotebookWorkspace",
);
return;
}
const dialogContent = useNotebook.getState().isPhoenixNotebooks
? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?"
: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?";
const resetConfirmationDialogProps: DialogProps = {
isModal: true,
title: "Reset Workspace",
subText: dialogContent,
primaryButtonText: "OK",
secondaryButtonText: "Cancel",
onPrimaryButtonClick: this._resetNotebookWorkspace,
onSecondaryButtonClick: () => useDialog.getState().closeDialog(),
};
useDialog.getState().openDialog(resetConfirmationDialogProps);
}
private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise<boolean> {
if (!databaseAccount) {
return false;
}
try {
const { value: workspaces } = await listByDatabaseAccount(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
);
return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default");
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace");
return false;
}
}
private _resetNotebookWorkspace = async () => {
useDialog.getState().closeDialog();
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
let connectionStatus: ContainerConnectionInfo;
try {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected";
Logger.logError(error, "NotebookContainerClient/resetWorkspace");
logConsoleError(error);
return;
}
TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
});
if (useNotebook.getState().isPhoenixNotebooks) {
useTabs.getState().closeAllNotebookTabs(true);
connectionStatus = {
status: ConnectionStatusType.Connecting,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
}
const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace();
if (connectionInfo?.status !== HttpStatusCodes.OK) {
throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`);
}
if (!connectionInfo?.data?.phoenixServiceUrl) {
throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`);
}
if (useNotebook.getState().isPhoenixNotebooks) {
await this.setNotebookInfo(true, connectionInfo, connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
}
logConsoleInfo("Successfully reset notebook workspace");
TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
});
} catch (error) {
logConsoleError(`Failed to reset notebook workspace: ${error}`);
TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
if (useNotebook.getState().isPhoenixNotebooks) {
connectionStatus = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().resetContainerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
}
throw error;
} finally {
clearInProgressMessage();
}
};
private getDeltaDatabases(
updatedDatabaseList: DataModels.Database[],
databases: ViewModels.Database[],
@@ -1010,92 +902,6 @@ export default class Explorer {
);
}
/**
* This creates a new notebook file, then opens the notebook
*/
public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create new notebook, but notebook is not enabled";
handleError(error, "Explorer/onNewNotebookClicked");
throw new Error(error);
}
if (useNotebook.getState().isPhoenixNotebooks) {
if (isGithubTree) {
await this.allocateContainer(PoolIdType.DefaultPoolId);
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
} else {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookModalTitle,
undefined,
"Create",
async () => {
await this.allocateContainer(PoolIdType.DefaultPoolId);
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
},
"Cancel",
undefined,
this.getNewNoteWarningText(),
);
}
} else {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
}
}
private getNewNoteWarningText(): JSX.Element {
return (
<>
<p>{Notebook.newNotebookModalContent1}</p>
<br />
<p>
{Notebook.newNotebookModalContent2}
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
{Notebook.learnMore}
</Link>
</p>
</>
);
}
private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void {
const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, {
dataExplorerArea: Constants.Areas.Notebook,
});
this.notebookManager?.notebookContentClient
.createNewNotebookFile(parent, isGithubTree)
.then((newFile: NotebookContentItem) => {
logConsoleInfo(`Successfully created: ${newFile.name}`);
TelemetryProcessor.traceSuccess(
Action.CreateNewNotebook,
{
dataExplorerArea: Constants.Areas.Notebook,
},
startKey,
);
return this.openNotebook(newFile);
})
.then(() => this.resourceTree.triggerRender())
.catch((error) => {
const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`;
logConsoleError(errorMessage);
TelemetryProcessor.traceFailure(
Action.CreateNewNotebook,
{
dataExplorerArea: Constants.Areas.Notebook,
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey,
);
})
.finally(clearInProgressMessage);
}
// TODO: Delete this function when ResourceTreeAdapter is removed.
public async refreshContentItem(item: NotebookContentItem): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
@@ -1130,10 +936,6 @@ export default class Explorer {
let title: string;
switch (kind) {
case ViewModels.TerminalKind.Default:
title = "Terminal";
break;
case ViewModels.TerminalKind.Mongo:
title = "Mongo Shell";
break;
@@ -1287,36 +1089,6 @@ export default class Explorer {
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
}
public openUploadFilePanel(parent?: NotebookContentItem): void {
if (useNotebook.getState().isPhoenixNotebooks) {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookUploadModalTitle,
undefined,
"Upload",
async () => {
await this.allocateContainer(PoolIdType.DefaultPoolId);
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePanel(parent);
},
"Cancel",
undefined,
this.getNewNoteWarningText(),
);
} else {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePanel(parent);
}
}
private uploadFilePanel(parent?: NotebookContentItem): void {
useSidePanel
.getState()
.openSidePanel(
"Upload file to notebook server",
<UploadFilePane uploadFile={(name: string, content: string) => this.uploadFile(name, content, parent)} />,
);
}
public getDownloadModalConent(fileName: string): JSX.Element {
if (useNotebook.getState().isPhoenixNotebooks) {
return (

View File

@@ -349,7 +349,7 @@ export class NodePropertiesComponent extends React.Component<
onActivated={this.setIsDeleteConfirm.bind(this, true)}
aria-label="Delete this vertex"
>
<img src={DeleteIcon} alt="Delete" />
<img src={DeleteIcon} alt="Delete" role="button" />
</AccessibleElement>
);
} else {
@@ -406,7 +406,7 @@ export class NodePropertiesComponent extends React.Component<
aria-label="Edit properties"
onActivated={expandClickHandler}
>
<img src={EditIcon} alt="Edit" />
<img src={EditIcon} alt="Edit" role="button" />
</AccessibleElement>
)}

View File

@@ -184,12 +184,18 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
className="rightPaneTrashIcon rightPaneBtns"
tabIndex={0}
role="button"
aria-label={`Delete ${data.key}`}
onClick={(event: React.MouseEvent<HTMLDivElement>) => removeNewVertexProperty(event, index)}
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) =>
removeNewVertexPropertyKeyPress(event, index)
}
>
<img className="refreshcol rightPaneTrashIconImg" src={DeleteIcon} alt="Remove property" />
<img
aria-label="hidden"
className="refreshcol rightPaneTrashIconImg"
src={DeleteIcon}
alt="Remove property"
/>
</div>
</div>
</div>

View File

@@ -5,6 +5,7 @@
*/
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { userContext } from "UserContext";
import * as React from "react";
import create, { UseStore } from "zustand";
@@ -40,6 +41,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const buttons = useCommandBar((state) => state.contextButtons);
const isHidden = useCommandBar((state) => state.isHidden);
const backgroundColor = StyleConstants.BaseLight;
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
const buttons =
@@ -105,6 +107,10 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
},
};
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
setKeyboardHandlers(keyboardHandlers);
return (
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
<FluentCommandBar

View File

@@ -2,10 +2,8 @@ import * as ko from "knockout";
import { AuthType } from "../../../AuthType";
import { DatabaseAccount } from "../../../Contracts/DataModels";
import { CollectionBase } from "../../../Contracts/ViewModels";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
import NotebookManager from "../../Notebook/NotebookManager";
import { useNotebook } from "../../Notebook/useNotebook";
import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode";
@@ -72,181 +70,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
});
describe("Enable notebook button", () => {
const enableNotebookBtnLabel = "Enable Notebooks (Preview)";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
updateUserContext({
portalEnv: "prod",
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
});
afterEach(() => {
updateUserContext({
portalEnv: "prod",
});
useNotebook.getState().setIsNotebookEnabled(false);
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
});
it("Notebooks is already enabled - button should be hidden", () => {
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeUndefined();
});
it("Account is running on one of the national clouds - button should be hidden", () => {
updateUserContext({
portalEnv: "mooncake",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeUndefined();
});
it("Notebooks is not enabled but is available - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
//TODO: modify once notebooks are available
expect(enableNotebookBtn).toBeUndefined();
//expect(enableNotebookBtn).toBeDefined();
//expect(enableNotebookBtn.disabled).toBe(false);
//expect(enableNotebookBtn.tooltipText).toBe("");
});
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
//TODO: modify once notebooks are available
expect(enableNotebookBtn).toBeUndefined();
//expect(enableNotebookBtn).toBeDefined();
//expect(enableNotebookBtn.disabled).toBe(true);
//expect(enableNotebookBtn.tooltipText).toBe(
// "Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
//);
});
});
describe("Open Mongo shell button", () => {
const openMongoShellBtnLabel = "Open Mongo shell";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
});
afterAll(() => {
updateUserContext({
apiType: "SQL",
});
useNotebook.getState().setIsShellEnabled(false);
});
beforeEach(() => {
updateUserContext({
apiType: "Mongo",
});
useNotebook.getState().setIsShellEnabled(true);
});
afterEach(() => {
useNotebook.getState().setIsNotebookEnabled(false);
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
});
it("Mongo Api not available - button should be hidden", () => {
updateUserContext({
apiType: "SQL",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
it("Running on a national cloud - button should be hidden", () => {
updateUserContext({
portalEnv: "mooncake",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
it("Notebooks is not enabled and is unavailable - button should be hidden", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
it("Notebooks is not enabled and is available - button should be hidden", () => {
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebookEnabled(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined();
//TODO: modify once notebooks are available
expect(openMongoShellBtn.disabled).toBe(true);
//expect(openMongoShellBtn.disabled).toBe(false);
//expect(openMongoShellBtn.tooltipText).toBe("");
});
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined();
//TODO: modify once notebooks are available
expect(openMongoShellBtn.disabled).toBe(true);
//expect(openMongoShellBtn.disabled).toBe(false);
//expect(openMongoShellBtn.tooltipText).toBe("");
});
it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => {
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
useNotebook.getState().setIsShellEnabled(false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
});
describe("Open Cassandra shell button", () => {
const openCassandraShellBtnLabel = "Open Cassandra shell";
const selectedNodeState = useSelectedNode.getState();
@@ -305,42 +128,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
it("Notebooks is not enabled and is available - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebookEnabled(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined();
//TODO: modify once notebooks are available
expect(openCassandraShellBtn.disabled).toBe(true);
//expect(openCassandraShellBtn.disabled).toBe(false);
//expect(openCassandraShellBtn.tooltipText).toBe("");
});
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined();
//TODO: modify once notebooks are available
expect(openCassandraShellBtn.disabled).toBe(true);
//expect(openCassandraShellBtn.disabled).toBe(false);
//expect(openCassandraShellBtn.tooltipText).toBe("");
});
});
describe("Open Postgres and vCore Mongo buttons", () => {
@@ -368,62 +155,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
});
describe("GitHub buttons", () => {
const connectToGitHubBtnLabel = "Connect to GitHub";
const manageGitHubSettingsBtnLabel = "Manage GitHub settings";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
mockExplorer.notebookManager = new NotebookManager();
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
});
afterEach(() => {
jest.resetAllMocks();
useNotebook.getState().setIsNotebookEnabled(false);
});
it("Notebooks is enabled and GitHubOAuthService is not logged in - connect to github button should be visible", () => {
useNotebook.getState().setIsNotebookEnabled(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
expect(connectToGitHubBtn).toBeDefined();
});
it("Notebooks is enabled and GitHubOAuthService is logged in - manage github settings button should be visible", () => {
useNotebook.getState().setIsNotebookEnabled(true);
mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const manageGitHubSettingsBtn = buttons.find(
(button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel,
);
expect(manageGitHubSettingsBtn).toBeDefined();
});
it("Notebooks is not enabled - connect to github and manage github settings buttons should be hidden", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
expect(connectToGitHubBtn).toBeUndefined();
const manageGitHubSettingsBtn = buttons.find(
(button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel,
);
expect(manageGitHubSettingsBtn).toBeUndefined();
});
});
describe("Resource token", () => {
const mockCollection = { id: ko.observable("test") } as CollectionBase;
useSelectedNode.getState().setSelectedNode(mockCollection);

View File

@@ -1,3 +1,4 @@
import { KeyboardAction } from "KeyboardShortcuts";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react";
import AddCollectionIcon from "../../../../images/AddCollection.svg";
@@ -7,14 +8,10 @@ import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
import AddUdfIcon from "../../../../images/AddUdf.svg";
import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg";
import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import HomeIcon from "../../../../images/Home_16.svg";
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
import GitHubIcon from "../../../../images/github.svg";
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
import SettingsIcon from "../../../../images/settings_15x15.svg";
import SynapseIcon from "../../../../images/synapse-link.svg";
@@ -22,7 +19,6 @@ import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants";
import { Platform, configContext } from "../../../ConfigContext";
import * as ViewModels from "../../../Contracts/ViewModels";
import { JunoClient } from "../../../Juno/JunoClient";
import { userContext } from "../../../UserContext";
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
@@ -33,7 +29,6 @@ import { useNotebook } from "../../Notebook/useNotebook";
import { OpenFullScreen } from "../../OpenFullScreen";
import { AddDatabasePanel } from "../../Panes/AddDatabasePanel/AddDatabasePanel";
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel";
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
import { useDatabases } from "../../useDatabases";
@@ -58,11 +53,12 @@ export function createStaticCommandBarButtons(
}
};
if (configContext.platform !== Platform.Fabric) {
const homeBtn = createHomeButton();
buttons.push(homeBtn);
if (configContext.platform !== Platform.Fabric) {
const newCollectionBtn = createNewCollectionGroup(container);
newCollectionBtn.keyboardAction = KeyboardAction.NEW_COLLECTION; // Just for the root button, not the child version we create below.
buttons.push(newCollectionBtn);
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
@@ -80,57 +76,6 @@ export function createStaticCommandBarButtons(
}
}
if (useNotebook.getState().isNotebookEnabled) {
addDivider();
const notebookButtons: CommandButtonComponentProps[] = [];
const newNotebookButton = createNewNotebookButton(container);
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
notebookButtons.push(newNotebookButton);
if (container.notebookManager?.gitHubOAuthService) {
notebookButtons.push(createManageGitHubAccountButton(container));
}
if (useNotebook.getState().isPhoenixFeatures && configContext.isTerminalEnabled) {
notebookButtons.push(createOpenTerminalButton(container));
}
if (useNotebook.getState().isPhoenixNotebooks && selectedNodeState.isConnectedToContainer()) {
notebookButtons.push(createNotebookWorkspaceResetButton(container));
}
if (
(userContext.apiType === "Mongo" &&
useNotebook.getState().isShellEnabled &&
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
userContext.apiType === "Cassandra"
) {
notebookButtons.push(createDivider());
if (userContext.apiType === "Cassandra") {
notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Cassandra));
} else {
notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Mongo));
}
}
notebookButtons.forEach((btn) => {
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
if (!useNotebook.getState().isPhoenixFeatures) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
}
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
if (!useNotebook.getState().isPhoenixFeatures) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
}
} else if (btn.commandButtonLabel.indexOf("Open Terminal") !== -1) {
if (!useNotebook.getState().isPhoenixFeatures) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
}
} else if (!useNotebook.getState().isPhoenixNotebooks) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
}
buttons.push(btn);
});
}
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
@@ -151,6 +96,7 @@ export function createStaticCommandBarButtons(
const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
keyboardAction: KeyboardAction.NEW_SPROC,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
@@ -240,7 +186,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
buttons.push(fullScreenButton);
}
if (configContext.platform !== Platform.Emulator) {
if (configContext.platform === Platform.Portal) {
const label = "Feedback";
const feedbackButtonOptions: CommandButtonComponentProps = {
iconSrc: FeedbackIcon,
@@ -334,6 +280,7 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
return {
iconSrc: AddDatabaseIcon,
iconAlt: label,
keyboardAction: KeyboardAction.NEW_DATABASE,
onCommandClick: async () => {
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) {
@@ -354,6 +301,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
id: "newQueryBtn",
iconSrc: AddSqlQueryIcon,
iconAlt: label,
keyboardAction: KeyboardAction.NEW_QUERY,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
@@ -369,6 +317,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
id: "newQueryBtn",
iconSrc: AddSqlQueryIcon,
iconAlt: label,
keyboardAction: KeyboardAction.NEW_QUERY,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
@@ -394,6 +343,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
keyboardAction: KeyboardAction.NEW_SPROC,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
@@ -413,6 +363,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
const newUserDefinedFunctionBtn: CommandButtonComponentProps = {
iconSrc: AddUdfIcon,
iconAlt: label,
keyboardAction: KeyboardAction.NEW_UDF,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
@@ -432,6 +383,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
const newTriggerBtn: CommandButtonComponentProps = {
iconSrc: AddTriggerIcon,
iconAlt: label,
keyboardAction: KeyboardAction.NEW_TRIGGER,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection);
@@ -449,45 +401,12 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
return buttons;
}
function applyNotebooksTemporarilyDownStyle(buttonProps: CommandButtonComponentProps, tooltip: string): void {
if (!buttonProps.isDivider) {
buttonProps.disabled = true;
buttonProps.tooltipText = tooltip;
}
}
function createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "New Notebook";
return {
id: "newNotebookBtn",
iconSrc: NewNotebookIcon,
iconAlt: label,
onCommandClick: () => container.onNewNotebookClicked(),
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}
function createuploadNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "Upload to Notebook Server";
return {
iconSrc: NewNotebookIcon,
iconAlt: label,
onCommandClick: () => container.openUploadFilePanel(),
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}
function createOpenQueryButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Query";
return {
iconSrc: BrowseQueriesIcon,
iconAlt: label,
keyboardAction: KeyboardAction.OPEN_QUERY,
onCommandClick: () =>
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
commandButtonLabel: label,
@@ -502,6 +421,7 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
return {
iconSrc: OpenQueryFromDiskIcon,
iconAlt: label,
keyboardAction: KeyboardAction.OPEN_QUERY_FROM_DISK,
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
commandButtonLabel: label,
ariaLabel: label,
@@ -510,19 +430,6 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
};
}
function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Terminal";
return {
iconSrc: CosmosTerminalIcon,
iconAlt: label,
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default),
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}
function createOpenTerminalButtonByKind(
container: Explorer,
terminalKind: ViewModels.TerminalKind,
@@ -562,45 +469,6 @@ function createOpenTerminalButtonByKind(
};
}
function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps {
const label = "Reset Workspace";
return {
iconSrc: ResetWorkspaceIcon,
iconAlt: label,
onCommandClick: () => container.resetNotebookWorkspace(),
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}
function createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps {
const connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
const junoClient = new JunoClient();
return {
iconSrc: GitHubIcon,
iconAlt: label,
onCommandClick: () => {
useSidePanel
.getState()
.openSidePanel(
label,
<GitHubReposPanel
explorer={container}
gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={junoClient}
/>,
);
},
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}
function createStaticCommandBarButtonsForResourceToken(
container: Explorer,
selectedNodeState: SelectedNodeState,

View File

@@ -7,6 +7,7 @@ import {
IDropdownStyles,
} from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { KeyboardHandlerMap } from "KeyboardShortcuts";
import * as React from "react";
import _ from "underscore";
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
@@ -59,21 +60,23 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
iconName: btn.iconName,
},
onClick: (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
onClick: btn.onCommandClick
? (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
btn.onCommandClick(ev);
let copilotEnabled = false;
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
}
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label, copilotEnabled });
},
}
: undefined,
key: `${btn.commandButtonLabel}${index}`,
text: label,
"data-test": label,
title: btn.tooltipText,
name: label,
disabled: btn.disabled,
ariaLabel: btn.ariaLabel,
"data-test": `CommandBar/Button:${label}`,
buttonStyles: {
root: {
backgroundColor: backgroundColor,
@@ -233,3 +236,28 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType,
onRender: () => <ConnectionStatus container={container} poolId={poolId} />,
};
};
export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap {
const handlers: KeyboardHandlerMap = {};
function createHandlers(buttons: CommandButtonComponentProps[]) {
buttons.forEach((button) => {
if (!button.disabled && button.keyboardAction) {
handlers[button.keyboardAction] = (e) => {
button.onCommandClick(e);
// If the handler is bound, it means the button is visible and enabled, so we should prevent the default action
return true;
};
}
if (button.children && button.children.length > 0) {
createHandlers(button.children);
}
});
}
createHandlers(allButtons);
return handlers;
}

View File

@@ -1,4 +1,5 @@
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
import { useDatabases } from "Explorer/useDatabases";
import React from "react";
import { ActionContracts } from "../../Contracts/ExplorerContracts";
import * as ViewModels from "../../Contracts/ViewModels";
@@ -40,12 +41,26 @@ function openCollectionTab(
databases: ViewModels.Database[],
initialDatabaseIndex = 0,
) {
//if databases are not yet loaded, wait until loaded
if (!databases || databases.length === 0) {
const databaseActionHandler = (databases: ViewModels.Database[]) => {
databasesUnsubscription();
openCollectionTab(action, databases, 0);
return;
};
const databasesUnsubscription = useDatabases.subscribe(databaseActionHandler, (state) => state.databases);
} else {
for (let i = initialDatabaseIndex; i < databases.length; i++) {
const database: ViewModels.Database = databases[i];
if (!!action.databaseResourceId && database.id() !== action.databaseResourceId) {
continue;
}
//expand database first if not expanded to load the collections
if (!database.isDatabaseExpanded?.()) {
database.expandDatabase?.();
}
const collectionActionHandler = (collections: ViewModels.Collection[]) => {
if (!action.collectionResourceId && collections.length === 0) {
subscription.dispose();
@@ -132,6 +147,7 @@ function openCollectionTab(
break;
}
}
}
function openPane(action: ActionContracts.OpenPane, explorer: Explorer) {

View File

@@ -1,20 +1,43 @@
import { PrimaryButton, Stack, Text } from "@fluentui/react";
import { AuthType } from "AuthType";
import { configContext } from "ConfigContext";
import { userContext } from "UserContext";
import * as React from "react";
export const OpenFullScreen: React.FunctionComponent = () => {
const searchParams = new URLSearchParams();
searchParams.append("openFrom", "portal");
let hasAccountContext = false;
let requiresConnectionString = false;
if (userContext.authType === AuthType.AAD) {
if (userContext.subscriptionId && userContext.databaseAccount) {
searchParams.append("subscription", userContext.subscriptionId);
searchParams.append("account", userContext.databaseAccount.name);
searchParams.append("authType", "entra");
hasAccountContext = true;
}
} else if (userContext.authType === AuthType.MasterKey || userContext.authType === AuthType.ResourceToken) {
requiresConnectionString = true;
}
return (
<>
<div style={{ padding: "34px" }}>
<Stack tokens={{ childrenGap: 10 }}>
<Text>
Open this database account in a new browser tab with Cosmos DB Explorer. You can connect using your
Microsoft account or a connection string.
Open this database account in a new browser tab with Cosmos DB Explorer.
{requiresConnectionString && " You'll need to provide a connection string."}
{hasAccountContext && " You may be prompted to sign in with Entra ID again."}
</Text>
<Text>
Open tabs and queries will not be carried over, but you can still access them in this tab.
</Text>
<Stack horizontal tokens={{ childrenGap: 10 }}>
<PrimaryButton
onClick={() => {
window.open("https://cosmos.azure.com/", "_blank");
}}
href={`${configContext.hostedExplorerURL}?${searchParams.toString()}`}
target="_blank"
text="Open"
iconProps={{ iconName: "OpenInNewWindow" }}
/>

View File

@@ -21,6 +21,7 @@ import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels";
import { SubscriptionType } from "Contracts/SubscriptionType";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { useSidePanel } from "hooks/useSidePanel";
import { useTeachingBubble } from "hooks/useTeachingBubble";
import React from "react";
@@ -29,7 +30,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils";
import { isCapabilityEnabled, isServerlessAccount } from "Utils/CapabilityUtils";
import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { getUpsellMessage } from "Utils/PricingUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
@@ -81,6 +82,26 @@ export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
excludedPaths: [],
};
const DefaultDatabaseVectorIndex: DataModels.IndexingPolicy = {
indexingMode: "consistent",
automatic: true,
includedPaths: [
{
path: "/*",
},
],
excludedPaths: [
{
path: '/"_etag"/?',
},
],
vectorIndexes: [],
};
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
vectorEmbeddings: [],
};
export interface AddCollectionPanelState {
createNewDatabase: boolean;
newDatabaseId: string;
@@ -101,6 +122,8 @@ export interface AddCollectionPanelState {
isExecuting: boolean;
isThroughputCapExceeded: boolean;
teachingBubbleStep: number;
vectorIndexingPolicy: string;
vectorEmbeddingPolicy: string;
}
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
@@ -136,6 +159,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
isExecuting: false,
isThroughputCapExceeded: false,
teachingBubbleStep: 0,
vectorIndexingPolicy: JSON.stringify(DefaultDatabaseVectorIndex, null, 2),
vectorEmbeddingPolicy: JSON.stringify(DefaultVectorEmbeddingPolicy, null, 2),
};
}
@@ -145,11 +170,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
}
componentDidUpdate(_prevProps: AddCollectionPanelProps, prevState: AddCollectionPanelState): void {
if (this.state.errorMessage && this.state.errorMessage !== prevState.errorMessage) {
this.scrollToSection("panelContainer");
}
}
render(): JSX.Element {
const isFirstResourceCreated = useDatabases.getState().isFirstResourceCreated();
return (
<form className="panelFormWrapper" onSubmit={this.submit.bind(this)}>
<form className="panelFormWrapper" onSubmit={this.submit.bind(this)} id="panelContainer">
{this.state.errorMessage && (
<PanelInfoErrorComponent
message={this.state.errorMessage}
@@ -382,6 +413,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)}
{!this.state.createNewDatabase && (
<Dropdown
ariaLabel="Choose an existing database"
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
style={{ width: 300, fontSize: 12 }}
placeholder="Choose an existing database"
@@ -800,8 +832,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without
impacting the performance of transactional workloads."
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
/>
</TooltipHost>
</Stack>
@@ -863,17 +894,76 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)}
</Stack>
)}
{this.shouldShowVectorSearchParameters() && (
<Stack>
<CollapsibleSectionComponent
title="Indexing Policy"
isExpandedByDefault={false}
onExpand={() => {
this.scrollToSection("collapsibleVectorPolicySectionContent");
}}
>
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Link href="https://aka.ms/CosmosDBVectorSetup" target="_blank">
Learn more
</Link>
<EditorReact
language={"json"}
content={this.state.vectorIndexingPolicy}
isReadOnly={false}
wordWrap={"on"}
ariaLabel={"Editing indexing policy"}
lineNumbers={"on"}
scrollBeyondLastLine={false}
spinnerClassName="panelSectionSpinner"
monacoContainerStyles={{
minHeight: 200,
}}
onContentChanged={(newIndexingPolicy: string) => this.setVectorIndexingPolicy(newIndexingPolicy)}
/>
</Stack>
</CollapsibleSectionComponent>
<CollapsibleSectionComponent
title="Container Vector Policy"
isExpandedByDefault={false}
onExpand={() => {
this.scrollToSection("collapsibleVectorPolicySectionContent");
}}
>
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Link href="https://aka.ms/CosmosDBVectorSetup" target="_blank">
Learn more
</Link>
<EditorReact
language={"json"}
content={this.state.vectorEmbeddingPolicy}
isReadOnly={false}
wordWrap={"on"}
ariaLabel={"Editing container vector policy"}
lineNumbers={"on"}
scrollBeyondLastLine={false}
spinnerClassName="panelSectionSpinner"
monacoContainerStyles={{
minHeight: 200,
}}
onContentChanged={(newVectorEmbeddingPolicy: string) =>
this.setVectorEmbeddingPolicy(newVectorEmbeddingPolicy)
}
/>
</Stack>
</CollapsibleSectionComponent>
</Stack>
)}
{userContext.apiType !== "Tables" && (
<CollapsibleSectionComponent
title="Advanced"
isExpandedByDefault={false}
onExpand={() => {
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
this.scrollToAdvancedSection();
this.scrollToSection("collapsibleAdvancedSectionContent");
}}
>
<Stack className="panelGroupSpacing" id="collapsibleSectionContent">
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
{isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport") && (
<Stack className="panelGroupSpacing">
<Stack horizontal>
@@ -924,10 +1014,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
/>
<Text variant="small">
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> To ensure compatibility with
older SDKs, the created container will use a legacy partitioning scheme that supports partition
key values of size only up to 101 bytes. If this is enabled, you will not be able to use
hierarchical partition keys.{" "}
<Icon iconName="InfoSolid" className="removeIcon" /> To ensure compatibility with older SDKs, the
created container will use a legacy partitioning scheme that supports partition key values of size
only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.{" "}
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
Learn more
</Link>
@@ -1070,6 +1159,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
}
private setVectorEmbeddingPolicy(vectorEmbeddingPolicy: string): void {
this.setState({
vectorEmbeddingPolicy,
});
}
private setVectorIndexingPolicy(vectorIndexingPolicy: string): void {
this.setState({
vectorIndexingPolicy,
});
}
private isSelectedDatabaseSharedThroughput(): boolean {
if (!this.state.selectedDatabaseId) {
return false;
@@ -1209,6 +1310,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
);
}
private shouldShowVectorSearchParameters() {
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
}
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
if (this.state.uniqueKeys?.length === 0) {
return undefined;
@@ -1265,6 +1370,22 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false;
}
if (this.shouldShowVectorSearchParameters()) {
try {
JSON.parse(this.state.vectorIndexingPolicy) as DataModels.IndexingPolicy;
} catch (e) {
this.setState({ errorMessage: "Invalid JSON format for indexingPolicy" });
return false;
}
try {
JSON.parse(this.state.vectorEmbeddingPolicy) as DataModels.VectorEmbeddingPolicy;
} catch (e) {
this.setState({ errorMessage: "Invalid JSON format for vectorEmbeddingPolicy" });
return false;
}
}
return true;
}
@@ -1287,8 +1408,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return Constants.AnalyticalStorageTtl.Disabled;
}
private scrollToAdvancedSection(): void {
document.getElementById("collapsibleSectionContent")?.scrollIntoView();
private scrollToSection(id: string): void {
document.getElementById(id)?.scrollIntoView();
}
private getSampleDBName(): string {
@@ -1340,10 +1461,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
: undefined;
const indexingPolicy: DataModels.IndexingPolicy = this.state.enableIndexing
let indexingPolicy: DataModels.IndexingPolicy = this.state.enableIndexing
? AllPropertiesIndexed
: SharedDatabaseDefault;
let vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
if (this.shouldShowVectorSearchParameters()) {
indexingPolicy = JSON.parse(this.state.vectorIndexingPolicy);
vectorEmbeddingPolicy = JSON.parse(this.state.vectorEmbeddingPolicy);
}
const telemetryData = {
database: {
id: databaseId,
@@ -1402,6 +1530,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
partitionKey,
uniqueKeyPolicy,
createMongoWildcardIndex: this.state.createMongoWildCardIndex,
vectorEmbeddingPolicy,
};
this.setState({ isExecuting: true });

View File

@@ -202,8 +202,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
required={true}
autoComplete="off"
styles={getTextFieldStyles()}
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
pattern="[^/?#\\-]*[^/?#- \\]"
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
placeholder="Type a new keyspace id"
size={40}
value={newKeyspaceId}
@@ -275,7 +275,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Enter CQL command to create the table.{" "}
<Link href="https://aka.ms/cassandra-create-table" target="_blank">
<Link className="underlinedLink" href="https://aka.ms/cassandra-create-table" target="_blank">
Learn More
</Link>
</Text>
@@ -292,8 +292,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
required={true}
ariaLabel="addCollection-table Id Create table"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
pattern="[^/?#\\-]*[^/?#- \\]"
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
placeholder="Enter table Id"
size={20}
value={tableId}

View File

@@ -6,6 +6,7 @@ import {
Icon,
IconButton,
Link,
MessageBar,
Stack,
Text,
TooltipHost,
@@ -207,6 +208,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
</Stack>
{createNewContainer ? (
<Stack>
<MessageBar>All configurations except for unique keys will be copied from the source container</MessageBar>
<Stack className="panelGroupSpacing">
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>

View File

@@ -379,6 +379,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
>
<CustomizedPrimaryButton
ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
text="OK"
@@ -386,6 +387,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
>
<PrimaryButton
ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
text="OK"
@@ -666,6 +668,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
>
<CustomizedDefaultButton
ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -948,6 +951,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
>
<DefaultButton
ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -1231,6 +1235,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
<BaseButton
ariaLabel="OK"
baseClassName="ms-Button"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -2105,6 +2110,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
aria-label="OK"
className="ms-Button ms-Button--primary root-122"
data-is-focusable={true}
data-test="Panel/OkButton"
id="sidePanelOkButton"
onClick={[Function]}
onKeyDown={[Function]}
@@ -2140,6 +2146,11 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
</div>
</PanelFooterComponent>
</form>
<span
className="screenReaderOnly"
id="screenReaderStatus"
role="status"
/>
</RightPaneForm>
</DeleteCollectionConfirmationPane>
`;

View File

@@ -9,6 +9,7 @@ import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { getDatabaseName } from "Utils/APITypeUtils";
import { logConsoleError } from "Utils/NotificationConsoleUtils";
import { useSidePanel } from "hooks/useSidePanel";
import { useTabs } from "hooks/useTabs";
@@ -37,11 +38,11 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
const submit = async (): Promise<void> => {
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
setFormError(
`Input database name "${databaseInput}" does not match the selected database "${selectedDatabase.id()}"`,
`Input ${getDatabaseName()} name "${databaseInput}" does not match the selected ${getDatabaseName()} "${selectedDatabase.id()}"`,
);
logConsoleError(`Error while deleting collection ${selectedDatabase && selectedDatabase.id()}`);
logConsoleError(`Error while deleting ${getDatabaseName()} ${selectedDatabase && selectedDatabase.id()}`);
logConsoleError(
`Input database name "${databaseInput}" does not match the selected database "${selectedDatabase.id()}"`,
`Input ${getDatabaseName()} name "${databaseInput}" does not match the selected ${getDatabaseName()} "${selectedDatabase.id()}"`,
);
return;
}
@@ -123,17 +124,18 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
message:
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
};
const confirmDatabase = "Confirm by typing the database id";
const reasonInfo = "Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?";
const confirmDatabase = `Confirm by typing the ${getDatabaseName()} id`;
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${getDatabaseName()}?`;
return (
<RightPaneForm {...props}>
{!formError && <PanelInfoErrorComponent {...errorProps} />}
<div className="panelMainContent">
<div className="confirmDeleteInput">
<span className="mandatoryStar">* </span>
<Text variant="small">Confirm by typing the database id</Text>
<Text variant="small">Confirm by typing the {getDatabaseName()} id</Text>
<TextField
id="confirmDatabaseId"
data-test="Input:confirmDatabaseId"
autoFocus
styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => {
@@ -149,7 +151,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
Help us improve Azure Cosmos DB!
</Text>
<Text variant="small" block>
What is the reason why you are deleting this database?
What is the reason why you are deleting this {getDatabaseName()}?
</Text>
<TextField
id="deleteDatabaseFeedbackInput"

View File

@@ -5312,6 +5312,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
>
<CustomizedPrimaryButton
ariaLabel="Execute"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
text="Execute"
@@ -5319,6 +5320,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
>
<PrimaryButton
ariaLabel="Execute"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
text="Execute"
@@ -5599,6 +5601,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
>
<CustomizedDefaultButton
ariaLabel="Execute"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -5881,6 +5884,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
>
<DefaultButton
ariaLabel="Execute"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -6164,6 +6168,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
<BaseButton
ariaLabel="Execute"
baseClassName="ms-Button"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -7038,6 +7043,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
aria-label="Execute"
className="ms-Button ms-Button--primary root-148"
data-is-focusable={true}
data-test="Panel/OkButton"
id="sidePanelOkButton"
onClick={[Function]}
onKeyDown={[Function]}
@@ -7073,6 +7079,11 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
</div>
</PanelFooterComponent>
</form>
<span
className="screenReaderOnly"
id="screenReaderStatus"
role="status"
/>
</RightPaneForm>
</ExecuteSprocParamsPane>
`;

View File

@@ -18,7 +18,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
Object {
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],

View File

@@ -48,6 +48,13 @@
font-size: @mediumFontSize;
padding: 0 @LargeSpace 0 @SmallSpace;
}
.panelSectionSpinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}

View File

@@ -53,6 +53,7 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
return (
<Panel
data-test={`Panel:${this.props.headerText}`}
headerText={this.props.headerText}
isOpen={this.props.isOpen}
onDismiss={this.onDissmiss}

View File

@@ -16,6 +16,7 @@ export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = (
<PrimaryButton
type="submit"
id="sidePanelOkButton"
data-test="Panel/OkButton"
text={buttonLabel}
ariaLabel={buttonLabel}
disabled={!!isButtonDisabled}

View File

@@ -2,6 +2,7 @@ import React, { CSSProperties, FunctionComponent, ReactNode } from "react";
import { PanelFooterComponent } from "../PanelFooterComponent";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
import { PanelLoadingScreen } from "../PanelLoadingScreen";
import { labelToLoadingItemName } from "Explorer/Tables/Constants";
export interface RightPaneFormProps {
formError: string;
@@ -27,6 +28,10 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
onSubmit();
const screenReaderStatusElement = document.getElementById("screenReaderStatus");
if (screenReaderStatusElement) {
screenReaderStatusElement.innerHTML = labelToLoadingItemName[submitButtonText] || "Loading";
}
};
return (
@@ -42,6 +47,7 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
/>
)}
</form>
<span role="status" className="screenReaderOnly" id="screenReaderStatus"></span>
{isExecuting && <PanelLoadingScreen />}
</>
);

View File

@@ -21,6 +21,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
>
<CustomizedPrimaryButton
ariaLabel="Load"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
text="Load"
@@ -28,6 +29,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
>
<PrimaryButton
ariaLabel="Load"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
text="Load"
@@ -308,6 +310,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
>
<CustomizedDefaultButton
ariaLabel="Load"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -590,6 +593,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
>
<DefaultButton
ariaLabel="Load"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -873,6 +877,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
<BaseButton
ariaLabel="Load"
baseClassName="ms-Button"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -1747,6 +1752,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
aria-label="Load"
className="ms-Button ms-Button--primary root-109"
data-is-focusable={true}
data-test="Panel/OkButton"
id="sidePanelOkButton"
onClick={[Function]}
onKeyDown={[Function]}
@@ -1782,5 +1788,10 @@ exports[`Right Pane Form should render Default properly 1`] = `
</div>
</PanelFooterComponent>
</form>
<span
className="screenReaderOnly"
id="screenReaderStatus"
role="status"
/>
</RightPaneForm>
`;

View File

@@ -9,12 +9,14 @@ import {
Toggle,
} from "@fluentui/react";
import * as Constants from "Common/Constants";
import { SplitterDirection } from "Common/Splitter";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { configContext } from "ConfigContext";
import {
DefaultRUThreshold,
LocalStorageUtility,
StorageKey,
getDefaultQueryResultsView,
getRUThreshold,
ruThresholdEnabled as isRUThresholdEnabled,
} from "Shared/StorageUtility";
@@ -47,6 +49,9 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
);
const [queryTimeout, setQueryTimeout] = useState<number>(LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout));
const [defaultQueryResultsView, setDefaultQueryResultsView] = useState<SplitterDirection>(
getDefaultQueryResultsView(),
);
const [automaticallyCancelQueryAfterTimeout, setAutomaticallyCancelQueryAfterTimeout] = useState<boolean>(
LocalStorageUtility.getEntryBoolean(StorageKey.AutomaticallyCancelQueryAfterTimeout),
);
@@ -121,6 +126,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString());
LocalStorageUtility.setEntryString(StorageKey.CopilotSampleDBEnabled, copilotSampleDBEnabled.toString());
LocalStorageUtility.setEntryString(StorageKey.DefaultQueryResultsView, defaultQueryResultsView);
if (shouldShowGraphAutoVizOption) {
LocalStorageUtility.setEntryBoolean(
@@ -197,6 +203,11 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{ key: Constants.PriorityLevel.High, text: "High" },
];
const defaultQueryResultsViewOptionList: IChoiceGroupOption[] = [
{ key: SplitterDirection.Vertical, text: "Vertical" },
{ key: SplitterDirection.Horizontal, text: "Horizontal" },
];
const handleOnPriorityLevelOptionChange = (
ev: React.FormEvent<HTMLInputElement>,
option: IChoiceGroupOption,
@@ -234,6 +245,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
}
};
const handleOnDefaultQueryResultsViewChange = (
ev: React.MouseEvent<HTMLElement>,
option: IChoiceGroupOption,
): void => {
setDefaultQueryResultsView(option.key as SplitterDirection);
};
const handleOnQueryRetryAttemptsSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => {
const retryAttempts = Number(newValue);
if (!isNaN(retryAttempts)) {
@@ -438,6 +456,25 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
)}
</div>
</div>
<div className="settingsSection">
<div className="settingsSectionPart">
<div>
<legend id="defaultQueryResultsView" className="settingsSectionLabel legendLabel">
Default Query Results View
</legend>
<InfoTooltip>Select the default view to use when displaying query results.</InfoTooltip>
</div>
<div>
<ChoiceGroup
ariaLabelledBy="defaultQueryResultsView"
selectedKey={defaultQueryResultsView}
options={defaultQueryResultsViewOptionList}
styles={choiceButtonStyles}
onChange={handleOnDefaultQueryResultsViewChange}
/>
</div>
</div>
</div>
</>
)}
<div className="settingsSection">
@@ -630,7 +667,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
Enable sample database
<InfoTooltip>
This is a sample database and collection with synthetic product data you can use to explore using
NoSQL queries and Copilot. This will appear as another database in the Data Explorer UI, and is
NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and is
created by, and maintained by Microsoft at no cost to you.
</InfoTooltip>
</div>
@@ -640,7 +677,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
label: { padding: 0 },
}}
className="padding"
ariaLabel="Enable sample db for Copilot"
ariaLabel="Enable sample db for Query Advisor"
checked={copilotSampleDBEnabled}
onChange={handleSampleDatabaseChange}
/>

View File

@@ -205,6 +205,67 @@ exports[`Settings Pane should render Default properly 1`] = `
</div>
</div>
</div>
<div
className="settingsSection"
>
<div
className="settingsSectionPart"
>
<div>
<legend
className="settingsSectionLabel legendLabel"
id="defaultQueryResultsView"
>
Default Query Results View
</legend>
<InfoTooltip>
Select the default view to use when displaying query results.
</InfoTooltip>
</div>
<div>
<StyledChoiceGroup
ariaLabelledBy="defaultQueryResultsView"
onChange={[Function]}
options={
Array [
Object {
"key": "vertical",
"text": "Vertical",
},
Object {
"key": "horizontal",
"text": "Horizontal",
},
]
}
selectedKey="vertical"
styles={
Object {
"flexContainer": Array [
Object {
"selectors": Object {
".ms-ChoiceField": Object {
"marginTop": 0,
},
".ms-ChoiceField-wrapper label": Object {
"fontSize": 12,
"paddingTop": 0,
},
".ms-ChoiceFieldGroup root-133": Object {
"clear": "both",
},
},
},
],
"root": Object {
"clear": "both",
},
}
}
/>
</div>
</div>
</div>
<div
className="settingsSection"
>

View File

@@ -8,7 +8,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
explorer={
Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
@@ -689,6 +688,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
>
<CustomizedPrimaryButton
ariaLabel="Create"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
text="Create"
@@ -696,6 +696,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
>
<PrimaryButton
ariaLabel="Create"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
text="Create"
@@ -976,6 +977,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
>
<CustomizedDefaultButton
ariaLabel="Create"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -1258,6 +1260,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
>
<DefaultButton
ariaLabel="Create"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -1541,6 +1544,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
<BaseButton
ariaLabel="Create"
baseClassName="ms-Button"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -2415,6 +2419,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
aria-label="Create"
className="ms-Button ms-Button--primary root-122"
data-is-focusable={true}
data-test="Panel/OkButton"
id="sidePanelOkButton"
onClick={[Function]}
onKeyDown={[Function]}
@@ -2450,6 +2455,11 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
</div>
</PanelFooterComponent>
</form>
<span
className="screenReaderOnly"
id="screenReaderStatus"
role="status"
/>
</RightPaneForm>
</StringInputPane>
`;

View File

@@ -124,8 +124,8 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
setIsExecuting(true);
const entity: Entities.ITableEntity = entityFromAttributes(entities);
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
try {
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
await tableEntityListViewModel.addEntityToCache(newEntity);
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
tableEntityListViewModel.redrawTableThrottled();
@@ -261,6 +261,7 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
<TextField
multiline
rows={5}
ariaLabel={entityAttributeProperty}
value={entityAttributeValue}
onChange={(event, newInput?: string) => {
entityChange(newInput, selectedRow, "value");

View File

@@ -1258,6 +1258,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
>
<CustomizedPrimaryButton
ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
text="OK"
@@ -1265,6 +1266,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
>
<PrimaryButton
ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
text="OK"
@@ -1545,6 +1547,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
>
<CustomizedDefaultButton
ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -1827,6 +1830,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
>
<DefaultButton
ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -2110,6 +2114,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
<BaseButton
ariaLabel="OK"
baseClassName="ms-Button"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -2984,6 +2989,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
aria-label="OK"
className="ms-Button ms-Button--primary root-125"
data-is-focusable={true}
data-test="Panel/OkButton"
id="sidePanelOkButton"
onClick={[Function]}
onKeyDown={[Function]}
@@ -3019,6 +3025,11 @@ exports[`Table query select Panel should render Default properly 1`] = `
</div>
</PanelFooterComponent>
</form>
<span
className="screenReaderOnly"
id="screenReaderStatus"
role="status"
/>
</RightPaneForm>
</TableQuerySelectPanel>
`;

View File

@@ -369,6 +369,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
>
<CustomizedPrimaryButton
ariaLabel="Add Entity"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
text="Add Entity"
@@ -376,6 +377,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
>
<PrimaryButton
ariaLabel="Add Entity"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
text="Add Entity"
@@ -656,6 +658,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
>
<CustomizedDefaultButton
ariaLabel="Add Entity"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -938,6 +941,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
>
<DefaultButton
ariaLabel="Add Entity"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -1221,6 +1225,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
<BaseButton
ariaLabel="Add Entity"
baseClassName="ms-Button"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -2095,6 +2100,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
aria-label="Add Entity"
className="ms-Button ms-Button--primary root-113"
data-is-focusable={true}
data-test="Panel/OkButton"
id="sidePanelOkButton"
onClick={[Function]}
onKeyDown={[Function]}
@@ -2130,6 +2136,11 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
</div>
</PanelFooterComponent>
</form>
<span
className="screenReaderOnly"
id="screenReaderStatus"
role="status"
/>
</RightPaneForm>
</AddTableEntityPanel>
`;

View File

@@ -375,6 +375,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
>
<CustomizedPrimaryButton
ariaLabel="Update"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
text="Update"
@@ -382,6 +383,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
>
<PrimaryButton
ariaLabel="Update"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
text="Update"
@@ -662,6 +664,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
>
<CustomizedDefaultButton
ariaLabel="Update"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -944,6 +947,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
>
<DefaultButton
ariaLabel="Update"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -1227,6 +1231,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
<BaseButton
ariaLabel="Update"
baseClassName="ms-Button"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -2101,6 +2106,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
aria-label="Update"
className="ms-Button ms-Button--primary root-113"
data-is-focusable={true}
data-test="Panel/OkButton"
id="sidePanelOkButton"
onClick={[Function]}
onKeyDown={[Function]}
@@ -2136,6 +2142,11 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
</div>
</PanelFooterComponent>
</form>
<span
className="screenReaderOnly"
id="screenReaderStatus"
role="status"
/>
</RightPaneForm>
</EditTableEntityPanel>
`;

View File

@@ -1,91 +0,0 @@
import { Upload } from "Common/Upload/Upload";
import { useSidePanel } from "hooks/useSidePanel";
import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface UploadFilePanelProps {
uploadFile: (name: string, content: string) => Promise<NotebookContentItem>;
}
export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({ uploadFile }: UploadFilePanelProps) => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const extensions: string = undefined; //ex. ".ipynb"
const errorMessage = "Could not upload file";
const inProgressMessage = "Uploading file to notebook server";
const successMessage = "Successfully uploaded file to notebook server";
const [files, setFiles] = useState<FileList>();
const [formErrors, setFormErrors] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const submit = () => {
setFormErrors("");
if (!files || files.length === 0) {
setFormErrors("No file specified. Please input a file.");
logConsoleError(`${errorMessage} -- No file specified. Please input a file.`);
return;
}
const file: File = files.item(0);
const clearMessage = logConsoleProgress(`${inProgressMessage}: ${file.name}`);
setIsExecuting(true);
onSubmit(files.item(0))
.then(
() => {
logConsoleInfo(`${successMessage} ${file.name}`);
closeSidePanel();
},
(error: string) => {
setFormErrors(errorMessage);
logConsoleError(`${errorMessage} ${file.name}: ${error}`);
},
)
.finally(() => {
setIsExecuting(false);
clearMessage();
});
};
const updateSelectedFiles = (event: ChangeEvent<HTMLInputElement>): void => {
setFiles(event.target.files);
};
const onSubmit = async (file: File): Promise<NotebookContentItem> => {
const readFileAsText = (inputFile: File): Promise<string> => {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onerror = () => {
reader.abort();
reject(`Problem parsing file: ${inputFile}`);
};
reader.onload = () => {
resolve(reader.result as string);
};
reader.readAsText(inputFile);
});
};
const fileContent = await readFileAsText(file);
return uploadFile(file.name, fileContent);
};
const props: RightPaneFormProps = {
formError: formErrors,
isExecuting: isExecuting,
submitButtonText: "Upload",
onSubmit: submit,
};
return (
<RightPaneForm {...props}>
<div className="paneMainContent">
<Upload label="Select file to upload" accept={extensions} onUpload={updateSelectedFiles} />
</div>
</RightPaneForm>
);
};

View File

@@ -3,6 +3,7 @@
exports[`AddCollectionPanel should render Default properly 1`] = `
<form
className="panelFormWrapper"
id="panelContainer"
onSubmit={[Function]}
>
<div
@@ -433,7 +434,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
>
<Stack
className="panelGroupSpacing"
id="collapsibleSectionContent"
id="collapsibleAdvancedSectionContent"
>
<Stack
className="panelGroupSpacing"
@@ -466,7 +467,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
<Icon
className="removeIcon"
iconName="InfoSolid"
tabIndex={0}
/>
To ensure compatibility with older SDKs, the created container will use a legacy partitioning scheme that supports partition key values of size only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.

View File

@@ -361,12 +361,15 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<span
className="css-113"
>
Confirm by typing the database id
Confirm by typing the
Database
id
</span>
</Text>
<StyledTextFieldBase
ariaLabel="Confirm by typing the database id"
ariaLabel="Confirm by typing the Database id"
autoFocus={true}
data-test="Input:confirmDatabaseId"
id="confirmDatabaseId"
onChange={[Function]}
required={true}
@@ -379,8 +382,9 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
}
>
<TextFieldBase
ariaLabel="Confirm by typing the database id"
ariaLabel="Confirm by typing the Database id"
autoFocus={true}
data-test="Input:confirmDatabaseId"
deferredValidationTime={200}
id="confirmDatabaseId"
onChange={[Function]}
@@ -673,9 +677,10 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
>
<input
aria-invalid={false}
aria-label="Confirm by typing the database id"
aria-label="Confirm by typing the Database id"
autoFocus={true}
className="ms-TextField-field field-117"
data-test="Input:confirmDatabaseId"
id="confirmDatabaseId"
onBlur={[Function]}
onChange={[Function]}
@@ -711,11 +716,13 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<span
className="css-126"
>
What is the reason why you are deleting this database?
What is the reason why you are deleting this
Database
?
</span>
</Text>
<StyledTextFieldBase
ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this Database?"
id="deleteDatabaseFeedbackInput"
multiline={true}
onChange={[Function]}
@@ -729,7 +736,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
}
>
<TextFieldBase
ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this Database?"
deferredValidationTime={200}
id="deleteDatabaseFeedbackInput"
multiline={true}
@@ -1023,7 +1030,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
>
<textarea
aria-invalid={false}
aria-label="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
aria-label="Help us improve Azure Cosmos DB! What is the reason why you are deleting this Database?"
className="ms-TextField-field field-128"
id="deleteDatabaseFeedbackInput"
onBlur={[Function]}
@@ -1049,6 +1056,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
>
<CustomizedPrimaryButton
ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
text="OK"
@@ -1056,6 +1064,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
>
<PrimaryButton
ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
text="OK"
@@ -1336,6 +1345,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
>
<CustomizedDefaultButton
ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -1618,6 +1628,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
>
<DefaultButton
ariaLabel="OK"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -1901,6 +1912,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<BaseButton
ariaLabel="OK"
baseClassName="ms-Button"
data-test="Panel/OkButton"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
@@ -2775,6 +2787,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
aria-label="OK"
className="ms-Button ms-Button--primary root-130"
data-is-focusable={true}
data-test="Panel/OkButton"
id="sidePanelOkButton"
onClick={[Function]}
onKeyDown={[Function]}
@@ -2810,6 +2823,11 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
</div>
</PanelFooterComponent>
</form>
<span
className="screenReaderOnly"
id="screenReaderStatus"
role="status"
/>
</RightPaneForm>
</DeleteDatabaseConfirmationPanel>
`;

View File

@@ -4,6 +4,7 @@ exports[`PaneContainerComponent test should be resize if notification console is
<StyledPanelBase
closeButtonAriaLabel="Close test"
customWidth="440px"
data-test="Panel:test"
headerClassName="panelHeader"
headerText="test"
isFooterAtBottom={true}
@@ -44,6 +45,7 @@ exports[`PaneContainerComponent test should render with panel content and header
<StyledPanelBase
closeButtonAriaLabel="Close test"
customWidth="440px"
data-test="Panel:test"
headerClassName="panelHeader"
headerText="test"
isFooterAtBottom={true}

View File

@@ -49,7 +49,7 @@ export const QueryCopilotFeedbackModal = ({
};
return (
<Modal isOpen={showFeedbackModal}>
<Modal isOpen={showFeedbackModal} styles={{ main: { borderRadius: 8, maxWidth: 600 } }}>
<form onSubmit={handleSubmit}>
<Stack style={{ padding: 24 }}>
<Stack horizontal horizontalAlign="space-between">
@@ -68,9 +68,14 @@ export const QueryCopilotFeedbackModal = ({
rows={3}
/>
<TextField
styles={{ root: { marginBottom: 14 } }}
styles={{
root: { marginBottom: 14 },
fieldGroup: { backgroundColor: "#F3F2F1", borderRadius: 4, borderColor: "#D1D1D1" },
}}
label="Query generated"
defaultValue={generatedQuery}
multiline
rows={3}
readOnly
/>
<Text style={{ fontSize: 12, marginBottom: 14 }}>

View File

@@ -3,6 +3,14 @@
exports[`Query Copilot Feedback Modal snapshot test shoud render and match snapshot 1`] = `
<Modal
isOpen={true}
styles={
Object {
"main": Object {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
@@ -67,9 +75,16 @@ exports[`Query Copilot Feedback Modal snapshot test shoud render and match snaps
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
Object {
"fieldGroup": Object {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": Object {
"marginBottom": 14,
},
@@ -125,6 +140,14 @@ exports[`Query Copilot Feedback Modal snapshot test shoud render and match snaps
exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`] = `
<Modal
isOpen={false}
styles={
Object {
"main": Object {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
@@ -189,9 +212,16 @@ exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`]
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
Object {
"fieldGroup": Object {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": Object {
"marginBottom": 14,
},
@@ -247,6 +277,14 @@ exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`]
exports[`Query Copilot Feedback Modal snapshot test should close on cancel click 1`] = `
<Modal
isOpen={false}
styles={
Object {
"main": Object {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
@@ -311,9 +349,16 @@ exports[`Query Copilot Feedback Modal snapshot test should close on cancel click
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
Object {
"fieldGroup": Object {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": Object {
"marginBottom": 14,
},
@@ -369,6 +414,14 @@ exports[`Query Copilot Feedback Modal snapshot test should close on cancel click
exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] = `
<Modal
isOpen={false}
styles={
Object {
"main": Object {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
@@ -433,9 +486,16 @@ exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] =
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
Object {
"fieldGroup": Object {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": Object {
"marginBottom": 14,
},
@@ -491,6 +551,14 @@ exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] =
exports[`Query Copilot Feedback Modal snapshot test should not render dont show again button 1`] = `
<Modal
isOpen={false}
styles={
Object {
"main": Object {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
@@ -555,9 +623,16 @@ exports[`Query Copilot Feedback Modal snapshot test should not render dont show
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
Object {
"fieldGroup": Object {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": Object {
"marginBottom": 14,
},
@@ -613,6 +688,14 @@ exports[`Query Copilot Feedback Modal snapshot test should not render dont show
exports[`Query Copilot Feedback Modal snapshot test should render dont show again button and check it 1`] = `
<Modal
isOpen={true}
styles={
Object {
"main": Object {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
@@ -677,9 +760,16 @@ exports[`Query Copilot Feedback Modal snapshot test should render dont show agai
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
Object {
"fieldGroup": Object {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": Object {
"marginBottom": 14,
},
@@ -750,6 +840,14 @@ exports[`Query Copilot Feedback Modal snapshot test should render dont show agai
exports[`Query Copilot Feedback Modal snapshot test should submit submission 1`] = `
<Modal
isOpen={false}
styles={
Object {
"main": Object {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
@@ -814,9 +912,16 @@ exports[`Query Copilot Feedback Modal snapshot test should submit submission 1`]
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
Object {
"fieldGroup": Object {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": Object {
"marginBottom": 14,
},

View File

@@ -11,8 +11,8 @@ import {
Link,
MessageBar,
MessageBarType,
ProgressIndicator,
Separator,
Spinner,
Stack,
TeachingBubble,
Text,
@@ -36,7 +36,6 @@ import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React, { useRef, useState } from "react";
import HintIcon from "../../../images/Hint.svg";
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
import RecentIcon from "../../../images/Recent.svg";
import errorIcon from "../../../images/close-black.svg";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
@@ -215,12 +214,12 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
if (response.ok) {
if (generateSQLQueryResponse?.sql !== "N/A") {
let query = `-- **Prompt:** ${userPrompt}\r\n`;
if (generateSQLQueryResponse.explanation) {
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
}
query += generateSQLQueryResponse.sql;
setQuery(query);
const queryExplanation = `-- **Explanation of query:** ${
generateSQLQueryResponse.explanation ? generateSQLQueryResponse.explanation : "N/A"
}\r\n`;
const currentGeneratedQuery = queryExplanation + generateSQLQueryResponse.sql;
const lastQuery = generatedQuery && query ? `${query}\r\n` : "";
setQuery(`${lastQuery}${currentGeneratedQuery}`);
setGeneratedQuery(generateSQLQueryResponse.sql);
setGeneratedQueryComments(generateSQLQueryResponse.explanation);
setShowFeedbackBar(true);
@@ -310,29 +309,24 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
return (
<Stack
className="copilot-prompt-pane"
styles={{ root: { backgroundColor: "#FAFAFA", padding: "16px 24px 0px" } }}
styles={{ root: { backgroundColor: "#FAFAFA", padding: "8px" } }}
id="copilot-textfield-label"
>
<Stack horizontal>
<Image src={CopilotIcon} style={{ width: 24, height: 24 }} alt="Copilot" role="none" />
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
<IconButton
iconProps={{ imageProps: { src: errorIcon } }}
onClick={() => {
toggleCopilot(false);
clearFeedback();
resetMessageStates();
}}
<Stack
horizontal
styles={{
root: {
marginLeft: "auto !important",
width: "100%",
borderWidth: 1,
borderStyle: "solid",
borderColor: "#D1D1D1",
borderRadius: 8,
boxShadow: "0px 4px 8px 0px #00000024",
},
}}
ariaLabel="Close"
title="Close copilot"
/>
</Stack>
<Stack horizontal verticalAlign="center">
>
<Stack style={{ width: "100%" }}>
<Stack horizontal verticalAlign="center" style={{ padding: "8px 8px 0px 8px" }}>
<TextField
id="naturalLanguageInput"
value={userPrompt}
@@ -348,11 +342,38 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
}
}}
style={{ lineHeight: 30 }}
styles={{ root: { width: "95%" }, fieldGroup: { borderRadius: 6 } }}
styles={{
root: { width: "100%" },
suffix: {
background: "none",
padding: 0,
},
fieldGroup: {
borderRadius: 4,
borderColor: "#D1D1D1",
"::after": {
border: "inherit",
borderWidth: 2,
borderBottomColor: "#464FEB",
borderRadius: 4,
},
},
}}
disabled={isGeneratingQuery}
autoComplete="off"
placeholder="Ask a question in natural language and well generate the query for you."
aria-labelledby="copilot-textfield-label"
onRenderSuffix={() => {
return (
<IconButton
iconProps={{ iconName: "Send" }}
disabled={isGeneratingQuery || !userPrompt.trim()}
style={{ background: "none" }}
onClick={() => startGenerateQueryProcess()}
aria-label="Send"
/>
);
}}
/>
{showPromptTeachingBubble && copilotTeachingBubbleVisible && (
<TeachingBubble
@@ -364,7 +385,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
hasSmallHeadline={true}
headline="Write a prompt"
>
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
Write a prompt here and Query Advisor will generate the query for you. You can also choose from our{" "}
<Link
onClick={() => {
setShowSamplePrompts(true);
@@ -377,16 +398,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
or write your own query
</TeachingBubble>
)}
<IconButton
iconProps={{ iconName: "Send" }}
disabled={isGeneratingQuery || !userPrompt.trim()}
style={{ marginLeft: 8 }}
onClick={() => startGenerateQueryProcess()}
aria-label="Send"
/>
<div role="alert" aria-label={getAriaLabel()}>
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
</div>
{showSamplePrompts && (
<Callout
styles={{ root: { minWidth: 400, maxWidth: "70vw" } }}
@@ -488,8 +499,9 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
</Callout>
)}
</Stack>
<Stack style={{ margin: "8px 0" }}>
{!isGeneratingQuery && (
<Stack style={{ padding: 8 }}>
{!showFeedbackBar && (
<Text style={{ fontSize: 12 }}>
AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "}
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank" style={{ color: "#0072D4" }}>
@@ -505,8 +517,8 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
messageBarType={MessageBarType.info}
styles={{ root: { backgroundColor: "#F0F6FF" }, icon: { color: "#015CDA" } }}
>
We were unable to generate a query based upon the prompt provided. Please modify the prompt and try again.
For examples of how to write a good prompt, please read
We were unable to generate a query based upon the prompt provided. Please modify the prompt and
try again. For examples of how to write a good prompt, please read
<Link href="https://aka.ms/cdb-copilot-writing" target="_blank">
this article.
</Link>{" "}
@@ -517,21 +529,27 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
</MessageBar>
)}
</Text>
</Stack>
)}
{showFeedbackBar && (
<Stack
style={{ backgroundColor: "#FFF8F0", padding: "2px 8px", minHeight: 32 }}
horizontal
verticalAlign="center"
>
<Stack horizontal verticalAlign="center" style={{ maxHeight: 20 }}>
{userContext.feedbackPolicies?.policyAllowFeedback && (
<Stack horizontal verticalAlign="center">
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
<Text style={{ fontSize: 12 }}>Provide feedback</Text>
{showCallout && !hideFeedbackModalForLikedQueries && (
<Callout
role="status"
style={{ padding: 8 }}
style={{ padding: "6px 12px" }}
styles={{
root: {
borderRadius: 8,
},
beakCurtain: {
borderRadius: 8,
},
calloutMain: {
borderRadius: 8,
},
}}
target="#likeBtn"
onDismiss={() => {
setShowCallout(false);
@@ -565,7 +583,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
)}
<IconButton
id="likeBtn"
style={{ marginLeft: 20 }}
style={{ marginLeft: 10 }}
aria-label="Like"
role="toggle"
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
@@ -584,7 +602,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
}}
/>
<IconButton
style={{ margin: "0 10px" }}
style={{ margin: "0 4px" }}
role="toggle"
aria-label="Dislike"
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
@@ -601,16 +619,30 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
}}
/>
<span role="status" style={{ position: "absolute", left: "-9999px" }} id="likeStatus"></span>
<Separator vertical style={{ color: "#EDEBE9" }} />
<Separator
vertical
styles={{
root: {
"::after": {
backgroundColor: "#767676",
},
},
}}
/>
</Stack>
)}
<CommandBarButton
className="copyQuery"
onClick={copyGeneratedCode}
iconProps={{ iconName: "Copy" }}
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }}
styles={{
root: {
backgroundColor: "inherit",
},
}}
>
Copy query
Copy code
</CommandBarButton>
<CommandBarButton
className="deleteQuery"
@@ -618,12 +650,58 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
setShowDeletePopup(true);
}}
iconProps={{ iconName: "Delete" }}
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }}
styles={{
root: {
backgroundColor: "inherit",
},
}}
>
Delete query
Clear editor
</CommandBarButton>
</Stack>
)}
</Stack>
)}
{isGeneratingQuery && (
<ProgressIndicator
label="Thinking..."
ariaLabel={getAriaLabel()}
barHeight={4}
styles={{
root: {
fontSize: 12,
width: "100%",
bottom: 0,
},
itemName: {
padding: "0px 8px",
},
itemProgress: {
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
padding: 0,
},
progressBar: {
backgroundImage:
"linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgb(24, 90, 189) 35%, rgb(71, 207, 250) 70%, rgb(180, 124, 248) 92%, rgba(0, 0, 0, 0))",
animationDuration: "5s",
},
}}
/>
)}
</Stack>
<IconButton
iconProps={{ imageProps: { src: errorIcon } }}
onClick={() => {
toggleCopilot(false);
clearFeedback();
resetMessageStates();
}}
ariaLabel="Close"
title="Close copilot"
/>
</Stack>
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
{query !== "" && query.trim().length !== 0 && (
<DeletePopup

View File

@@ -57,12 +57,12 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
const toggleCopilotButton = {
iconSrc: QueryCommandIcon,
iconAlt: "Copilot",
iconAlt: "Query Advisor",
onCommandClick: () => {
toggleCopilot(true);
},
commandButtonLabel: "Copilot",
ariaLabel: "Copilot",
commandButtonLabel: "Query Advisor",
ariaLabel: "Query Advisor",
hasPopup: false,
disabled: copilotActive,
};

View File

@@ -1,6 +1,7 @@
import { FeedOptions } from "@azure/cosmos";
import {
Areas,
BackendApi,
ConnectionStatusType,
ContainerStatusType,
HttpStatusCodes,
@@ -30,6 +31,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { useTabs } from "hooks/useTabs";
@@ -80,7 +82,11 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis
};
export const getCopilotEnabled = async (): Promise<boolean> => {
const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`;
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings)
? configContext.PORTAL_BACKEND_ENDPOINT
: configContext.BACKEND_ENDPOINT;
const url = `${backendEndpoint}/api/portalsettings/querycopilot`;
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token };

View File

@@ -23,7 +23,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
explorer={
Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],

View File

@@ -25,7 +25,6 @@ import * as React from "react";
import ConnectIcon from "../../../images/Connect_color.svg";
import ContainersIcon from "../../../images/Containers.svg";
import LinkIcon from "../../../images/Link_blue.svg";
import NotebookColorIcon from "../../../images/Notebooks.svg";
import PowerShellIcon from "../../../images/PowerShell.svg";
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
@@ -151,9 +150,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
{useQueryCopilot.getState().copilotEnabled && (
<SplashScreenButton
imgSrc={CopilotIcon}
title={"Query faster with Copilot"}
title={"Query faster with Query Advisor"}
description={
"Copilot is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
"Query Advisor is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
}
onClick={() => {
const copilotVersion = userContext.features.copilotVersion;
@@ -410,14 +409,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
},
};
heroes.push(launchQuickstartBtn);
} else if (useNotebook.getState().isPhoenixNotebooks) {
const newNotebookBtn = {
iconSrc: NotebookColorIcon,
title: "New notebook",
description: "Visualize your data stored in Azure Cosmos DB",
onClick: () => this.container.onNewNotebookClicked(),
};
heroes.push(newNotebookBtn);
}
heroes.push(this.getShellCard());
@@ -689,11 +680,20 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: "Learn the Fundamentals",
description: "Watch Azure Cosmos DB Live TV show introductory and how to videos.",
};
let items: item[];
const commonItems: item[] = [
{
link: "https://learn.microsoft.com/azure/cosmos-db/data-explorer-shortcuts",
title: "Data Explorer keyboard shortcuts",
description: "Learn keyboard shortcuts to navigate Data Explorer.",
},
];
let apiItems: item[];
switch (userContext.apiType) {
case "SQL":
case "Postgres":
items = [
apiItems = [
{
link: "https://aka.ms/msl-sdk-connect",
title: "Get Started using an SDK",
@@ -708,7 +708,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
];
break;
case "Mongo":
items = [
apiItems = [
{
link: "https://aka.ms/mongonodejs",
title: "Build an app with Node.js",
@@ -723,7 +723,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
];
break;
case "Cassandra":
items = [
apiItems = [
{
link: "https://aka.ms/cassandracontainer",
title: "Create a Container",
@@ -738,7 +738,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
];
break;
case "Gremlin":
items = [
apiItems = [
{
link: "https://aka.ms/graphquickstart",
title: "Get Started ",
@@ -753,7 +753,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
];
break;
case "Tables":
items = [
apiItems = [
{
link: "https://aka.ms/tabledotnet",
title: "Build a .NET App",
@@ -770,6 +770,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
default:
break;
}
const items = [...commonItems, ...apiItems];
return (
<Stack>
{items.map((item, i) => (

View File

@@ -225,3 +225,8 @@ export const InputType = {
DateTime: "datetime-local",
Number: "number",
};
export const labelToLoadingItemName: Record<string, string> = {
"Add Row": "Adding row to table",
"Add Entity": "Adding entity to table",
};

View File

@@ -1,5 +1,6 @@
import ko from "knockout";
import { isEnvironmentAltPressed, isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils";
import * as Constants from "../Constants";
import * as Entities from "../Entities";
import * as Utilities from "../Utilities";
@@ -28,7 +29,7 @@ export default class DataTableOperationManager {
var elem: JQuery<Element> = $(event.currentTarget);
this.updateLastSelectedItem(elem, event.shiftKey);
if (Utilities.isEnvironmentCtrlPressed(event)) {
if (isEnvironmentCtrlPressed(event)) {
this.applyCtrlSelection(elem);
} else if (event.shiftKey) {
this.applyShiftSelection(elem);
@@ -74,9 +75,9 @@ export default class DataTableOperationManager {
DataTableOperations.scrollToRowIfNeeded(dataTableRows, safeIndex, isUpArrowKey);
}
} else if (
Utilities.isEnvironmentCtrlPressed(event) &&
!Utilities.isEnvironmentShiftPressed(event) &&
!Utilities.isEnvironmentAltPressed(event) &&
isEnvironmentCtrlPressed(event) &&
!isEnvironmentShiftPressed(event) &&
!isEnvironmentAltPressed(event) &&
event.keyCode === Constants.keyCodes.A
) {
this.applySelectAll();

View File

@@ -3,6 +3,7 @@ import * as ko from "knockout";
import Q from "q";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants";
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
import { handleError } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { createDocument } from "../../Common/dataAccess/createDocument";
@@ -19,7 +20,6 @@ import Explorer from "../Explorer";
import * as TableConstants from "./Constants";
import * as Entities from "./Entities";
import * as TableEntityProcessor from "./TableEntityProcessor";
import { CassandraProxyAPIs } from "../../Common/Constants";
export interface CassandraTableKeys {
partitionKeys: CassandraTableKey[];
@@ -172,8 +172,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(entity);
},
(error) => {
handleError(error, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
deferred.reject(error);
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(errorText, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
deferred.reject(errorText);
},
)
.finally(clearInProgressMessage);
@@ -406,12 +407,13 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve();
},
(error) => {
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(
error,
errorText,
"CreateKeyspaceCassandra",
`Error while creating a keyspace with query ${createKeyspaceQuery}`,
);
deferred.reject(error);
deferred.reject(errorText);
},
)
.finally(clearInProgressMessage);
@@ -444,8 +446,13 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve();
},
(error) => {
handleError(error, "CreateTableCassandra", `Error while creating a table with query ${createTableQuery}`);
deferred.reject(error);
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(
errorText,
"CreateTableCassandra",
`Error while creating a table with query ${createTableQuery}`,
);
deferred.reject(errorText);
},
)
.finally(clearInProgressMessage);
@@ -458,7 +465,7 @@ export class CassandraAPIDataClient extends TableDataClient {
}
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
if (!this.useCassandraProxyEndpoint("getTableKeys")) {
if (!this.useCassandraProxyEndpoint("getKeys")) {
return this.getTableKeys_ToBeDeprecated(collection);
}
@@ -493,8 +500,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data);
},
(error: any) => {
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(error);
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(errorText);
},
)
.done(clearInProgressMessage);
@@ -533,8 +541,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data);
},
(error: any) => {
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(error);
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(errorText);
},
)
.done(clearInProgressMessage);
@@ -578,8 +587,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data.columns);
},
(error: any) => {
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(error);
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(errorText);
},
)
.done(clearInProgressMessage);
@@ -618,8 +628,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data.columns);
},
(error: any) => {
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(error);
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(errorText);
},
)
.done(clearInProgressMessage);
@@ -732,17 +743,23 @@ export class CassandraAPIDataClient extends TableDataClient {
}
private useCassandraProxyEndpoint(api: string): boolean {
const activeCassandraProxyEndpoints: string[] = [
CassandraProxyEndpoints.Development,
CassandraProxyEndpoints.Mpac,
CassandraProxyEndpoints.Prod,
];
let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
if (userContext.databaseAccount.properties.ipRules?.length > 0) {
if (
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development &&
userContext.databaseAccount.properties.ipRules?.length > 0
) {
canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED;
}
return (
canAccessCassandraProxy &&
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
[Constants.CassandraProxyEndpoints.Development, Constants.CassandraProxyEndpoints.Mpac].includes(
configContext.CASSANDRA_PROXY_ENDPOINT,
)
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
);
}
}

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