Compare commits

...

54 Commits

Author SHA1 Message Date
Senthamil Sindhu
b432bd38eb Support NPS for PostGres 2023-08-31 19:25:06 -07:00
Senthamil Sindhu
ecb39dd99f Fix comment on indentation 2023-08-28 09:46:29 -07:00
Senthamil Sindhu
51782f1157 Remove NPS feature flag 2023-08-28 09:19:26 -07:00
Predrag Klepic
425e375d50 Reintroduce feature flags (#1590)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-08-28 16:56:13 +02:00
Armando Trejo Oliver
55846b98bd Send undefined poolId instead of "default" pool (#1589) 2023-08-24 16:05:09 -07:00
Armando Trejo Oliver
3d02f07262 Add poolId as parameter to allocateContainer (#1588) 2023-08-24 12:56:31 -07:00
Predrag Klepic
449118a1bf [Query Copilot] V2 Backend integration (#1584)
* Initial implementetation of backend integration

* Added parameters and interfaces moved

* Initial client implementation

* Additional changes for React FC's

* Updated snapshot of Footer

* Additional Copilot implementation

* Test adjustments and client implementation

* Additional test implementations

* Naming convetion for functions

* Changing {} to any

* Additional changes to the type

* Additional test changes

* Removal of prevention

* adding comment

* Additional changes to tests

* Moving logic based on comments

* client implementation along with corrected tests

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-08-24 09:07:29 +02:00
jawelton74
8405dbe8da Fix spelling of Refresh for self-serve blades. (#1585) 2023-08-23 16:56:12 -07:00
v-darkora
19100ec437 [Query Copilot V2] Unit tests for V2 Copilot (#1580)
* Add tests for V2 of copilot and fix query parameter feature flag

* Fix merge changes
2023-08-21 16:29:00 +02:00
Predrag Klepic
ebd40cb9b0 [Query Copilot] Query Copilot button dropdown and shortcut (#1582)
* Copilot dropdown implementation

* additional changes

* added unit test for key event

* Additional test changes

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-08-21 16:17:06 +02:00
Predrag Klepic
986dbe7d54 [Query Copilot] Adding feature flags for phoenix and Schema (#1583)
* Adding feature flags for phoenix and schema

* Feedback uri modified

* Additional Constant changes

* Reverting PriorityLevel

* Additional changes in tests

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-08-21 16:16:32 +02:00
Predrag Klepic
143f7d8f2c [Query Copilot] Copilot V2 Retrieving bubble (#1579)
* Changing order of the features

* test name changed

* updates snapshots

* Bubble implementation

* Bubble tests implemented

* Correction for CopilotSampleDB implementation

* rollback to previous query tab changes

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-08-21 11:19:04 +02:00
FAIZ CHACHIYA
b646f9f4cb Wmt priority execution feature (#1546)
* Execute the queries with high/low priority

* improvement made to the Cosmos client and created separate plugin class for setting priority header

* added test cases for the priority execution utility

* removed unwanted code

* fix compile time issues

* fix compile time issues

* fixed lint and stylinkg issues

* fixed lint and styling issues

* skip the lint check for src/Utils/PriorityBasedExecutionUtils.ts

* incorporating review comments, added the default priority level changes

* changed the priority to default instead of low

* removed the unwanted if condition

---------

Co-authored-by: Faiz Chachiya <faizchachiya@microsoft.com>
2023-08-18 15:40:35 +05:30
Predrag Klepic
0f52db73e7 [Query Copilot] Fix failing comparison for CopilosSampleDB (#1581)
* Fix failing comparison for CopilosSampleDB

* using constant

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-08-18 10:48:18 +02:00
Predrag Klepic
9c1b9e6ff6 [Query Copilot] Allocation of container and copilot request sent to Phoenix endpoint (#1576)
* Switch to tools federation endpoints for query copilot

* Remove extra / in url

* Initial allocateContainer implementation

* Additional feedback modal changes

* updated tests

* PhoenixClient reverted to previous endpoint

* Changes based on PR comments

* Update based on tests

* updated endpoint

* Back to previous implementation and test improve

* removing notebook

---------

Co-authored-by: Victor Meng <vimeng@microsoft.com>
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-08-18 10:47:19 +02:00
MokireddySampath
19041ffedd Border at the bottom has been added for the tabs to differntiate from unselected tabs (#1509)
* Border at the bottom has been added  for the tabs to differntiate from unselected tabs while using contrast themes

* Update Tabs.tsx

* Update Tabs.tsx
2023-08-17 23:11:25 +05:30
v-darkora
ceb66ed5b8 [Query Copilot V2] Restructure code for V2 of copilot (#1577)
* Initial folder and code restructure

* Add snapshots

* Remove flag for local testing

* Remove unnecessary code

* Fix snapshot

* Update tests
2023-08-16 10:10:21 +02:00
Predrag Klepic
96b88ac344 [Query Copilot] Version 2 initial code (#1566)
* Query Copilot Sidecar initial commit

* additional improvements with the Welcome pop

* Additional implementation and messages for sidecar

* introducing copilot version

* Renaming from car to bar

* Image names changed

* fixing PR errors

* additional changes related with the versions

* Additional implementations and fixes

* Removing unused interface

* Additional styling changes and state management

* Additional changes related with Sidebar

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-08-15 10:01:07 +02:00
MokireddySampath
bcedf0a29f Max value has been added to throughput for unsharded and sharded collections (#1506)
* arialabel has been added to close button of invitational youtube video

* heading role has been addedd and tag has been changed to h1

* outline has been restored to choose columns link in entities page

* Update QuickstartCarousel.tsx

* Update SplashScreen.tsx

* Update TableEntity.tsx

* outline for edit entity has been added on focus

* keyboard accessibility added to rows in table entities

* learn more link under analytical store

* Column header is populated with text

* aria label has been changed for  the screen readers to read placeholder text along with label text

* Update queryBuilder.less

* Update TableEntity.tsx

* Update ThroughputInputAutoPilotV3Component.tsx

* Update ThroughputInputAutoPilotV3Component.tsx

* Update ThroughputInputAutoPilotV3Component.test.tsx.snap

* Update ThroughputInput.less

* Update PanelComponent.less

* Update DataTableBindingManager.ts

* Update AddCollectionPanel.tsx

* Update AddCollectionPanel.test.tsx.snap

* spec.ts files updated for the tests

* update to container.spec files

* info button in stats tab is made accessible with keyboard

* focus order after invoking entities has been made to move logicallt from the entities tab instead of the table getting focus

* For unsharded collections the max value has been added

* Update AddCollectionPanel.tsx

* Update AddCollectionPanel.test.tsx.snap

* Update DataTableBindingManager.ts

* Update QueryTabComponent.tsx

* Update QueryTabComponent.tsx

* Update container.spec.ts

* Update container.spec.ts

* Update container32.spec.ts

* Update container.spec.ts

* Update container.spec.ts
2023-08-14 21:51:58 +05:30
MokireddySampath
67133017ce Defect2266802 (#1455)
* arialabel has been added to close button of invitational youtube video

* heading role has been addedd and tag has been changed to h1

* outline has been restored to choose columns link in entities page

* header text color changed to meet luminosity ratio requirement

* Update queryBuilder.less

* Update TableEntity.tsx

* Update QuickstartCarousel.tsx

* Update SplashScreen.tsx

* Update SplashScreen.tsx
2023-08-14 21:50:24 +05:30
MokireddySampath
b5d7ab0a30 Improper arialabel aria-name has been removed (#1561) 2023-08-14 21:49:15 +05:30
MokireddySampath
6dba4937ce Fix 1586730 : WCAG 4.1.1: Ensures every id attribute value used in ARIA and in labels is unique (input[aria-labelledby="TextFieldLabel339"]) (#1562)
* id has been removed for the input elements in add table row dialog since they have no significance in selection of elements and they are repetetive which is not a good practice

* Update EntityValue.tsx

* Update EntityValue.tsx
2023-08-14 21:48:48 +05:30
MokireddySampath
1a3ca94efb Bug 2262682:Screen reader is not announcing the updated deleting status information after deleting the container. (#1563) 2023-08-14 21:48:25 +05:30
Predrag Klepic
9f7783f3f9 [Query Copilot] Revise Copilot UI texts (#1569)
* Revise Copilot UI texts

* Test snapshots updated

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-08-09 21:43:55 +02:00
sindhuba
daa26d289b Fix sendMessage in DE (#1570)
* Fix issue in sendMessage in DE

* Run npm format
2023-08-09 09:28:25 -07:00
sindhuba
879cb08949 Fix issue with feature flag (#1567) 2023-08-08 08:03:42 -07:00
sindhuba
92f43c28a7 Add logic in DE to show NPS Survey (#1565)
* Send messages from DE to Portal to display NPS Survey

* Address comments
2023-08-04 13:24:30 -07:00
bogercraig
5f0c7bcea2 Users/bogercraig/endpointvalidation (#1554)
* Adding example endpoint with trailing forward slash.

* Move backend and ARM endpoint validation to configContext for initialization from config.json.

* Added debugging script and attempts to relocate endpoint validation list.

* Move default endpoint list to endpoint validation code and allow falling back to the default list during unit tests if configContext is not initialized.

* Remove leftover debugger statements.

* Remove test debug script in package.json for debugging unit tests in browser.

* Run prettier on modified files.

* Overwriting with package.json from master.

* Overwriting with version from master.

* Remove test ARM endpoint.

* Replace ternary operator with || for more concise arguments per Victor's feedback.

---------

Co-authored-by: Craig Boger <craig.boger@microsoft.com>
2023-08-03 14:47:50 -04:00
Predrag Klepic
fa55d528ad [Query Copilot] Maintain Query Copilot state when switching tabs (#1559)
* Sample implementation for saving states

* Maintaining Query Copilot state

* Fixing failed PR checks

* Additional changes based on checks

* snapshots updated

* Changes based on merging previous PR

* test mock changed

* Fixing minor bug for close button

* Destruct of queryCopilotState

* passing only function in Tabs component

* Maintaining copilot state with zustand store

* additional test changes

* test snapshot updated

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-08-03 09:23:31 +02:00
Predrag Klepic
c873fed7aa Disable Query Copilot Tab flickering (#1564)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-08-02 17:37:37 +02:00
v-darkora
7ec5290293 [Query Copilot] Update feature flag after sample data connection info fetch (#1560)
* Update feature flag if sample data exists

* Add additional conditional

* Revert useknockout to starting condition

* Use tracked property for rendering conditiona
2023-07-28 09:43:12 +02:00
v-darkora
4005128211 [Query Copilot] Add telemetry for query copilot (#1555) 2023-07-27 11:41:41 -07:00
v-darkora
38d13cc74e [Query Copilot] Generate query on enter (#1558) 2023-07-27 10:45:42 -07:00
Predrag Klepic
4ab93a5a32 [Query Copilot] Updated Copilot links (#1556)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-27 10:43:24 -07:00
Predrag Klepic
44e25c0769 [Query Copilot] New Query button added on Items section (#1552)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-27 10:42:17 -07:00
MokireddySampath
8ea8f0230f role has been changed to heading (#1453)
* arialabel has been added to close button of invitational youtube video

* heading role has been addedd and tag has been changed to h1

* Update QuickstartCarousel.tsx

* Update QuickstartCarousel.tsx

* Update SplashScreen.tsx
2023-07-27 07:30:27 +05:30
MokireddySampath
655b998b84 outline has been added to choose columns links (#1454)
* arialabel has been added to close button of invitational youtube video

* heading role has been addedd and tag has been changed to h1

* outline has been restored to choose columns link in entities page

* Update QuickstartCarousel.tsx

* Update SplashScreen.tsx

* Update TableEntity.tsx

* outline for edit entity has been added on focus
2023-07-27 07:29:54 +05:30
Predrag Klepic
9e2f6a9c89 [Query Copilot] QueryCopilotUtilities.ts tests (#1550)
* Improving test coverage

* Not leaving empty functions

* Additional test editing

* Correction of the unit test

* Changes made so the tests work correctly

* removing problematic tests

* QueryCopilotUtilities

* Changes based on Lint suggestion

* Changes based on lint

* Additional lint suggestion solved

* sample implementation

* removing console log

* Fixing non empty lint function error

* Changed any to void in mocked function

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-25 09:16:10 +02:00
Predrag Klepic
42e11d5160 [Query Copilot] Hide error message bar when request is successful (#1542)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-19 16:05:09 -07:00
Predrag Klepic
10037d844e [Query Copilot] Improving test coverage (#1539)
* Improving test coverage

* Not leaving empty functions

* Additional test editing

* Correction of the unit test

* Changes made so the tests work correctly

* removing problematic tests

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-19 10:13:04 +02:00
victor-meng
13434715b3 Properly check if a container is query copilot sample container (#1540) 2023-07-18 22:50:31 -07:00
v-darkora
676434cac5 [Query Copilot] Adding tests for the welcome modal (#1538) 2023-07-18 17:38:36 -07:00
Predrag Klepic
3d5580865a Query Results no longer blocked for clicking/copying content (#1537)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-17 11:15:01 -07:00
Predrag Klepic
7375cc717c [Query Copilot] Scrollable Copilot tab, improved history and minor fixes (#1536)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-17 11:10:41 -07:00
v-darkora
de3e56bb99 [Query Copilot] Add unit tests for Welcome Modal (#1535) 2023-07-14 15:18:38 -07:00
Predrag Klepic
53cd78452b [Query Copilot] Dropdown hide and buttons disabled in specific occasions (#1534)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-14 15:01:16 -07:00
v-darkora
fb6eb635c1 [Query Copilot] Handle response if it returns a 500 status (#1533) 2023-07-13 10:50:49 -07:00
v-darkora
fb6c0caca6 [Query Copilot] Update sample data resource tree item stylings (#1530)
* Update sample data resource tree item stylings

* Clean up sample data tree

---------

Co-authored-by: Victor Meng <vimeng@microsoft.com>
2023-07-13 09:17:29 +02:00
Predrag Klepic
e9f3c64239 [Query Copilot] Not disabling create database/container buttons for Sample Database (#1531)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-12 16:38:42 -07:00
MokireddySampath
bb6ee5deec Required attribute added for the input in delete dialog (#1524) 2023-07-12 22:30:23 +05:30
MokireddySampath
5796b28045 alt text added for the representative images in the splashscreen home page (#1525) 2023-07-12 22:30:04 +05:30
MokireddySampath
9635d14599 removing the connect value for ariacontrols variable (#1527) 2023-07-12 22:29:43 +05:30
MokireddySampath
c58d5765c2 aria label attribute updated with label name for the input (#1532) 2023-07-12 22:29:18 +05:30
victor-meng
708f4316b4 Fix "items" button in sample data resource tree does not open documents tab (#1528) 2023-07-11 16:35:54 -07:00
111 changed files with 6133 additions and 719 deletions

View File

@@ -144,4 +144,5 @@ src/Explorer/Notebook/NotebookRenderer/decorators/kbd-shortcuts/index.tsx
src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx
src/Explorer/Tree/ResourceTreeAdapter.tsx src/Explorer/Tree/ResourceTreeAdapter.tsx
__mocks__/monaco-editor.ts __mocks__/monaco-editor.ts
src/Explorer/Tree/ResourceTree.tsx src/Explorer/Tree/ResourceTree.tsx
src/Utils/PriorityBasedExecutionUtils.ts

View File

@@ -1,3 +1,3 @@
<svg width="10" height="14" viewBox="0 0 10 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 2.5C10 3.88071 7.76142 5 5 5C2.23858 5 0 3.88071 0 2.5C0 1.11929 2.23858 0 5 0C7.76142 0 10 1.11929 10 2.5ZM0 11.5V4.487C1.057 5.413 2.864 6 5 6C7.136 6 8.943 5.413 10 4.487V11.5C10 12.925 7.851 14 5 14C2.149 14 0 12.925 0 11.5Z" fill="#0078D4"/> <path d="M10 2.5C10 3.88071 7.76142 5 5 5C2.23858 5 0 3.88071 0 2.5C0 1.11929 2.23858 0 5 0C7.76142 0 10 1.11929 10 2.5ZM0 11.5V4.487C1.057 5.413 2.864 6 5 6C7.136 6 8.943 5.413 10 4.487V11.5C10 12.925 7.851 14 5 14C2.149 14 0 12.925 0 11.5Z" fill="#0078D4"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 363 B

After

Width:  |  Height:  |  Size: 363 B

View File

@@ -1,3 +1,3 @@
<svg width="13" height="17" viewBox="0 0 13 17" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.21207 0C3.73772 0 3.32085 0.314446 3.19054 0.770537L0.940937 8.64415C0.747029 9.32283 1.25663 9.99841 1.96246 9.99841H3.22974L2.06002 14.6773C1.79611 15.7329 3.10089 16.4551 3.85526 15.6726L12.5318 6.81506L12.5354 6.81137C13.1762 6.1436 12.7152 5 11.7688 5H9.20509L10.4665 1.40582L10.469 1.39836C10.6983 0.710426 10.1863 0 9.46114 0H4.21207Z" fill="#0078D4"/> <path d="M4.21207 0C3.73772 0 3.32085 0.314446 3.19054 0.770537L0.940937 8.64415C0.747029 9.32283 1.25663 9.99841 1.96246 9.99841H3.22974L2.06002 14.6773C1.79611 15.7329 3.10089 16.4551 3.85526 15.6726L12.5318 6.81506L12.5354 6.81137C13.1762 6.1436 12.7152 5 11.7688 5H9.20509L10.4665 1.40582L10.469 1.39836C10.6983 0.710426 10.1863 0 9.46114 0H4.21207Z" fill="#0078D4"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 475 B

After

Width:  |  Height:  |  Size: 475 B

View File

@@ -0,0 +1,34 @@
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.79997 1.36139L9.59997 1.54445V6.72957L6.71997 7.39864L5.75998 8.71588V10.7866C5.75998 12.178 6.51265 13.4605 7.72739 14.139L10.4421 15.6554L5.75997 18.1318L3.81352 18.4092L1.96738 17.378C0.75264 16.6995 -3.05176e-05 15.417 -3.05176e-05 14.0256V7.97332C-3.05176e-05 6.58155 0.753043 5.29871 1.9683 4.62034L7.3904 1.59367L7.38722 1.59591L7.79997 1.36139Z" fill="url(#paint0_radial_1157_162756)"/>
<path d="M7.79997 1.36139L9.59997 1.54445V6.72957L6.71997 7.39864L5.75998 8.71588V10.7866C5.75998 12.178 6.51265 13.4605 7.72739 14.139L10.4421 15.6554L5.75997 18.1318L3.81352 18.4092L1.96738 17.378C0.75264 16.6995 -3.05176e-05 15.417 -3.05176e-05 14.0256V7.97332C-3.05176e-05 6.58155 0.753043 5.29871 1.9683 4.62034L7.3904 1.59367L7.38722 1.59591L7.79997 1.36139Z" fill="url(#paint1_linear_1157_162756)"/>
<path d="M13.4397 8.11841L18.2397 10.9984L19.1997 11.9584V14.025C19.1997 15.4164 18.447 16.6989 17.2323 17.3774L11.4723 20.5948C10.3085 21.2448 8.89088 21.2448 7.72711 20.5948L1.96711 17.3774C1.85515 17.3149 1.74713 17.2472 1.64325 17.1748L1.96708 17.3557C3.13085 18.0057 4.54849 18.0057 5.71226 17.3557L11.4723 14.1383C12.687 13.4598 13.4397 12.1773 13.4397 10.7859V8.11841Z" fill="url(#paint2_radial_1157_162756)"/>
<path d="M13.4397 8.11841L18.2397 10.9984L19.1997 11.9584V14.025C19.1997 15.4164 18.447 16.6989 17.2323 17.3774L11.4723 20.5948C10.3085 21.2448 8.89088 21.2448 7.72711 20.5948L1.96711 17.3774C1.85515 17.3149 1.74713 17.2472 1.64325 17.1748L1.96708 17.3557C3.13085 18.0057 4.54849 18.0057 5.71226 17.3557L11.4723 14.1383C12.687 13.4598 13.4397 12.1773 13.4397 10.7859V8.11841Z" fill="url(#paint3_linear_1157_162756)"/>
<path d="M17.2316 4.62014L11.4716 1.40484C10.3083 0.755475 8.89151 0.755475 7.72821 1.40484L7.38921 1.59407C6.37448 2.30824 5.75989 3.47665 5.75989 4.73397V8.74548L7.72821 7.64674C8.89151 6.99738 10.3083 6.99738 11.4716 7.64674L17.2316 10.862C18.4192 11.525 19.1654 12.7652 19.1987 14.1202C19.1995 14.0886 19.1999 14.057 19.1999 14.0254V7.97311C19.1999 6.58134 18.4468 5.29851 17.2316 4.62014Z" fill="url(#paint4_radial_1157_162756)"/>
<path d="M17.2316 4.62014L11.4716 1.40484C10.3083 0.755475 8.89151 0.755475 7.72821 1.40484L7.38921 1.59407C6.37448 2.30824 5.75989 3.47665 5.75989 4.73397V8.74548L7.72821 7.64674C8.89151 6.99738 10.3083 6.99738 11.4716 7.64674L17.2316 10.862C18.4192 11.525 19.1654 12.7652 19.1987 14.1202C19.1995 14.0886 19.1999 14.057 19.1999 14.0254V7.97311C19.1999 6.58134 18.4468 5.29851 17.2316 4.62014Z" fill="url(#paint5_linear_1157_162756)"/>
<defs>
<radialGradient id="paint0_radial_1157_162756" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(8.1032 6.84805) rotate(112.544) scale(14.0711 11.584)">
<stop offset="0.206732" stop-color="#4995D0"/>
<stop offset="0.875628" stop-color="#0078D4"/>
</radialGradient>
<linearGradient id="paint1_linear_1157_162756" x1="6.63744" y1="16.8883" x2="5.87214" y2="15.464" gradientUnits="userSpaceOnUse">
<stop offset="0.9999" stop-color="#0078D4"/>
<stop offset="1" stop-color="#0078D4" stop-opacity="0"/>
</linearGradient>
<radialGradient id="paint2_radial_1157_162756" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(4.13968 15.6306) rotate(-3.9995) scale(14.7718 9.27561)">
<stop offset="0.140029" stop-color="#80C8FF"/>
<stop offset="0.952721" stop-color="#0078D4"/>
</radialGradient>
<linearGradient id="paint3_linear_1157_162756" x1="16.1362" y1="10.2483" x2="15.3298" y2="11.6531" gradientUnits="userSpaceOnUse">
<stop offset="0.9999" stop-color="#3F8AC3"/>
<stop offset="1" stop-color="#8C66BA" stop-opacity="0"/>
</linearGradient>
<radialGradient id="paint4_radial_1157_162756" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(15.9793 10.7505) rotate(-138.33) scale(13.6809 7.80282)">
<stop stop-color="#7BC6FF"/>
<stop offset="0.839255" stop-color="#0078D4"/>
</radialGradient>
<linearGradient id="paint5_linear_1157_162756" x1="5.75989" y1="6.14584" x2="7.32799" y2="6.14584" gradientUnits="userSpaceOnUse">
<stop offset="0.9999" stop-color="#0078D4"/>
<stop offset="1" stop-color="#436DCD" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,29 @@
<svg width="121" height="94" viewBox="0 0 121 94" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M3.25029 80.7474H97.1699C97.8805 80.7474 98.5648 80.471 99.078 79.9578C99.5913 79.4446 99.8676 78.7603 99.8676 78.0497V13.8575C99.8676 12.3704 98.3148 11.0018 96.2224 11.0018L4.53992 12.3968C4.18461 12.3968 3.84247 12.4626 3.51348 12.5942C3.18449 12.7389 2.89498 12.9231 2.64495 13.1863C2.39492 13.4364 2.19752 13.7259 2.05277 14.0549C1.90801 14.3838 1.85538 14.7392 1.85538 15.0813L0.552582 78.076C0.552582 78.7866 0.82893 79.4709 1.34215 79.971C1.85537 80.471 2.53967 80.7474 3.25029 80.7474Z" fill="#1F1D20"/>
<path d="M96.9332 9.80411H2.67139C1.19752 9.80411 0 11.0016 0 12.4755V76.7993C0 78.2732 1.19752 79.4707 2.67139 79.4707H96.9463C98.4202 79.4707 99.6177 78.2732 99.6177 76.7993V12.4755C99.6045 11.0016 98.407 9.80411 96.9332 9.80411Z" fill="url(#paint0_linear_1157_163007)"/>
<path d="M95.6963 13.7121H50.1379V74.904H95.6963V13.7121Z" fill="white"/>
<path d="M50.7829 13.7121H5.22461V74.904H50.7829V13.7121Z" fill="#CDCDD0"/>
<path d="M14.3309 0V60.6918C14.3309 60.6918 26.4903 59.4811 36.3468 64.4291C46.2165 69.3771 50.7829 74.8514 50.7829 74.8514V14.1465C50.7829 14.1465 45.0979 6.90876 36.3468 4.23737C29.2275 2.05289 14.3309 0 14.3309 0Z" fill="#EAEAEA"/>
<path d="M59.1387 42.9135L56.2699 41.6633L53.388 42.9135V9.80411H59.1387V42.9135Z" fill="url(#paint1_linear_1157_163007)"/>
<path opacity="0.15" d="M86.7212 25.6883H66.7713C65.9028 25.6883 65.2053 26.3989 65.2053 27.2543C65.2053 28.1228 65.9159 28.8202 66.7713 28.8202H86.7212C87.5897 28.8202 88.2872 28.1096 88.2872 27.2543C88.3003 26.3857 87.5897 25.6883 86.7212 25.6883Z" fill="#1F1D20"/>
<path opacity="0.15" d="M86.7212 32.5045H66.7713C65.9028 32.5045 65.2053 33.2151 65.2053 34.0705C65.2053 34.939 65.9159 35.6365 66.7713 35.6365H86.7212C87.5897 35.6365 88.2872 34.9258 88.2872 34.0705C88.3003 33.2019 87.5897 32.5045 86.7212 32.5045Z" fill="#1F1D20"/>
<path opacity="0.15" fill-rule="evenodd" clip-rule="evenodd" d="M66.7715 39.3336H86.695C87.5636 39.3336 88.261 40.0442 88.261 40.8995C88.261 41.7681 87.5504 42.4655 86.695 42.4655H66.7715C65.903 42.4655 65.2055 41.7549 65.2055 40.8995C65.1923 40.0442 65.8898 39.3336 66.7715 39.3336Z" fill="#1F1D20"/>
<path d="M86.7212 25.1614H66.7713C65.9028 25.1614 65.2053 25.872 65.2053 26.7273C65.2053 27.5959 65.9159 28.2933 66.7713 28.2933H86.7212C87.5897 28.2933 88.2872 27.5827 88.2872 26.7273C88.3003 25.8588 87.5897 25.1614 86.7212 25.1614Z" fill="#50E6FF"/>
<path d="M86.7212 31.9917H66.7713C65.9028 31.9917 65.2053 32.7023 65.2053 33.5577C65.2053 34.4262 65.9159 35.1237 66.7713 35.1237H86.7212C87.5897 35.1237 88.2872 34.4131 88.2872 33.5577C88.3003 32.6892 87.5897 31.9917 86.7212 31.9917Z" fill="#32B0E7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M66.7715 38.808H86.695C87.5636 38.808 88.261 39.5186 88.261 40.3739C88.261 41.2425 87.5504 41.9399 86.695 41.9399H66.7715C65.903 41.9531 65.2055 41.2293 65.2055 40.3739C65.1923 39.5186 65.8898 38.808 66.7715 38.808Z" fill="#185A97"/>
<path opacity="0.2" d="M120.295 60.2875H80.476V85.9653C80.476 87.0514 81.3515 87.9269 82.4375 87.9269H86.6156V94L92.733 87.9269H118.333C119.419 87.9269 120.295 87.0514 120.295 85.9653V60.2875Z" fill="#1F1D21"/>
<path d="M118.965 58.6804H81.8058C81.0744 58.6804 80.476 59.2788 80.476 60.0103V84.9898C80.476 85.7213 81.0744 86.3197 81.8058 86.3197H86.6156V92.3928L92.733 86.3197H118.965C119.696 86.3197 120.295 85.7213 120.295 84.9898V60.0103C120.295 59.2788 119.696 58.6804 118.965 58.6804Z" fill="#49C8EF"/>
<path d="M106.564 67.8017H94.1846C93.697 67.8017 93.298 68.2006 93.298 68.6883C93.298 69.1759 93.697 69.5748 94.1846 69.5748H106.564C107.051 69.5748 107.45 69.1759 107.45 68.6883C107.45 68.2006 107.051 67.8017 106.564 67.8017Z" fill="#C3F1FF"/>
<path d="M106.564 71.6913H94.1846C93.697 71.6913 93.298 72.0903 93.298 72.5779C93.298 73.0655 93.697 73.4645 94.1846 73.4645H106.564C107.051 73.4645 107.45 73.0655 107.45 72.5779C107.45 72.1014 107.051 71.6913 106.564 71.6913Z" fill="#C3F1FF"/>
<path d="M106.564 75.6026H94.1846C93.697 75.6026 93.298 76.0016 93.298 76.4892C93.298 76.9768 93.697 77.3758 94.1846 77.3758H106.564C107.051 77.3758 107.45 76.9768 107.45 76.4892C107.45 75.9905 107.051 75.6026 106.564 75.6026Z" fill="#C3F1FF"/>
<defs>
<linearGradient id="paint0_linear_1157_163007" x1="49.8133" y1="14.2868" x2="49.8133" y2="85.0398" gradientUnits="userSpaceOnUse">
<stop stop-color="#007ED8"/>
<stop offset="0.7065" stop-color="#002D4C"/>
</linearGradient>
<linearGradient id="paint1_linear_1157_163007" x1="36.5785" y1="-9.69372" x2="104.813" y2="115.29" gradientUnits="userSpaceOnUse">
<stop stop-color="#007ED8"/>
<stop offset="0.7065" stop-color="#002D4C"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -0,0 +1,3 @@
<svg width="20" height="22" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5849 1.28273C11.0125 0.262814 8.98746 0.262813 7.41509 1.28273L2.16509 4.68813C0.8149 5.56393 0 7.06384 0 8.6732V13.3268C0 14.9361 0.814898 16.4361 2.16509 17.3119L7.41509 20.7173C8.98746 21.7372 11.0125 21.7372 12.5849 20.7173L17.8349 17.3119C19.1851 16.4361 20 14.9361 20 13.3268V8.6732C20 7.06384 19.1851 5.56393 17.8349 4.68813L12.5849 1.28273ZM9.50097 2.05627C10.2759 1.93601 11.0848 2.09764 11.7686 2.54117L17.0186 5.94657C17.9424 6.54581 18.5 7.57205 18.5 8.6732V11.3955C18.287 11.1926 18.0465 11.0117 17.7802 10.8585L11.8647 7.4571C10.7104 6.79343 9.29086 6.79152 8.13486 7.45209L7.5 7.81487V4.41942C7.5 3.54181 8.01028 2.74428 8.80712 2.37651L9.50097 2.05627ZM17.8644 15.2257L17.7932 15.3503C17.5776 15.6214 17.3172 15.8597 17.0186 16.0534L11.7686 19.4588C10.6928 20.1567 9.30721 20.1567 8.23138 19.4588L5.86807 17.9259C6.11557 17.8361 6.35595 17.7193 6.58487 17.5754L12.2457 14.0172C13.3374 13.3309 14 12.1318 14 10.8423V10.4152L17.0324 12.1589C18.1077 12.7771 18.4798 14.1489 17.8644 15.2257ZM12.5 9.55272V10.8423C12.5 11.616 12.1025 12.3354 11.4474 12.7472L10.0303 13.6379L8.57078 12.7398C7.90535 12.3303 7.5 11.6049 7.5 10.8235V9.54249L8.87907 8.75445C9.57267 8.35811 10.4244 8.35926 11.1169 8.75746L12.5 9.55272ZM8.61445 14.5279L5.78662 16.3054C5.02045 16.787 4.04031 16.7627 3.29894 16.2437L2.5521 15.721C1.88767 15.1111 1.5 14.245 1.5 13.3268V8.6732C1.5 7.57205 2.05756 6.5458 2.98138 5.94657L6.0268 3.97116C6.00907 4.11873 6 4.26836 6 4.41942V10.8235C6 12.1258 6.67558 13.3348 7.78463 14.0173L8.61445 14.5279Z" fill="#0078D4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,3 @@
<svg width="14" height="13" viewBox="0 0 14 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 0C1.61929 0 0.5 1.11929 0.5 2.5V9.5C0.5 10.8807 1.61929 12 3 12H4.75716C4.50353 11.6929 4.28261 11.3578 4.09971 11H3C2.17157 11 1.5 10.3284 1.5 9.5V2.5C1.5 1.67157 2.17157 1 3 1H10C10.8284 1 11.5 1.67157 11.5 2.5V3.59971C11.8578 3.78261 12.1929 4.00353 12.5 4.25716V2.5C12.5 1.11929 11.3807 0 10 0H3ZM9 13C11.4853 13 13.5 10.9853 13.5 8.5C13.5 6.01472 11.4853 4 9 4C6.51472 4 4.5 6.01472 4.5 8.5C4.5 10.9853 6.51472 13 9 13ZM10.8536 6.64645C11.0488 6.84171 11.0488 7.15829 10.8536 7.35355L9.70711 8.5L10.8536 9.64645C11.0488 9.84171 11.0488 10.1583 10.8536 10.3536C10.6583 10.5488 10.3417 10.5488 10.1464 10.3536L9 9.20711L7.85355 10.3536C7.65829 10.5488 7.34171 10.5488 7.14645 10.3536C6.95118 10.1583 6.95118 9.84171 7.14645 9.64645L8.29289 8.5L7.14645 7.35355C6.95118 7.15829 6.95118 6.84171 7.14645 6.64645C7.34171 6.45118 7.65829 6.45118 7.85355 6.64645L9 7.79289L10.1464 6.64645C10.3417 6.45118 10.6583 6.45118 10.8536 6.64645Z" fill="#0078D4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -108,7 +108,7 @@ table.storage thead td {
background-color: @BaseLight; background-color: @BaseLight;
/*[{datatable-header-background}]*/ /*[{datatable-header-background}]*/
text-align: left; text-align: left;
color: #808080; color: #333333;
/*[{datatable-header-text}]*/ /*[{datatable-header-text}]*/
outline: none; outline: none;
} }

View File

@@ -230,7 +230,7 @@ input::-webkit-inner-spin-button {
.advanced-options-panel .advanced-options .select .select-options-link { .advanced-options-panel .advanced-options .select .select-options-link {
margin-left: 4px; margin-left: 4px;
cursor: pointer; cursor: pointer;
outline: none; padding: 2px;
} }
.query-panel .row .column-headers .Field { .query-panel .row .column-headers .Field {

View File

@@ -28,4 +28,4 @@
.clickDisabled { .clickDisabled {
pointer-events: none; pointer-events: none;
} }
} }

20
package-lock.json generated
View File

@@ -164,6 +164,7 @@
"jest": "26.6.3", "jest": "26.6.3",
"jest-canvas-mock": "2.3.1", "jest-canvas-mock": "2.3.1",
"jest-playwright-preset": "1.5.1", "jest-playwright-preset": "1.5.1",
"jest-react-hooks-shallow": "1.5.1",
"jest-trx-results-processor": "0.0.7", "jest-trx-results-processor": "0.0.7",
"less": "3.8.1", "less": "3.8.1",
"less-loader": "4.1.0", "less-loader": "4.1.0",
@@ -19578,6 +19579,16 @@
"node": ">=8.9.0" "node": ">=8.9.0"
} }
}, },
"node_modules/jest-react-hooks-shallow": {
"version": "1.5.1",
"resolved": "https://msazure.pkgs.visualstudio.com/_packaging/AzurePortal/npm/registry/jest-react-hooks-shallow/-/jest-react-hooks-shallow-1.5.1.tgz",
"integrity": "sha1-1SI4EGG7nf9WcfhLBT0LPxy+N/k=",
"dev": true,
"license": "ISC",
"dependencies": {
"react": "^16.8.0"
}
},
"node_modules/jest-regex-util": { "node_modules/jest-regex-util": {
"version": "24.9.0", "version": "24.9.0",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz",
@@ -48608,6 +48619,15 @@
} }
} }
}, },
"jest-react-hooks-shallow": {
"version": "1.5.1",
"resolved": "https://msazure.pkgs.visualstudio.com/_packaging/AzurePortal/npm/registry/jest-react-hooks-shallow/-/jest-react-hooks-shallow-1.5.1.tgz",
"integrity": "sha1-1SI4EGG7nf9WcfhLBT0LPxy+N/k=",
"dev": true,
"requires": {
"react": "^16.8.0"
}
},
"jest-regex-util": { "jest-regex-util": {
"version": "24.9.0", "version": "24.9.0",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz",

View File

@@ -160,6 +160,7 @@
"jest": "26.6.3", "jest": "26.6.3",
"jest-canvas-mock": "2.3.1", "jest-canvas-mock": "2.3.1",
"jest-playwright-preset": "1.5.1", "jest-playwright-preset": "1.5.1",
"jest-react-hooks-shallow": "1.5.1",
"jest-trx-results-processor": "0.0.7", "jest-trx-results-processor": "0.0.7",
"less": "3.8.1", "less": "3.8.1",
"less-loader": "4.1.0", "less-loader": "4.1.0",
@@ -232,4 +233,4 @@
"prettier": { "prettier": {
"printWidth": 120 "printWidth": 120
} }
} }

View File

@@ -358,6 +358,7 @@ export enum ContainerStatusType {
export enum PoolIdType { export enum PoolIdType {
DefaultPoolId = "default", DefaultPoolId = "default",
QueryCopilot = "query-copilot",
} }
export const EmulatorMasterKey = export const EmulatorMasterKey =
@@ -429,30 +430,101 @@ export class JunoEndpoints {
public static readonly Stage = "https://tools-staging.cosmos.azure.com"; public static readonly Stage = "https://tools-staging.cosmos.azure.com";
} }
export class PriorityLevel {
public static readonly High = "high";
public static readonly Low = "low";
public static readonly Default = "low";
}
export const QueryCopilotSampleDatabaseId = "CopilotSampleDb"; export const QueryCopilotSampleDatabaseId = "CopilotSampleDb";
export const QueryCopilotSampleContainerId = "SampleContainer"; export const QueryCopilotSampleContainerId = "SampleContainer";
export const QueryCopilotSampleContainerSchema = { export const QueryCopilotSampleContainerSchema = {
product: { product: {
sampleData: { sampleData: {
id: "de6fadec-0384-43c8-93ea-16c0170b5460", id: "c415e70f-9bf5-4cda-aebe-a290cb8b94c2",
name: "Premium Phone Mini (Red)", name: "Amazing Phone 3000 (Black)",
price: 652.04, price: 223.33,
category: "Electronics", category: "Electronics",
description: description:
"This Premium Phone Mini (Red) is designed by the company under agreement with the FCC, so we'd give it a pass in either direction, but no one should be using this handset without a compatible handset. All in all, this phone is one of the most affordable Android handsets out there at $100. Check them out.\n\n9. HTC One M9 4", "This Amazing Phone 3000 (Black) is made of black metal! It has a very well made aluminum body and it feels very comfortable. We loved the sound that comes out of it! Also, the design of the phone was a little loose at first because I was using the camera and felt uncomfortable wearing it. The phone is actually made slightly smaller than these photos! This is due to the addition of a 3.3mm filter",
stock: 74, stock: 84,
countryOfOrigin: "Mexico", countryOfOrigin: "USA",
firstAvailable: "2018-07-07 17:42:28", firstAvailable: "2018-09-07 19:41:44",
priceHistory: [592.81], priceHistory: [238.68, 234.7, 221.49, 205.88, 220.15],
customerRatings: [ customerRatings: [
{ username: "dannyhowell", stars: 1, date: "2022-03-12 17:01:23", verifiedUser: true }, {
{ username: "lindsay26", stars: 1, date: "2022-12-29 07:18:20", verifiedUser: false }, username: "steven66",
{ username: "smithmiguel", stars: 3, date: "2022-09-08 11:43:27", verifiedUser: false }, firstName: "Carol",
{ username: "julie07", stars: 3, date: "2021-03-14 23:54:10", verifiedUser: true }, gender: "female",
{ username: "kelly93", stars: 3, date: "2022-11-05 12:45:43", verifiedUser: false }, lastName: "Shelton",
{ username: "katherinereynolds", stars: 2, date: "2022-09-14 11:49:36", verifiedUser: false }, age: "25-35",
{ username: "chandlerkenneth", stars: 1, date: "2022-01-14 12:14:43", verifiedUser: true }, area: "suburban",
address: "261 Collins Burgs Apt. 332\nNorth Taylor, NM 32268",
stars: 5,
date: "2021-04-22 13:42:14",
verifiedUser: true,
},
{
username: "khudson",
firstName: "Ronald",
gender: "male",
lastName: "Webb",
age: "18-24",
area: "suburban",
address: "9912 Parker Court Apt. 068\nNorth Austin, HI 76225",
stars: 5,
date: "2021-02-07 07:00:22",
verifiedUser: false,
},
{
username: "lfrancis",
firstName: "Brady",
gender: "male",
lastName: "Wright",
age: "35-45",
area: "urban",
address: "PSC 5437, Box 3159\nAPO AA 26385",
stars: 2,
date: "2022-02-23 21:40:10",
verifiedUser: false,
},
{
username: "nicolemartinez",
firstName: "Megan",
gender: "female",
lastName: "Tran",
age: "18-24",
area: "rural",
address: "7445 Salazar Brooks\nNew Sarah, PW 18097",
stars: 4,
date: "2021-09-01 22:21:40",
verifiedUser: false,
},
{
username: "uguzman",
firstName: "Deanna",
gender: "female",
lastName: "Campbell",
age: "18-24",
area: "urban",
address: "41104 Moreno Fort Suite 872\nPort Michaelbury, AK 48712",
stars: 1,
date: "2022-03-07 02:23:14",
verifiedUser: false,
},
{
username: "rebeccahunt",
firstName: "Jared",
gender: "male",
lastName: "Lopez",
age: "18-24",
area: "rural",
address: "392 Morgan Village Apt. 785\nGreenshire, CT 05921",
stars: 5,
date: "2021-04-17 04:17:49",
verifiedUser: false,
},
], ],
rareProperty: true, rareProperty: true,
}, },
@@ -494,6 +566,24 @@ export const QueryCopilotSampleContainerSchema = {
username: { username: {
type: "string", type: "string",
}, },
firstName: {
type: "string",
},
gender: {
type: "string",
},
lastName: {
type: "string",
},
age: {
type: "string",
},
area: {
type: "string",
},
address: {
type: "string",
},
stars: { stars: {
type: "number", type: "number",
}, },
@@ -516,3 +606,69 @@ export const QueryCopilotSampleContainerSchema = {
}, },
}, },
}; };
export const ShortenedQueryCopilotSampleContainerSchema = {
containerSchema: {
product: {
sampleData: {
categoryName: "Components, Saddles",
name: "LL Road Seat/Saddle",
price: 27.12,
tags: [
{
id: "0573D684-9140-4DEE-89AF-4E4A90E65666",
name: "Tag-113",
},
{
id: "6C2F05C8-1E61-4912-BE1A-C67A378429BB",
name: "Tag-5",
},
],
},
schema: {
properties: {
categoryName: {
type: "string",
},
name: {
type: "string",
},
price: {
type: "number",
},
tags: {
items: {
properties: {
id: {
type: "string",
},
name: {
type: "string",
},
},
type: "object",
},
type: "array",
},
},
type: "object",
},
},
},
userPrompt: "find all products",
};

View File

@@ -4,6 +4,9 @@ import { userContext } from "../UserContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { EmulatorMasterKey, HttpHeaders } from "./Constants"; import { EmulatorMasterKey, HttpHeaders } from "./Constants";
import { getErrorMessage } from "./ErrorHandlingUtils"; import { getErrorMessage } from "./ErrorHandlingUtils";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { PriorityLevel } from "../Common/Constants";
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
const _global = typeof self === "undefined" ? window : self; const _global = typeof self === "undefined" ? window : self;
@@ -105,6 +108,13 @@ export function client(): Cosmos.CosmosClient {
if (configContext.PROXY_PATH !== undefined) { if (configContext.PROXY_PATH !== undefined) {
(options as any).plugins = [{ on: "request", plugin: requestPlugin }]; (options as any).plugins = [{ on: "request", plugin: requestPlugin }];
} }
if (userContext.features.enablePriorityBasedThrottling && userContext.apiType === "SQL") {
const plugins = (options as any).plugins || [];
plugins.push({ on: "request", plugin: PriorityBasedExecutionUtils.requestPlugin });
(options as any).plugins = plugins;
}
_client = new Cosmos.CosmosClient(options); _client = new Cosmos.CosmosClient(options);
return _client; return _client;
} }

View File

@@ -39,7 +39,6 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
/> />
<TextField <TextField
label={entityValueLabel && entityValueLabel} label={entityValueLabel && entityValueLabel}
id="entityTimeId"
autoFocus autoFocus
type="time" type="time"
value={entityTimeValue} value={entityTimeValue}
@@ -54,7 +53,6 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
<TextField <TextField
label={entityValueLabel && entityValueLabel} label={entityValueLabel && entityValueLabel}
className="addEntityTextField" className="addEntityTextField"
id="entityValueId"
disabled={isEntityValueDisable} disabled={isEntityValueDisable}
type={entityValueType} type={entityValueType}
placeholder={entityValuePlaceholder} placeholder={entityValuePlaceholder}

View File

@@ -106,7 +106,6 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
<Stack horizontal tokens={sectionStackTokens}> <Stack horizontal tokens={sectionStackTokens}>
<TextField <TextField
label={entityPropertyLabel && entityPropertyLabel} label={entityPropertyLabel && entityPropertyLabel}
id="entityPropertyId"
autoFocus autoFocus
disabled={isPropertyTypeDisable} disabled={isPropertyTypeDisable}
placeholder={entityPropertyPlaceHolder} placeholder={entityPropertyPlaceHolder}
@@ -120,7 +119,6 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
onChange={onEntityTypeChange} onChange={onEntityTypeChange}
options={options} options={options}
disabled={isPropertyTypeDisable} disabled={isPropertyTypeDisable}
id="entityTypeId"
styles={dropdownStyles} styles={dropdownStyles}
/> />
<EntityValue <EntityValue
@@ -137,7 +135,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
/> />
{!isEntityValueDisable && ( {!isEntityValueDisable && (
<TooltipHost content="Edit property" id="editTooltip"> <TooltipHost content="Edit property" id="editTooltip">
<div tabIndex={0}> <div>
<Image <Image
{...imageProps} {...imageProps}
src={EditIcon} src={EditIcon}

View File

@@ -1,14 +1,14 @@
import { import {
allowedAadEndpoints, allowedAadEndpoints,
allowedArcadiaEndpoints, allowedArcadiaEndpoints,
allowedArmEndpoints,
allowedBackendEndpoints,
allowedEmulatorEndpoints, allowedEmulatorEndpoints,
allowedGraphEndpoints, allowedGraphEndpoints,
allowedHostedExplorerEndpoints, allowedHostedExplorerEndpoints,
allowedJunoOrigins, allowedJunoOrigins,
allowedMongoBackendEndpoints, allowedMongoBackendEndpoints,
allowedMsalRedirectEndpoints, allowedMsalRedirectEndpoints,
defaultAllowedArmEndpoints,
defaultAllowedBackendEndpoints,
validateEndpoint, validateEndpoint,
} from "Utils/EndpointValidation"; } from "Utils/EndpointValidation";
@@ -20,6 +20,8 @@ export enum Platform {
export interface ConfigContext { export interface ConfigContext {
platform: Platform; platform: Platform;
allowedArmEndpoints: ReadonlyArray<string>;
allowedBackendEndpoints: ReadonlyArray<string>;
allowedParentFrameOrigins: ReadonlyArray<string>; allowedParentFrameOrigins: ReadonlyArray<string>;
gitSha?: string; gitSha?: string;
proxyPath?: string; proxyPath?: string;
@@ -49,6 +51,8 @@ export interface ConfigContext {
// Default configuration // Default configuration
let configContext: Readonly<ConfigContext> = { let configContext: Readonly<ConfigContext> = {
platform: Platform.Portal, platform: Platform.Portal,
allowedArmEndpoints: defaultAllowedArmEndpoints,
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
allowedParentFrameOrigins: [ allowedParentFrameOrigins: [
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`, `^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`, `^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
@@ -77,7 +81,7 @@ let configContext: Readonly<ConfigContext> = {
export function resetConfigContext(): void { export function resetConfigContext(): void {
if (process.env.NODE_ENV !== "test") { if (process.env.NODE_ENV !== "test") {
throw new Error("resetConfigContext can only becalled in a test environment"); throw new Error("resetConfigContext can only be called in a test environment");
} }
configContext = {} as ConfigContext; configContext = {} as ConfigContext;
} }
@@ -87,7 +91,7 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
return; return;
} }
if (!validateEndpoint(newContext.ARM_ENDPOINT, allowedArmEndpoints)) { if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
delete newContext.ARM_ENDPOINT; delete newContext.ARM_ENDPOINT;
} }
@@ -107,7 +111,12 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
delete newContext.ARCADIA_ENDPOINT; delete newContext.ARCADIA_ENDPOINT;
} }
if (!validateEndpoint(newContext.BACKEND_ENDPOINT, allowedBackendEndpoints)) { if (
!validateEndpoint(
newContext.BACKEND_ENDPOINT,
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints
)
) {
delete newContext.BACKEND_ENDPOINT; delete newContext.BACKEND_ENDPOINT;
} }
@@ -130,7 +139,7 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
Object.assign(configContext, newContext); Object.assign(configContext, newContext);
} }
// Injected for local develpment. These will be removed in the production bundle by webpack // Injected for local development. These will be removed in the production bundle by webpack
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
const port: string = process.env.PORT || "1234"; const port: string = process.env.PORT || "1234";
updateConfigContext({ updateConfigContext({

View File

@@ -1,3 +1,6 @@
import { useDatabases } from "Explorer/useDatabases";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
import React from "react"; import React from "react";
import AddCollectionIcon from "../../images/AddCollection.svg"; import AddCollectionIcon from "../../images/AddCollection.svg";
@@ -144,11 +147,24 @@ export const createCollectionContextMenuButton = (
export const createSampleCollectionContextMenuButton = (): TreeNodeMenuItem[] => { export const createSampleCollectionContextMenuButton = (): TreeNodeMenuItem[] => {
const items: TreeNodeMenuItem[] = []; const items: TreeNodeMenuItem[] = [];
if (userContext.apiType === "SQL") { if (userContext.apiType === "SQL") {
items.push({ const copilotVersion = userContext.features.copilotVersion;
iconSrc: AddSqlQueryIcon, if (copilotVersion === "v1.0") {
onClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot), items.push({
label: "New SQL Query", iconSrc: AddSqlQueryIcon,
}); onClick: () => {
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
},
label: "New SQL Query",
});
} else if (copilotVersion === "v2.0") {
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => sampleCollection && sampleCollection.onNewQueryClick(sampleCollection, undefined),
label: "New SQL Query",
});
}
} }
return items; return items;

View File

@@ -25,6 +25,7 @@ export class AccordionComponent extends React.Component<AccordionComponentProps>
export interface AccordionItemComponentProps { export interface AccordionItemComponentProps {
title: string; title: string;
isExpanded?: boolean; isExpanded?: boolean;
containerStyles?: React.CSSProperties;
styles?: React.CSSProperties; styles?: React.CSSProperties;
} }
@@ -54,9 +55,9 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
} }
public render(): JSX.Element { public render(): JSX.Element {
const { styles } = this.props; const { containerStyles, styles } = this.props;
return ( return (
<div className="accordionItemContainer"> <div className="accordionItemContainer" style={{ ...containerStyles }}>
<div className="accordionItemHeader" onClick={this.onHeaderClick} onKeyPress={this.onHeaderKeyPress}> <div className="accordionItemHeader" onClick={this.onHeaderClick} onKeyPress={this.onHeaderKeyPress}>
{this.renderCollapseExpandIcon()} {this.renderCollapseExpandIcon()}
{this.props.title} {this.props.title}

View File

@@ -49,7 +49,6 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
onClick={this.toggleCollapsed} onClick={this.toggleCollapsed}
onKeyPress={this.onKeyPress} onKeyPress={this.onKeyPress}
tabIndex={0} tabIndex={0}
aria-name="Advanced"
role="button" role="button"
aria-expanded={this.state.isExpanded} aria-expanded={this.state.isExpanded}
> >

View File

@@ -4,7 +4,6 @@ exports[`CollapsibleSectionComponent renders 1`] = `
<Fragment> <Fragment>
<Stack <Stack
aria-expanded={true} aria-expanded={true}
aria-name="Advanced"
className="collapsibleSection" className="collapsibleSection"
horizontal={true} horizontal={true}
onClick={[Function]} onClick={[Function]}

View File

@@ -31,6 +31,13 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
public componentDidMount(): void { public componentDidMount(): void {
this.createEditor(this.configureEditor.bind(this)); this.createEditor(this.configureEditor.bind(this));
setTimeout(() => {
const suggestionWidget = this.editor?.getDomNode()?.querySelector(".suggest-widget") as HTMLElement;
if (suggestionWidget) {
suggestionWidget.style.display = "none";
}
}, 100);
} }
public componentDidUpdate(previous: EditorReactProps) { public componentDidUpdate(previous: EditorReactProps) {

View File

@@ -249,6 +249,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)} onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
step={AutoPilotUtils.autoPilotIncrementStep} step={AutoPilotUtils.autoPilotIncrementStep}
min={AutoPilotUtils.autoPilotThroughput1K} min={AutoPilotUtils.autoPilotThroughput1K}
max={isSharded ? Number.MAX_SAFE_INTEGER.toString() : "10000"}
value={throughput.toString()} value={throughput.toString()}
ariaLabel={`${isDatabase ? "Database" : getCollectionName()} max RU/s`} ariaLabel={`${isDatabase ? "Database" : getCollectionName()} max RU/s`}
required={true} required={true}

View File

@@ -1644,6 +1644,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
errorMessage="" errorMessage=""
id="autoscaleRUValueField" id="autoscaleRUValueField"
key=".0:$.2" key=".0:$.2"
max="9007199254740991"
min={1000} min={1000}
onChange={[Function]} onChange={[Function]}
required={true} required={true}
@@ -1667,6 +1668,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
deferredValidationTime={200} deferredValidationTime={200}
errorMessage="" errorMessage=""
id="autoscaleRUValueField" id="autoscaleRUValueField"
max="9007199254740991"
min={1000} min={1000}
onChange={[Function]} onChange={[Function]}
required={true} required={true}
@@ -1964,6 +1966,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
aria-label="Container max RU/s" aria-label="Container max RU/s"
className="ms-TextField-field field-64" className="ms-TextField-field field-64"
id="autoscaleRUValueField" id="autoscaleRUValueField"
max="9007199254740991"
min={1000} min={1000}
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}

View File

@@ -1,5 +1,8 @@
import { Link } from "@fluentui/react/lib/Link"; import { Link } from "@fluentui/react/lib/Link";
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { sendMessage } from "Common/MessageHandler";
import { Platform } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { IGalleryItem } from "Juno/JunoClient"; import { IGalleryItem } from "Juno/JunoClient";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation"; import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
import * as ko from "knockout"; import * as ko from "knockout";
@@ -23,7 +26,7 @@ import { PhoenixClient } from "../Phoenix/PhoenixClient";
import * as ExplorerSettings from "../Shared/ExplorerSettings"; import * as ExplorerSettings from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../UserContext"; import { isAccountNewerThanThresholdInMs, userContext } from "../UserContext";
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils"; import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
import { stringToBlob } from "../Utils/BlobUtils"; import { stringToBlob } from "../Utils/BlobUtils";
import { isCapabilityEnabled } from "../Utils/CapabilityUtils"; import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
@@ -258,6 +261,45 @@ export default class Explorer {
// TODO: return result // TODO: return result
} }
private getRandomInt(max: number) {
return Math.floor(Math.random() * max);
}
public openNPSSurveyDialog(): void {
if (!Platform.Portal) {
return;
}
const NINETY_DAYS_IN_MS = 7776000000;
const ONE_DAY_IN_MS = 86400000;
const isAccountNewerThanNinetyDays = isAccountNewerThanThresholdInMs(
userContext.databaseAccount?.systemData?.createdAt || "",
NINETY_DAYS_IN_MS
);
// Try Cosmos DB subscription - survey shown to random 25% of users at day 1 in Data Explorer.
if (userContext.isTryCosmosDBSubscription) {
if (
isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS) &&
this.getRandomInt(100) < 25
) {
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
}
} else {
// An existing account is lesser than 90 days old. For existing account show to random 10 % of users in Data Explorer.
if (isAccountNewerThanNinetyDays) {
if (this.getRandomInt(100) < 10) {
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
}
} else {
// An existing account is greater than 90 days. For existing account show to random 25 % of users in Data Explorer.
if (this.getRandomInt(100) < 25) {
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
}
}
}
}
public async refreshDatabaseForResourceToken(): Promise<void> { public async refreshDatabaseForResourceToken(): Promise<void> {
const databaseId = userContext.parsedResourceToken?.databaseId; const databaseId = userContext.parsedResourceToken?.databaseId;
const collectionId = userContext.parsedResourceToken?.collectionId; const collectionId = userContext.parsedResourceToken?.collectionId;
@@ -343,7 +385,7 @@ export default class Explorer {
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
} }
public async allocateContainer(): Promise<void> { public async allocateContainer(poolId: PoolIdType): Promise<void> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo; const notebookServerInfo = useNotebook.getState().notebookServerInfo;
const isAllocating = useNotebook.getState().isAllocating; const isAllocating = useNotebook.getState().isAllocating;
if ( if (
@@ -353,7 +395,7 @@ export default class Explorer {
) { ) {
const provisionData: IProvisionData = { const provisionData: IProvisionData = {
cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint, cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint,
poolId: PoolIdType.DefaultPoolId, poolId: poolId === PoolIdType.DefaultPoolId ? undefined : poolId,
}; };
const connectionStatus: ContainerConnectionInfo = { const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Connecting, status: ConnectionStatusType.Connecting,
@@ -608,6 +650,8 @@ export default class Explorer {
private _initSettings() { private _initSettings() {
if (!ExplorerSettings.hasSettingsDefined()) { if (!ExplorerSettings.hasSettingsDefined()) {
ExplorerSettings.createDefaultSettings(); ExplorerSettings.createDefaultSettings();
} else {
ExplorerSettings.ensurePriorityLevel();
} }
} }
@@ -711,7 +755,7 @@ export default class Explorer {
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`); throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
} }
if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) { if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) {
await this.allocateContainer(); await this.allocateContainer(PoolIdType.DefaultPoolId);
} }
const notebookTabs = useTabs const notebookTabs = useTabs
@@ -936,7 +980,7 @@ export default class Explorer {
} }
if (useNotebook.getState().isPhoenixNotebooks) { if (useNotebook.getState().isPhoenixNotebooks) {
if (isGithubTree) { if (isGithubTree) {
await this.allocateContainer(); await this.allocateContainer(PoolIdType.DefaultPoolId);
parent = parent || this.resourceTree.myNotebooksContentRoot; parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree); this.createNewNoteBook(parent, isGithubTree);
} else { } else {
@@ -945,7 +989,7 @@ export default class Explorer {
undefined, undefined,
"Create", "Create",
async () => { async () => {
await this.allocateContainer(); await this.allocateContainer(PoolIdType.DefaultPoolId);
parent = parent || this.resourceTree.myNotebooksContentRoot; parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree); this.createNewNoteBook(parent, isGithubTree);
}, },
@@ -1024,7 +1068,7 @@ export default class Explorer {
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> { public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
if (useNotebook.getState().isPhoenixFeatures) { if (useNotebook.getState().isPhoenixFeatures) {
await this.allocateContainer(); await this.allocateContainer(PoolIdType.DefaultPoolId);
const notebookServerInfo = useNotebook.getState().notebookServerInfo; const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) { if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
this.connectToNotebookTerminal(kind); this.connectToNotebookTerminal(kind);
@@ -1168,7 +1212,7 @@ export default class Explorer {
await useNotebook.getState().getPhoenixStatus(); await useNotebook.getState().getPhoenixStatus();
} }
if (useNotebook.getState().isPhoenixNotebooks) { if (useNotebook.getState().isPhoenixNotebooks) {
await this.allocateContainer(); await this.allocateContainer(PoolIdType.DefaultPoolId);
} }
// We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb // We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb
@@ -1205,7 +1249,7 @@ export default class Explorer {
undefined, undefined,
"Upload", "Upload",
async () => { async () => {
await this.allocateContainer(); await this.allocateContainer(PoolIdType.DefaultPoolId);
parent = parent || this.resourceTree.myNotebooksContentRoot; parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePanel(parent); this.uploadFilePanel(parent);
}, },
@@ -1291,7 +1335,7 @@ export default class Explorer {
return; return;
} }
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection); const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
useDatabases.setState({ sampleDataResourceTokenCollection }); useDatabases.setState({ sampleDataResourceTokenCollection });
} }
} }

View File

@@ -5,10 +5,10 @@
*/ */
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { useNotebook } from "Explorer/Notebook/useNotebook"; import { useNotebook } from "Explorer/Notebook/useNotebook";
import * as React from "react";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import * as React from "react";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import { ConnectionStatusType, StyleConstants } from "../../../Common/Constants"; import { ConnectionStatusType, PoolIdType, StyleConstants } from "../../../Common/Constants";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
@@ -76,7 +76,9 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
(useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) && (useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) &&
connectionInfo?.status !== ConnectionStatusType.Connect connectionInfo?.status !== ConnectionStatusType.Connect
) { ) {
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus")); uiFabricControlButtons.unshift(
CommandBarUtil.createConnectionStatus(container, PoolIdType.DefaultPoolId, "connectionStatus")
);
} }
return ( return (

View File

@@ -1,3 +1,5 @@
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react"; import * as React from "react";
import AddCollectionIcon from "../../../../images/AddCollection.svg"; import AddCollectionIcon from "../../../../images/AddCollection.svg";
@@ -268,7 +270,6 @@ function createNewCollectionGroup(container: Explorer): CommandButtonComponentPr
ariaLabel: label, ariaLabel: label,
hasPopup: true, hasPopup: true,
id: "createNewContainerCommandButton", id: "createNewContainerCommandButton",
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
}; };
} }
@@ -314,7 +315,6 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
hasPopup: true, hasPopup: true,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
}; };
} }
@@ -328,6 +328,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
onCommandClick: () => { onCommandClick: () => {
if (useSelectedNode.getState().isQueryCopilotCollectionSelected()) { if (useSelectedNode.getState().isQueryCopilotCollectionSelected()) {
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot); useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
} else { } else {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection); selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
@@ -493,7 +494,7 @@ function createOpenTerminalButton(container: Explorer): CommandButtonComponentPr
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default), onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default),
commandButtonLabel: label, commandButtonLabel: label,
hasPopup: false, hasPopup: false,
disabled: false, disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label, ariaLabel: label,
}; };
} }

View File

@@ -9,7 +9,7 @@ import {
import * as React from "react"; import * as React from "react";
import _ from "underscore"; import _ from "underscore";
import ChevronDownIcon from "../../../../images/Chevron_down.svg"; import ChevronDownIcon from "../../../../images/Chevron_down.svg";
import { StyleConstants } from "../../../Common/Constants"; import { PoolIdType, StyleConstants } from "../../../Common/Constants";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
@@ -204,9 +204,9 @@ export const createMemoryTracker = (key: string): ICommandBarItemProps => {
}; };
}; };
export const createConnectionStatus = (container: Explorer, key: string): ICommandBarItemProps => { export const createConnectionStatus = (container: Explorer, poolId: PoolIdType, key: string): ICommandBarItemProps => {
return { return {
key, key,
onRender: () => <ConnectionStatus container={container} />, onRender: () => <ConnectionStatus container={container} poolId={poolId} />,
}; };
}; };

View File

@@ -14,14 +14,15 @@ import { useId } from "@fluentui/react-hooks";
import { ActionButton, DefaultButton } from "@fluentui/react/lib/Button"; import { ActionButton, DefaultButton } from "@fluentui/react/lib/Button";
import * as React from "react"; import * as React from "react";
import "../../../../less/hostedexplorer.less"; import "../../../../less/hostedexplorer.less";
import { ConnectionStatusType, ContainerStatusType, Notebook } from "../../../Common/Constants"; import { ConnectionStatusType, ContainerStatusType, Notebook, PoolIdType } from "../../../Common/Constants";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook"; import { useNotebook } from "../../Notebook/useNotebook";
import "../CommandBar/ConnectionStatusComponent.less"; import "../CommandBar/ConnectionStatusComponent.less";
interface Props { interface Props {
container: Explorer; container: Explorer;
poolId: PoolIdType;
} }
export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Element => { export const ConnectionStatus: React.FC<Props> = ({ container, poolId }: Props): JSX.Element => {
const connectionInfo = useNotebook((state) => state.connectionInfo); const connectionInfo = useNotebook((state) => state.connectionInfo);
const [second, setSecond] = React.useState("00"); const [second, setSecond] = React.useState("00");
const [minute, setMinute] = React.useState("00"); const [minute, setMinute] = React.useState("00");
@@ -93,7 +94,7 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.Reconnect) (connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.Reconnect)
) { ) {
return ( return (
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}> <ActionButton className="commandReactBtn" onClick={() => container.allocateContainer(poolId)}>
<TooltipHost content={toolTipContent}> <TooltipHost content={toolTipContent}>
<Stack className="connectionStatusContainer" horizontal> <Stack className="connectionStatusContainer" horizontal>
<Icon iconName="ConnectVirtualMachine" className="connectIcon" /> <Icon iconName="ConnectVirtualMachine" className="connectIcon" />
@@ -133,7 +134,9 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
id={buttonId} id={buttonId}
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"} className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
onClick={(e: React.MouseEvent<HTMLSpanElement>) => onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault() connectionInfo.status === ConnectionStatusType.Failed
? container.allocateContainer(poolId)
: e.preventDefault()
} }
> >
<Stack className="connectionStatusContainer" horizontal> <Stack className="connectionStatusContainer" horizontal>

View File

@@ -122,7 +122,9 @@ export class NotificationConsoleComponent extends React.Component<
{userContext.features.pr && <PrPreview pr={userContext.features.pr} />} {userContext.features.pr && <PrPreview pr={userContext.features.pr} />}
<span className="consoleSplitter" /> <span className="consoleSplitter" />
<span className="headerStatus"> <span className="headerStatus">
<span className="headerStatusEllipsis">{this.state.headerStatus}</span> <span className="headerStatusEllipsis" aria-live="assertive" aria-atomic="true">
{this.state.headerStatus}
</span>
</span> </span>
</div> </div>
<div <div

View File

@@ -64,6 +64,8 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
className="headerStatus" className="headerStatus"
> >
<span <span
aria-atomic="true"
aria-live="assertive"
className="headerStatusEllipsis" className="headerStatusEllipsis"
/> />
</span> </span>
@@ -225,6 +227,8 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
className="headerStatus" className="headerStatus"
> >
<span <span
aria-atomic="true"
aria-live="assertive"
className="headerStatusEllipsis" className="headerStatusEllipsis"
> >
message message

View File

@@ -1425,6 +1425,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
this.setState({ isExecuting: false }); this.setState({ isExecuting: false });
TelemetryProcessor.traceSuccess(Action.CreateCollection, telemetryData, startKey); TelemetryProcessor.traceSuccess(Action.CreateCollection, telemetryData, startKey);
useSidePanel.getState().closeSidePanel(); useSidePanel.getState().closeSidePanel();
// open NPS Survey Dialog once the collection is created
this.props.explorer.openNPSSurveyDialog();
} catch (error) { } catch (error) {
const errorMessage: string = getErrorMessage(error); const errorMessage: string = getErrorMessage(error);
this.setState({ isExecuting: false, errorMessage, showErrorDetails: true }); this.setState({ isExecuting: false, errorMessage, showErrorDetails: true });

View File

@@ -2,13 +2,13 @@ import { Checkbox, Dropdown, IDropdownOption, Link, Stack, Text, TextField } fro
import * as Constants from "Common/Constants"; import * as Constants from "Common/Constants";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react";
import * as SharedConstants from "Shared/Constants"; import * as SharedConstants from "Shared/Constants";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { isServerlessAccount } from "Utils/CapabilityUtils"; import { isServerlessAccount } from "Utils/CapabilityUtils";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput"; import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { CassandraAPIDataClient } from "../../Tables/TableDataClient"; import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
@@ -291,7 +291,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
styles={getTextFieldStyles({ fontSize: 12, width: 150 })} styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
aria-required="true" aria-required="true"
required={true} required={true}
ariaLabel="addCollection-tableId" ariaLabel="addCollection-table Id Create table"
autoComplete="off" autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]" pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'" title="May not end with space nor contain characters '\' '/' '#' '?'"

View File

@@ -1,12 +1,12 @@
import { IDropdownOption } from "@fluentui/react"; import { IDropdownOption } from "@fluentui/react";
import React, { FormEvent, FunctionComponent, useEffect, useState } from "react"; import React, { FormEvent, FunctionComponent, useEffect, useState } from "react";
import { HttpStatusCodes } from "../../../Common/Constants"; import { HttpStatusCodes, PoolIdType } from "../../../Common/Constants";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient"; import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
import * as GitHubUtils from "../../../Utils/GitHubUtils"; import * as GitHubUtils from "../../../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { useSidePanel } from "../../../hooks/useSidePanel";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
import { useNotebook } from "../../Notebook/useNotebook"; import { useNotebook } from "../../Notebook/useNotebook";
@@ -109,7 +109,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
}; };
isGithubTree = false; isGithubTree = false;
if (useNotebook.getState().isPhoenixNotebooks) { if (useNotebook.getState().isPhoenixNotebooks) {
await container.allocateContainer(); await container.allocateContainer(PoolIdType.DefaultPoolId);
} }
break; break;

View File

@@ -1,18 +1,18 @@
import { Text, TextField } from "@fluentui/react"; import { Text, TextField } from "@fluentui/react";
import { Areas } from "Common/Constants"; import { Areas } from "Common/Constants";
import { deleteCollection } from "Common/dataAccess/deleteCollection";
import DeleteFeedback from "Common/DeleteFeedback"; import DeleteFeedback from "Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { deleteCollection } from "Common/dataAccess/deleteCollection";
import { Collection } from "Contracts/ViewModels"; import { Collection } from "Contracts/ViewModels";
import { useSidePanel } from "hooks/useSidePanel";
import { useTabs } from "hooks/useTabs";
import React, { FunctionComponent, useState } from "react";
import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils"; import { getCollectionName } from "Utils/APITypeUtils";
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
import { useSidePanel } from "hooks/useSidePanel";
import { useTabs } from "hooks/useTabs";
import React, { FunctionComponent, useState } from "react";
import { useDatabases } from "../../useDatabases"; import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
@@ -126,6 +126,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
setInputCollectionName(newInput); setInputCollectionName(newInput);
}} }}
ariaLabel={confirmContainer} ariaLabel={confirmContainer}
required
/> />
</div> </div>
{shouldRecordFeedback() && ( {shouldRecordFeedback() && (

View File

@@ -44,6 +44,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
autoFocus={true} autoFocus={true}
id="confirmCollectionId" id="confirmCollectionId"
onChange={[Function]} onChange={[Function]}
required={true}
styles={ styles={
Object { Object {
"fieldGroup": Object { "fieldGroup": Object {
@@ -59,6 +60,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
deferredValidationTime={200} deferredValidationTime={200}
id="confirmCollectionId" id="confirmCollectionId"
onChange={[Function]} onChange={[Function]}
required={true}
resizable={true} resizable={true}
styles={[Function]} styles={[Function]}
theme={ theme={
@@ -338,7 +340,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
value="" value=""
> >
<div <div
className="ms-TextField root-55" className="ms-TextField is-required root-55"
> >
<div <div
className="ms-TextField-wrapper" className="ms-TextField-wrapper"
@@ -356,6 +358,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
onChange={[Function]} onChange={[Function]}
onFocus={[Function]} onFocus={[Function]}
onInput={[Function]} onInput={[Function]}
required={true}
type="text" type="text"
value="" value=""
/> />

View File

@@ -140,6 +140,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
setDatabaseInput(newInput); setDatabaseInput(newInput);
}} }}
ariaLabel={confirmDatabase} ariaLabel={confirmDatabase}
required
/> />
</div> </div>
{isLastNonEmptyDatabase() && ( {isLastNonEmptyDatabase() && (

View File

@@ -112,6 +112,9 @@
margin-top: 28px; margin-top: 28px;
margin-left: 4px !important; margin-left: 4px !important;
} }
.addRemoveIcon [alt="editEntity"]:focus,.addRemoveIconLabel [alt="editEntity"]:focus{
border:1px dashed #605E5C
}
.addNewParamStyle { .addNewParamStyle {
margin-top: 5px; margin-top: 5px;
margin-left: 5px !important; margin-left: 5px !important;

View File

@@ -41,12 +41,18 @@ export const SettingsPane: FunctionComponent = () => {
? LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism) ? LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism)
: Constants.Queries.DefaultMaxDegreeOfParallelism : Constants.Queries.DefaultMaxDegreeOfParallelism
); );
const [priorityLevel, setPriorityLevel] = useState<string>(
LocalStorageUtility.hasItem(StorageKey.PriorityLevel)
? LocalStorageUtility.getEntryString(StorageKey.PriorityLevel)
: Constants.PriorityLevel.Default
);
const explorerVersion = configContext.gitSha; const explorerVersion = configContext.gitSha;
const shouldShowQueryPageOptions = userContext.apiType === "SQL"; const shouldShowQueryPageOptions = userContext.apiType === "SQL";
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin"; const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin"; const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
const shouldShowParallelismOption = userContext.apiType !== "Gremlin"; const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
const shouldShowPriorityLevelOption =
userContext.features.enablePriorityBasedThrottling && userContext.apiType === "SQL";
const handlerOnSubmit = (e: MouseEvent<HTMLButtonElement>) => { const handlerOnSubmit = (e: MouseEvent<HTMLButtonElement>) => {
setIsExecuting(true); setIsExecuting(true);
@@ -58,6 +64,7 @@ export const SettingsPane: FunctionComponent = () => {
LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString()); LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString());
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString()); LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism); LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString());
if (shouldShowGraphAutoVizOption) { if (shouldShowGraphAutoVizOption) {
LocalStorageUtility.setEntryBoolean( LocalStorageUtility.setEntryBoolean(
@@ -76,6 +83,7 @@ export const SettingsPane: FunctionComponent = () => {
StorageKey.MaxDegreeOfParellism StorageKey.MaxDegreeOfParellism
)}` )}`
); );
logConsoleInfo(`Updated priority level setting to ${LocalStorageUtility.getEntryString(StorageKey.PriorityLevel)}`);
if (shouldShowGraphAutoVizOption) { if (shouldShowGraphAutoVizOption) {
logConsoleInfo( logConsoleInfo(
@@ -116,6 +124,18 @@ export const SettingsPane: FunctionComponent = () => {
{ key: "true", text: "JSON" }, { key: "true", text: "JSON" },
]; ];
const priorityLevelOptionList: IChoiceGroupOption[] = [
{ key: Constants.PriorityLevel.Low, text: "Low" },
{ key: Constants.PriorityLevel.High, text: "High" },
];
const handleOnPriorityLevelOptionChange = (
ev: React.FormEvent<HTMLInputElement>,
option: IChoiceGroupOption
): void => {
setPriorityLevel(option.key);
};
const handleOnPageOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => { const handleOnPageOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
setPageOption(option.key); setPageOption(option.key);
}; };
@@ -260,6 +280,29 @@ export const SettingsPane: FunctionComponent = () => {
</div> </div>
</div> </div>
)} )}
{shouldShowPriorityLevelOption && (
<div className="settingsSection">
<div className="settingsSectionPart">
<fieldset>
<legend id="priorityLevel" className="settingsSectionLabel legendLabel">
Priority Level
</legend>
<InfoTooltip>
Sets the priority level for data-plane requests from Data Explorer when using Priority-Based
Execution. If &quot;None&quot; is selected, Data Explorer will not specify priority level, and the
server-side default priority level will be used.
</InfoTooltip>
<ChoiceGroup
ariaLabelledBy="priorityLevel"
selectedKey={priorityLevel}
options={priorityLevelOptionList}
styles={choiceButtonStyles}
onChange={handleOnPriorityLevelOptionChange}
/>
</fieldset>
</div>
</div>
)}
{shouldShowGraphAutoVizOption && ( {shouldShowGraphAutoVizOption && (
<div className="settingsSection"> <div className="settingsSection">
<div className="settingsSectionPart"> <div className="settingsSectionPart">

View File

@@ -371,6 +371,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
autoFocus={true} autoFocus={true}
id="confirmDatabaseId" id="confirmDatabaseId"
onChange={[Function]} onChange={[Function]}
required={true}
styles={ styles={
Object { Object {
"fieldGroup": Object { "fieldGroup": Object {
@@ -385,6 +386,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
deferredValidationTime={200} deferredValidationTime={200}
id="confirmDatabaseId" id="confirmDatabaseId"
onChange={[Function]} onChange={[Function]}
required={true}
resizable={true} resizable={true}
styles={[Function]} styles={[Function]}
theme={ theme={
@@ -663,7 +665,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
validateOnLoad={true} validateOnLoad={true}
> >
<div <div
className="ms-TextField root-58" className="ms-TextField is-required root-58"
> >
<div <div
className="ms-TextField-wrapper" className="ms-TextField-wrapper"
@@ -681,6 +683,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
onChange={[Function]} onChange={[Function]}
onFocus={[Function]} onFocus={[Function]}
onInput={[Function]} onInput={[Function]}
required={true}
type="text" type="text"
value="" value=""
/> />

View File

@@ -0,0 +1,127 @@
import { Checkbox, ChoiceGroup, DefaultButton, IconButton, PrimaryButton, TextField } from "@fluentui/react";
import Explorer from "Explorer/Explorer";
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { getUserEmail } from "Utils/UserUtils";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
jest.mock("Utils/UserUtils");
(getUserEmail as jest.Mock).mockResolvedValue("test@email.com");
jest.mock("Explorer/QueryCopilot/Shared/QueryCopilotClient");
SubmitFeedback as jest.Mock;
describe("Query Copilot Feedback Modal snapshot test", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("shoud render and match snapshot", () => {
useQueryCopilot.getState().openFeedbackModal("test query", false, "test prompt");
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
expect(wrapper.props().isOpen).toBeTruthy();
expect(wrapper).toMatchSnapshot();
});
it("should close on cancel click", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
const cancelButton = wrapper.find(IconButton);
cancelButton.simulate("click");
wrapper.setProps({});
expect(wrapper.props().isOpen).toBeFalsy();
expect(wrapper).toMatchSnapshot();
});
it("should get user unput", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
const testUserInput = "test user input";
const userInput = wrapper.find(TextField).first();
userInput.simulate("change", {}, testUserInput);
expect(wrapper.find(TextField).first().props().value).toEqual(testUserInput);
expect(wrapper).toMatchSnapshot();
});
it("should record user contact choice no", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
const contactAllowed = wrapper.find(ChoiceGroup);
contactAllowed.simulate("change", {}, { key: "no" });
expect(getUserEmail).toHaveBeenCalledTimes(3);
expect(wrapper.find(ChoiceGroup).props().selectedKey).toEqual("no");
expect(wrapper).toMatchSnapshot();
});
it("should record user contact choice yes", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
const contactAllowed = wrapper.find(ChoiceGroup);
contactAllowed.simulate("change", {}, { key: "yes" });
expect(getUserEmail).toHaveBeenCalledTimes(4);
expect(wrapper.find(ChoiceGroup).props().selectedKey).toEqual("yes");
expect(wrapper).toMatchSnapshot();
});
it("should not render dont show again button", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
const dontShowAgain = wrapper.find(Checkbox);
expect(dontShowAgain).toHaveLength(0);
expect(wrapper).toMatchSnapshot();
});
it("should render dont show again button and check it ", () => {
useQueryCopilot.getState().openFeedbackModal("test query", true, "test prompt");
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
const dontShowAgain = wrapper.find(Checkbox);
dontShowAgain.simulate("change", {}, true);
expect(wrapper.find(Checkbox)).toHaveLength(1);
expect(wrapper.find(Checkbox).first().props().checked).toBeTruthy();
expect(wrapper).toMatchSnapshot();
});
it("should cancel submission", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
const cancelButton = wrapper.find(DefaultButton);
cancelButton.simulate("click");
wrapper.setProps({});
expect(wrapper.props().isOpen).toBeFalsy();
expect(wrapper).toMatchSnapshot();
});
it("should submit submission", () => {
const explorer = new Explorer();
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />);
const submitButton = wrapper.find(PrimaryButton);
submitButton.simulate("click");
wrapper.setProps({});
expect(SubmitFeedback).toHaveBeenCalledTimes(1);
expect(SubmitFeedback).toHaveBeenCalledWith({
params: {
likeQuery: false,
generatedQuery: "",
userPrompt: "",
description: "",
contact: getUserEmail(),
},
explorer: explorer,
});
expect(wrapper.props().isOpen).toBeFalsy();
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -10,12 +10,13 @@ import {
Text, Text,
TextField, TextField,
} from "@fluentui/react"; } from "@fluentui/react";
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities"; import Explorer from "Explorer/Explorer";
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react"; import React from "react";
import { getUserEmail } from "../../Utils/UserUtils"; import { getUserEmail } from "../../../Utils/UserUtils";
export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => { export const QueryCopilotFeedbackModal = ({ explorer }: { explorer: Explorer }): JSX.Element => {
const { const {
generatedQuery, generatedQuery,
userPrompt, userPrompt,
@@ -28,6 +29,7 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
const [description, setDescription] = React.useState<string>(""); const [description, setDescription] = React.useState<string>("");
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false); const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
const [contact, setContact] = React.useState<string>(getUserEmail()); const [contact, setContact] = React.useState<string>(getUserEmail());
return ( return (
<Modal isOpen={showFeedbackModal}> <Modal isOpen={showFeedbackModal}>
<Stack style={{ padding: 24 }}> <Stack style={{ padding: 24 }}>
@@ -77,11 +79,13 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
}} }}
></ChoiceGroup> ></ChoiceGroup>
<Text style={{ fontSize: 12, marginBottom: 14 }}> <Text style={{ fontSize: 12, marginBottom: 14 }}>
By pressing submit, your feedback will be used to improve Microsoft products and services. IT admins for your By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the{" "}
organization will be able to view and manage your feedback data.{" "} {
<Link href="" target="_blank"> <Link href="https://privacy.microsoft.com/privacystatement" target="_blank">
Privacy statement Privacy statement
</Link> </Link>
}{" "}
for more information.
</Text> </Text>
{likeQuery && ( {likeQuery && (
<Checkbox <Checkbox
@@ -97,7 +101,10 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
onClick={() => { onClick={() => {
closeFeedbackModal(); closeFeedbackModal();
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked); setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
submitFeedback({ generatedQuery, likeQuery, description, userPrompt, contact }); SubmitFeedback({
params: { generatedQuery, likeQuery, description, userPrompt, contact },
explorer: explorer,
});
}} }}
> >
Submit Submit

View File

@@ -0,0 +1,32 @@
import { shallow } from "enzyme";
import { withHooks } from "jest-react-hooks-shallow";
import React from "react";
import { WelcomeModal } from "./WelcomeModal";
describe("Query Copilot Welcome Modal snapshot test", () => {
it("should render when isOpen is true", () => {
withHooks(() => {
const spy = jest.spyOn(localStorage, "setItem");
spy.mockClear();
const wrapper = shallow(<WelcomeModal visible={true} />);
expect(wrapper.props().children.props.isOpen).toBeTruthy();
expect(wrapper).toMatchSnapshot();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenLastCalledWith("hideWelcomeModal", "true");
});
});
it("should not render when isOpen is false", () => {
withHooks(() => {
const spy = jest.spyOn(localStorage, "setItem");
spy.mockClear();
const wrapper = shallow(<WelcomeModal visible={false} />);
expect(wrapper.props().children.props.isOpen).toBeFalsy();
expect(spy).not.toHaveBeenCalled();
expect(spy.mock.instances.length).toBe(0);
});
});
});

View File

@@ -42,8 +42,8 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
</Stack> </Stack>
</Stack> </Stack>
<Stack horizontalAlign="center"> <Stack horizontalAlign="center">
<Stack.Item align="center"> <Stack.Item align="center" style={{ textAlign: "center" }}>
<Text className="title bold">Welcome to Copilot in CosmosDB</Text> <Text className="title bold">Welcome to Query Copilot for Azure Cosmos DB (Private Preview)</Text>
</Stack.Item> </Stack.Item>
<Stack.Item align="center" className="text"> <Stack.Item align="center" className="text">
<Stack horizontal> <Stack horizontal>
@@ -52,7 +52,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
</StackItem> </StackItem>
<StackItem align="start"> <StackItem align="start">
<Text className="bold"> <Text className="bold">
Let copilot do the work for you Let Query Copilot do the work for you
<br /> <br />
</Text> </Text>
</StackItem> </StackItem>
@@ -60,7 +60,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
<Text> <Text>
Ask Copilot to generate a query by describing the query in your words. Ask Copilot to generate a query by describing the query in your words.
<br /> <br />
<Link href="">Learn more</Link> <Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
</Text> </Text>
</Stack.Item> </Stack.Item>
<Stack.Item align="center" className="text"> <Stack.Item align="center" className="text">
@@ -78,7 +78,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
<Text> <Text>
AI-generated content can have mistakes. Make sure its accurate and appropriate before using it. AI-generated content can have mistakes. Make sure its accurate and appropriate before using it.
<br /> <br />
<Link href="">Read preview terms</Link> <Link href="http://aka.ms/cdb-copilot-preview-terms">Read preview terms</Link>
</Text> </Text>
</Stack.Item> </Stack.Item>
<Stack.Item align="center" className="text"> <Stack.Item align="center" className="text">
@@ -88,15 +88,16 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
</StackItem> </StackItem>
<StackItem align="start"> <StackItem align="start">
<Text className="bold"> <Text className="bold">
Copilot currently works only a sample database Query Copilot works on a sample database.
<br /> <br />
</Text> </Text>
</StackItem> </StackItem>
</Stack> </Stack>
<Text> <Text>
Copilot is setup on a sample database we have configured for you at no cost While in Private Preview, Query Copilot is setup to work on sample database we have configured for you
at no cost.
<br /> <br />
<Link href="">Learn more</Link> <Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
</Text> </Text>
</Stack.Item> </Stack.Item>
</Stack> </Stack>

View File

@@ -0,0 +1,201 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is true 1`] = `
<Fragment>
<Modal
isBlocking={false}
isOpen={true}
onDismiss={[Function]}
>
<Stack
className="modalContentPadding"
>
<Stack
horizontal={true}
>
<Stack
grow={4}
horizontal={true}
horizontalAlign="end"
>
<StackItem>
<Image
src=""
/>
</StackItem>
</Stack>
<Stack
className="exitPadding"
grow={1}
horizontal={true}
horizontalAlign="end"
verticalAlign="start"
>
<StackItem
className="previewMargin"
>
<Text
className="preview"
>
Preview
</Text>
</StackItem>
<StackItem>
<CustomizedIconButton
ariaLabel="Exit"
className="exitIcon"
iconProps={
Object {
"iconName": "Cancel",
}
}
onClick={[Function]}
title="Exit"
/>
</StackItem>
</Stack>
</Stack>
<Stack
horizontalAlign="center"
>
<StackItem
align="center"
style={
Object {
"textAlign": "center",
}
}
>
<Text
className="title bold"
>
Welcome to Query Copilot for Azure Cosmos DB (Private Preview)
</Text>
</StackItem>
<StackItem
align="center"
className="text"
>
<Stack
horizontal={true}
>
<StackItem
align="start"
className="imageTextPadding"
>
<Image
src=""
/>
</StackItem>
<StackItem
align="start"
>
<Text
className="bold"
>
Let Query Copilot do the work for you
<br />
</Text>
</StackItem>
</Stack>
<Text>
Ask Copilot to generate a query by describing the query in your words.
<br />
<StyledLinkBase
href="http://aka.ms/cdb-copilot-learn-more"
>
Learn more
</StyledLinkBase>
</Text>
</StackItem>
<StackItem
align="center"
className="text"
>
<Stack
horizontal={true}
>
<StackItem
align="start"
className="imageTextPadding"
>
<Image
src=""
/>
</StackItem>
<StackItem
align="start"
>
<Text
className="bold"
>
Use your judgement
<br />
</Text>
</StackItem>
</Stack>
<Text>
AI-generated content can have mistakes. Make sure its accurate and appropriate before using it.
<br />
<StyledLinkBase
href="http://aka.ms/cdb-copilot-preview-terms"
>
Read preview terms
</StyledLinkBase>
</Text>
</StackItem>
<StackItem
align="center"
className="text"
>
<Stack
horizontal={true}
>
<StackItem
align="start"
className="imageTextPadding"
>
<Image
src=""
/>
</StackItem>
<StackItem
align="start"
>
<Text
className="bold"
>
Query Copilot works on a sample database.
<br />
</Text>
</StackItem>
</Stack>
<Text>
While in Private Preview, Query Copilot is setup to work on sample database we have configured for you at no cost.
<br />
<StyledLinkBase
href="http://aka.ms/cdb-copilot-learn-more"
>
Learn more
</StyledLinkBase>
</Text>
</StackItem>
</Stack>
<Stack
className="buttonPadding"
>
<StackItem
align="center"
>
<CustomizedPrimaryButton
className="tryButton"
onClick={[Function]}
>
Try Copilot
</CustomizedPrimaryButton>
</StackItem>
</Stack>
</Stack>
</Modal>
</Fragment>
`;

View File

@@ -1,11 +1,48 @@
import { IconButton } from "@fluentui/react";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react"; import React from "react";
import { any } from "underscore";
import { CopyPopup } from "./CopyPopup"; import { CopyPopup } from "./CopyPopup";
describe("Copy Popup snapshot test", () => { describe("Copy Popup snapshot test", () => {
const setShowCopyPopupMock = jest.fn();
it("should render when showCopyPopup is true", () => { it("should render when showCopyPopup is true", () => {
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={() => any} />); const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={setShowCopyPopupMock} />);
expect(wrapper.exists()).toBe(true);
expect(wrapper.prop("setShowCopyPopup")).toBeUndefined();
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("should render when showCopyPopup is false", () => {
const wrapper = shallow(<CopyPopup showCopyPopup={false} setShowCopyPopup={setShowCopyPopupMock} />);
expect(wrapper.prop("showCopyPopup")).toBeFalsy();
expect(wrapper.prop("setShowCopyPopup")).toBeUndefined();
expect(wrapper).toMatchSnapshot();
});
it("should call setShowCopyPopup(false) when close button is clicked", () => {
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={setShowCopyPopupMock} />);
const closeButton = wrapper.find(IconButton);
closeButton.props().onClick?.({} as React.MouseEvent<HTMLButtonElement, MouseEvent>);
expect(setShowCopyPopupMock).toHaveBeenCalledWith(false);
});
it("should have the correct inline styles", () => {
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={setShowCopyPopupMock} />);
const stackStyle = wrapper.find("Stack").first().props().style;
expect(stackStyle).toEqual({
position: "fixed",
width: 345,
height: 66,
padding: 10,
gap: 5,
top: 75,
right: 20,
background: "#FFFFFF",
boxShadow: "0 2px 6px rgba(0, 0, 0, 0.16)",
});
});
}); });

View File

@@ -1,19 +1,113 @@
import { shallow } from "enzyme"; import { mount, shallow } from "enzyme";
import React from "react"; import React from "react";
import { any } from "underscore";
import { DeletePopup } from "./DeletePopup"; import { DeletePopup } from "./DeletePopup";
describe("Delete Popup snapshot test", () => { describe("Delete Popup snapshot test", () => {
const setShowDeletePopupMock = jest.fn();
const setQueryMock = jest.fn();
const clearFeedbackMock = jest.fn();
const showFeedbackBarMock = jest.fn();
it("should render when showDeletePopup is true", () => { it("should render when showDeletePopup is true", () => {
const wrapper = shallow( const wrapper = shallow(
<DeletePopup <DeletePopup
showDeletePopup={true} showDeletePopup={true}
setShowDeletePopup={() => any} setShowDeletePopup={setShowDeletePopupMock}
setQuery={() => any} setQuery={setQueryMock}
clearFeedback={() => any} clearFeedback={clearFeedbackMock}
showFeedbackBar={() => any} showFeedbackBar={showFeedbackBarMock}
/> />
); );
expect(wrapper.find("Modal").prop("isOpen")).toBeTruthy();
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("should not render when showDeletePopup is false", () => {
const wrapper = shallow(
<DeletePopup
showDeletePopup={false}
setShowDeletePopup={setShowDeletePopupMock}
setQuery={setQueryMock}
clearFeedback={clearFeedbackMock}
showFeedbackBar={showFeedbackBarMock}
/>
);
expect(wrapper.props().children.props.showDeletePopup).toBeFalsy();
expect(wrapper).toMatchSnapshot();
});
it("should call setQuery with an empty string and setShowDeletePopup(false) when delete button is clicked", () => {
const wrapper = mount(
<DeletePopup
showDeletePopup={true}
setShowDeletePopup={setShowDeletePopupMock}
setQuery={setQueryMock}
clearFeedback={clearFeedbackMock}
showFeedbackBar={showFeedbackBarMock}
/>
);
wrapper.find("PrimaryButton").simulate("click");
expect(setQueryMock).toHaveBeenCalledWith("");
expect(setShowDeletePopupMock).toHaveBeenCalledWith(false);
});
it("should call setShowDeletePopup(false) when close button is clicked", () => {
const setShowDeletePopupMock = jest.fn();
const wrapper = mount(
<DeletePopup
showDeletePopup={true}
setShowDeletePopup={setShowDeletePopupMock}
setQuery={setQueryMock}
clearFeedback={clearFeedbackMock}
showFeedbackBar={showFeedbackBarMock}
/>
);
wrapper.find("DefaultButton").at(1).simulate("click");
expect(setShowDeletePopupMock).toHaveBeenCalledWith(false);
});
it("should render the appropriate text content", () => {
const wrapper = shallow(
<DeletePopup
showDeletePopup={true}
setShowDeletePopup={setShowDeletePopupMock}
setQuery={setQueryMock}
clearFeedback={clearFeedbackMock}
showFeedbackBar={showFeedbackBarMock}
/>
);
const textContent = wrapper
.find("Text")
.map((text, index) => <React.Fragment key={index}>{text.props().children}</React.Fragment>);
expect(textContent).toEqual([
<React.Fragment key={0}>
<b>Delete code?</b>
</React.Fragment>,
<React.Fragment key={1}>
This will clear the query from the query builder pane along with all comments and also reset the prompt pane
</React.Fragment>,
]);
});
it("should have the correct inline style", () => {
const wrapper = shallow(
<DeletePopup
showDeletePopup={true}
setShowDeletePopup={setShowDeletePopupMock}
setQuery={setQueryMock}
clearFeedback={clearFeedbackMock}
showFeedbackBar={showFeedbackBarMock}
/>
);
const stackStyle = wrapper.find("Stack[style]").props().style;
expect(stackStyle).toEqual({ padding: "16px 24px", height: "auto" });
});
}); });

View File

@@ -1,5 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Copy Popup snapshot test should render when showCopyPopup is false 1`] = `<Fragment />`;
exports[`Copy Popup snapshot test should render when showCopyPopup is true 1`] = ` exports[`Copy Popup snapshot test should render when showCopyPopup is true 1`] = `
<Stack <Stack
style={ style={

View File

@@ -1,5 +1,83 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Delete Popup snapshot test should not render when showDeletePopup is false 1`] = `
<Modal
isOpen={false}
styles={
Object {
"main": Object {
"minHeight": "122px",
"minWidth": "880px",
},
}
}
>
<Stack
style={
Object {
"height": "auto",
"padding": "16px 24px",
}
}
>
<Text
style={
Object {
"fontSize": "18px",
"height": 24,
}
}
>
<b>
Delete code?
</b>
</Text>
<Text
style={
Object {
"marginBottom": 20,
"marginTop": 10,
}
}
>
This will clear the query from the query builder pane along with all comments and also reset the prompt pane
</Text>
<Stack
horizontal={true}
horizontalAlign="start"
tokens={
Object {
"childrenGap": 10,
}
}
>
<CustomizedPrimaryButton
onClick={[Function]}
style={
Object {
"height": 24,
"padding": "0px 20px",
}
}
>
Delete
</CustomizedPrimaryButton>
<CustomizedDefaultButton
onClick={[Function]}
style={
Object {
"height": 24,
"padding": "0px 20px",
}
}
>
Close
</CustomizedDefaultButton>
</Stack>
</Stack>
</Modal>
`;
exports[`Delete Popup snapshot test should render when showDeletePopup is true 1`] = ` exports[`Delete Popup snapshot test should render when showDeletePopup is true 1`] = `
<Modal <Modal
isOpen={true} isOpen={true}

View File

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

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { FeedOptions } from "@azure/cosmos";
import { import {
Callout, Callout,
CommandBarButton, CommandBarButton,
@@ -18,28 +18,32 @@ import {
} from "@fluentui/react"; } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import { import {
PoolIdType,
QueryCopilotSampleContainerId, QueryCopilotSampleContainerId,
QueryCopilotSampleContainerSchema, QueryCopilotSampleContainerSchema,
QueryCopilotSampleDatabaseId, ShortenedQueryCopilotSampleContainerSchema,
} from "Common/Constants"; } from "Common/Constants";
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils"; import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility"; import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
import { MinimalQueryIterator } from "Common/IteratorUtilities"; import { MinimalQueryIterator } from "Common/IteratorUtilities";
import { sampleDataClient } from "Common/SampleDataClient"; import { createUri } from "Common/UrlUtility";
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage"; import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
import { QueryResults } from "Contracts/ViewModels"; import { QueryResults } from "Contracts/ViewModels";
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane"; import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal"; import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal";
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup"; import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup"; import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities"; import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/SamplePrompts/SamplePrompts"; import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/SamplePrompts/SamplePrompts";
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection"; import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { queryPagesUntilContentPresent } from "Utils/QueryUtils"; import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
@@ -50,7 +54,6 @@ import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import HintIcon from "../../../images/Hint.svg"; import HintIcon from "../../../images/Hint.svg";
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg"; import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
import RecentIcon from "../../../images/Recent.svg"; import RecentIcon from "../../../images/Recent.svg";
import SamplePromptsIcon from "../../../images/SamplePromptsIcon.svg";
import XErrorMessage from "../../../images/X-errorMessage.svg"; import XErrorMessage from "../../../images/X-errorMessage.svg";
import SaveQueryIcon from "../../../images/save-cosmos.svg"; import SaveQueryIcon from "../../../images/save-cosmos.svg";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
@@ -60,49 +63,57 @@ interface SuggestedPrompt {
text: string; text: string;
} }
interface QueryCopilotTabProps {
initialInput: string;
explorer: Explorer;
}
interface GenerateSQLQueryResponse {
apiVersion: string;
sql: string;
explanation: string;
generateStart: string;
generateEnd: string;
}
const promptStyles: IButtonStyles = { const promptStyles: IButtonStyles = {
root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } }, root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } },
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 }, label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
}; };
export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
initialInput,
explorer,
}: QueryCopilotTabProps): JSX.Element => {
const hideFeedbackModalForLikedQueries = useQueryCopilot((state) => state.hideFeedbackModalForLikedQueries);
const [userPrompt, setUserPrompt] = useState<string>(initialInput || "");
const [generatedQuery, setGeneratedQuery] = useState<string>("");
const [query, setQuery] = useState<string>("");
const [selectedQuery, setSelectedQuery] = useState<string>("");
const [isGeneratingQuery, setIsGeneratingQuery] = useState<boolean>(false);
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const [likeQuery, setLikeQuery] = useState<boolean>();
const [dislikeQuery, setDislikeQuery] = useState<boolean>();
const [showCallout, setShowCallout] = useState<boolean>(false);
const [showSamplePrompts, setShowSamplePrompts] = useState<boolean>(false);
const [queryIterator, setQueryIterator] = useState<MinimalQueryIterator>();
const [queryResults, setQueryResults] = useState<QueryResults>();
const [errorMessage, setErrorMessage] = useState<string>("");
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false); const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
const inputEdited = useRef(false); const inputEdited = useRef(false);
const [isSamplePromptsOpen, setIsSamplePromptsOpen] = useState<boolean>(false); const {
const [showDeletePopup, setShowDeletePopup] = useState<boolean>(false); hideFeedbackModalForLikedQueries,
const [showFeedbackBar, setShowFeedbackBar] = useState<boolean>(false); userPrompt,
const [showCopyPopup, setshowCopyPopup] = useState<boolean>(false); setUserPrompt,
const [showErrorMessageBar, setShowErrorMessageBar] = useState<boolean>(false); generatedQuery,
setGeneratedQuery,
query,
setQuery,
selectedQuery,
setSelectedQuery,
isGeneratingQuery,
setIsGeneratingQuery,
isExecuting,
setIsExecuting,
likeQuery,
setLikeQuery,
dislikeQuery,
setDislikeQuery,
showCallout,
setShowCallout,
showSamplePrompts,
setShowSamplePrompts,
queryIterator,
setQueryIterator,
queryResults,
setQueryResults,
errorMessage,
setErrorMessage,
isSamplePromptsOpen,
setIsSamplePromptsOpen,
showDeletePopup,
setShowDeletePopup,
showFeedbackBar,
setShowFeedbackBar,
showCopyPopup,
setshowCopyPopup,
showErrorMessageBar,
setShowErrorMessageBar,
generatedQueryComments,
setGeneratedQueryComments,
shouldAllocateContainer,
setShouldAllocateContainer,
} = useQueryCopilot();
const sampleProps: SamplePromptsProps = { const sampleProps: SamplePromptsProps = {
isSamplePromptsOpen: isSamplePromptsOpen, isSamplePromptsOpen: isSamplePromptsOpen,
@@ -128,12 +139,12 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
}; };
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`); const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
const cachedHistories = cachedHistoriesString?.split(","); const cachedHistories = cachedHistoriesString?.split("|");
const [histories, setHistories] = useState<string[]>(cachedHistories || []); const [histories, setHistories] = useState<string[]>(cachedHistories || []);
const suggestedPrompts: SuggestedPrompt[] = [ const suggestedPrompts: SuggestedPrompt[] = [
{ id: 1, text: "Give me all customers whose names start with C" }, { id: 1, text: 'Show all products that have the word "ultra" in the name or description' },
{ id: 2, text: "Show me all customers" }, { id: 2, text: "What are all of the possible categories for the products, and their counts?" },
{ id: 3, text: "Show me all customers who bought a bike in 2019" }, { id: 3, text: 'Show me all products that have been reviewed by someone with a username that contains "bob"' },
]; ];
const [filteredHistories, setFilteredHistories] = useState<string[]>(histories); const [filteredHistories, setFilteredHistories] = useState<string[]>(histories);
const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts); const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts);
@@ -155,38 +166,69 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
}; };
const updateHistories = (): void => { const updateHistories = (): void => {
const newHistories = histories.length < 3 ? [userPrompt, ...histories] : [userPrompt, histories[1], histories[2]]; const formattedUserPrompt = userPrompt.replace(/\s+/g, " ").trim();
const existingHistories = histories.map((history) => history.replace(/\s+/g, " ").trim());
const updatedHistories = existingHistories.filter(
(history) => history.toLowerCase() !== formattedUserPrompt.toLowerCase()
);
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
setHistories(newHistories); setHistories(newHistories);
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join(",")); localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join("|"));
}; };
const generateSQLQuery = async (): Promise<void> => { const generateSQLQuery = async (): Promise<void> => {
try { try {
if (shouldAllocateContainer && userContext.features.enableCopilotPhoenixGateaway) {
await explorer.allocateContainer(PoolIdType.QueryCopilot);
setShouldAllocateContainer(false);
}
setIsGeneratingQuery(true); setIsGeneratingQuery(true);
useTabs.getState().setIsTabExecuting(true); useTabs.getState().setIsTabExecuting(true);
useTabs.getState().setIsQueryErrorThrown(false); useTabs.getState().setIsQueryErrorThrown(false);
const payload = { const payload = {
containerSchema: QueryCopilotSampleContainerSchema, containerSchema: userContext.features.enableCopilotFullSchema
? QueryCopilotSampleContainerSchema
: ShortenedQueryCopilotSampleContainerSchema,
userPrompt: userPrompt, userPrompt: userPrompt,
}; };
setShowDeletePopup(false); setShowDeletePopup(false);
const response = await fetch("https://copilotorchestrater.azurewebsites.net/generateSQLQuery", { useQueryCopilot.getState().refreshCorrelationId();
const serverInfo = useNotebook.getState().notebookServerInfo;
const queryUri = userContext.features.enableCopilotPhoenixGateaway
? createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery")
: createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery");
const response = await fetch(queryUri, {
method: "POST", method: "POST",
headers: { headers: {
"content-type": "application/json", "content-type": "application/json",
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
}, },
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json(); const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
if (generateSQLQueryResponse?.sql) { if (response.status === 404) {
let query = `-- **Prompt:** ${userPrompt}\r\n`; setShouldAllocateContainer(true);
if (generateSQLQueryResponse.explanation) { }
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`; if (response.ok) {
if (generateSQLQueryResponse?.sql) {
let query = `-- **Prompt:** ${userPrompt}\r\n`;
if (generateSQLQueryResponse.explanation) {
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
}
query += generateSQLQueryResponse.sql;
setQuery(query);
setGeneratedQuery(generateSQLQueryResponse.sql);
setGeneratedQueryComments(generateSQLQueryResponse.explanation);
setShowErrorMessageBar(false);
} }
query += generateSQLQueryResponse.sql; } else {
setQuery(query); handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
setGeneratedQuery(generateSQLQueryResponse.sql); useTabs.getState().setIsQueryErrorThrown(true);
setShowErrorMessageBar(true);
} }
} catch (error) { } catch (error) {
handleError(error, "executeNaturalLanguageQuery"); handleError(error, "executeNaturalLanguageQuery");
@@ -200,15 +242,14 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
} }
}; };
const querySampleDocuments = (query: string, options: FeedOptions): QueryIterator<ItemDefinition & Resource> => {
options = getCommonQueryOptions(options);
return sampleDataClient()
.database(QueryCopilotSampleDatabaseId)
.container(QueryCopilotSampleContainerId)
.items.query(query, options);
};
const onExecuteQueryClick = async (): Promise<void> => { const onExecuteQueryClick = async (): Promise<void> => {
traceStart(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId,
userPrompt: userPrompt,
generatedQuery: generatedQuery,
generatedQueryComments: generatedQueryComments,
executedQuery: selectedQuery || query,
});
const queryToExecute = selectedQuery || query; const queryToExecute = selectedQuery || query;
const queryIterator = querySampleDocuments(queryToExecute, { const queryIterator = querySampleDocuments(queryToExecute, {
enableCrossPartitionQuery: shouldEnableCrossPartitionKey(), enableCrossPartitionQuery: shouldEnableCrossPartitionKey(),
@@ -233,8 +274,16 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
setQueryResults(queryResults); setQueryResults(queryResults);
setErrorMessage(""); setErrorMessage("");
setShowErrorMessageBar(false);
traceSuccess(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId,
});
} catch (error) { } catch (error) {
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId,
errorMessage: errorMessage,
});
setErrorMessage(errorMessage); setErrorMessage(errorMessage);
handleError(errorMessage, "executeQueryCopilotTab"); handleError(errorMessage, "executeQueryCopilotTab");
useTabs.getState().setIsQueryErrorThrown(true); useTabs.getState().setIsQueryErrorThrown(true);
@@ -254,6 +303,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
commandButtonLabel: executeQueryBtnLabel, commandButtonLabel: executeQueryBtnLabel,
ariaLabel: executeQueryBtnLabel, ariaLabel: executeQueryBtnLabel,
hasPopup: false, hasPopup: false,
disabled: query?.trim() === "",
}; };
const saveQueryBtn = { const saveQueryBtn = {
@@ -264,23 +314,26 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
commandButtonLabel: "Save Query", commandButtonLabel: "Save Query",
ariaLabel: "Save Query", ariaLabel: "Save Query",
hasPopup: false, hasPopup: false,
disabled: query?.trim() === "",
}; };
const samplePromptsBtn = { // Sample Prompts temporary disabled due current design
iconSrc: SamplePromptsIcon, // const samplePromptsBtn = {
iconAlt: "Sample Prompts", // iconSrc: SamplePromptsIcon,
onCommandClick: () => setIsSamplePromptsOpen(true), // iconAlt: "Sample Prompts",
commandButtonLabel: "Sample Prompts", // onCommandClick: () => setIsSamplePromptsOpen(true),
ariaLabel: "Sample Prompts", // commandButtonLabel: "Sample Prompts",
hasPopup: false, // ariaLabel: "Sample Prompts",
}; // hasPopup: false,
// };
return [executeQueryBtn, saveQueryBtn, samplePromptsBtn]; return [executeQueryBtn, saveQueryBtn];
}; };
const showTeachingBubble = (): void => { const showTeachingBubble = (): void => {
if (!inputEdited.current) { const shouldShowTeachingBubble = !inputEdited.current && userPrompt.trim() === "";
if (shouldShowTeachingBubble) {
setTimeout(() => { setTimeout(() => {
if (!inputEdited.current) { if (shouldShowTeachingBubble) {
toggleCopilotTeachingBubbleVisible(); toggleCopilotTeachingBubbleVisible();
inputEdited.current = true; inputEdited.current = true;
} }
@@ -294,282 +347,308 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
setShowCallout(false); setShowCallout(false);
}; };
const startGenerateQueryProcess = () => {
updateHistories();
generateSQLQuery();
resetButtonState();
};
React.useEffect(() => { React.useEffect(() => {
useCommandBar.getState().setContextButtons(getCommandbarButtons()); useCommandBar.getState().setContextButtons(getCommandbarButtons());
}, [query, selectedQuery]); }, [query, selectedQuery]);
React.useEffect(() => { React.useEffect(() => {
if (initialInput) {
generateSQLQuery();
}
showTeachingBubble(); showTeachingBubble();
useTabs.getState().setIsQueryErrorThrown(false); useTabs.getState().setIsQueryErrorThrown(false);
}, []); }, []);
return ( return (
<Stack className="tab-pane" style={{ padding: 24, width: "100%", height: "100%" }}> <Stack className="tab-pane" style={{ padding: 24, width: "100%" }}>
<Stack horizontal verticalAlign="center"> <div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
<Image src={CopilotIcon} /> <Stack horizontal verticalAlign="center">
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text> <Image src={CopilotIcon} />
</Stack> <Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%", position: "relative" }}> </Stack>
<TextField <Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%", position: "relative" }}>
id="naturalLanguageInput" <TextField
value={userPrompt} id="naturalLanguageInput"
onChange={handleUserPromptChange} value={userPrompt}
onClick={() => { onChange={handleUserPromptChange}
inputEdited.current = true; onClick={() => {
setShowSamplePrompts(true); inputEdited.current = true;
}} setShowSamplePrompts(true);
style={{ lineHeight: 30 }} }}
styles={{ root: { width: "95%" } }} onKeyDown={(e) => {
disabled={isGeneratingQuery} if (e.key === "Enter") {
autoComplete="off" startGenerateQueryProcess();
/> }
{copilotTeachingBubbleVisible && ( }}
<TeachingBubble style={{ lineHeight: 30 }}
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }} styles={{ root: { width: "95%" } }}
target="#naturalLanguageInput" disabled={isGeneratingQuery}
hasCloseButton={true} autoComplete="off"
closeButtonAriaLabel="Close" />
onDismiss={toggleCopilotTeachingBubbleVisible} {copilotTeachingBubbleVisible && (
hasSmallHeadline={true} <TeachingBubble
headline="Write a prompt" calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
> target="#naturalLanguageInput"
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "} hasCloseButton={true}
<Link closeButtonAriaLabel="Close"
onClick={() => { onDismiss={toggleCopilotTeachingBubbleVisible}
setShowSamplePrompts(true); hasSmallHeadline={true}
toggleCopilotTeachingBubbleVisible(); headline="Write a prompt"
}}
style={{ color: "white", fontWeight: 600 }}
> >
sample prompts Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
</Link>{" "} <Link
or write your own query onClick={() => {
</TeachingBubble> setShowSamplePrompts(true);
)} toggleCopilotTeachingBubbleVisible();
<IconButton
iconProps={{ iconName: "Send" }}
disabled={isGeneratingQuery || !userPrompt.trim()}
style={{ marginLeft: 8 }}
onClick={() => {
updateHistories();
generateSQLQuery();
resetButtonState();
}}
/>
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
{showSamplePrompts && (
<Callout
styles={{ root: { minWidth: 400 } }}
target="#naturalLanguageInput"
isBeakVisible={false}
onDismiss={() => setShowSamplePrompts(false)}
directionalHintFixed={true}
directionalHint={DirectionalHint.bottomLeftEdge}
alignTargetEdge={true}
gapSpace={4}
>
<Stack>
{filteredHistories?.length > 0 && (
<Stack>
<Text
style={{
width: "100%",
fontSize: 14,
fontWeight: 600,
color: "#0078D4",
marginLeft: 16,
padding: "4px 0",
}}
>
Recent
</Text>
{filteredHistories.map((history, i) => (
<DefaultButton
key={i}
onClick={() => {
setUserPrompt(history);
setShowSamplePrompts(false);
}}
onRenderIcon={() => <Image src={RecentIcon} />}
styles={promptStyles}
>
{history}
</DefaultButton>
))}
</Stack>
)}
<Text
style={{
width: "100%",
fontSize: 14,
fontWeight: 600,
color: "#0078D4",
marginLeft: 16,
padding: "4px 0",
}} }}
style={{ color: "white", fontWeight: 600 }}
> >
Suggested Prompts sample prompts
</Text> </Link>{" "}
{filteredSuggestedPrompts.map((prompt) => ( or write your own query
<DefaultButton </TeachingBubble>
key={prompt.id}
onClick={() => {
setUserPrompt(prompt.text);
setShowSamplePrompts(false);
}}
onRenderIcon={() => <Image src={HintIcon} />}
styles={promptStyles}
>
{prompt.text}
</DefaultButton>
))}
<Separator
styles={{
root: {
selectors: { "::before": { background: "#E1DFDD" } },
padding: 0,
},
}}
/>
<Text
style={{
width: "100%",
fontSize: 14,
marginLeft: 16,
padding: "4px 0",
}}
>
Learn about{" "}
<Link target="_blank" href="">
writing effective prompts
</Link>
</Text>
</Stack>
</Callout>
)}
</Stack>
<Text style={{ marginTop: 8, marginBottom: 24, fontSize: 12 }}>
AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "}
<Link href="" target="_blank">
Read preview terms
</Link>
{showErrorMessageBar && (
<Stack style={{ backgroundColor: "#FEF0F1", padding: "4px 8px" }} horizontal verticalAlign="center">
<Image src={XErrorMessage} style={{ marginRight: "8px" }} />
<Text style={{ fontSize: 12 }}>
We ran into an error and were not able to execute query. Please try again after sometime
</Text>
</Stack>
)}
</Text>
{showFeedbackBar && (
<Stack style={{ backgroundColor: "#FFF8F0", padding: "2px 8px" }} horizontal verticalAlign="center">
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
{showCallout && !hideFeedbackModalForLikedQueries && (
<Callout
style={{ padding: 8 }}
target="#likeBtn"
onDismiss={() => {
setShowCallout(false);
submitFeedback({ generatedQuery, likeQuery, description: "", userPrompt: userPrompt });
}}
directionalHint={DirectionalHint.topCenter}
>
<Text>
Thank you. Need to give{" "}
<Link
onClick={() => {
setShowCallout(false);
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
}}
>
more feedback?
</Link>
</Text>
</Callout>
)} )}
<IconButton <IconButton
id="likeBtn" iconProps={{ iconName: "Send" }}
style={{ marginLeft: 20 }} disabled={isGeneratingQuery || !userPrompt.trim()}
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }} style={{ marginLeft: 8 }}
onClick={() => { onClick={() => startGenerateQueryProcess()}
setShowCallout(!likeQuery);
setLikeQuery(!likeQuery);
if (dislikeQuery) {
setDislikeQuery(!dislikeQuery);
}
}}
/> />
<IconButton {isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
style={{ margin: "0 10px" }} {showSamplePrompts && (
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }} <Callout
onClick={() => { styles={{ root: { minWidth: 400 } }}
if (!dislikeQuery) { target="#naturalLanguageInput"
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt); isBeakVisible={false}
setLikeQuery(false); onDismiss={() => setShowSamplePrompts(false)}
} directionalHintFixed={true}
setDislikeQuery(!dislikeQuery); directionalHint={DirectionalHint.bottomLeftEdge}
setShowCallout(false); alignTargetEdge={true}
}} gapSpace={4}
/> >
<Separator vertical style={{ color: "#EDEBE9" }} /> <Stack>
<CommandBarButton {filteredHistories?.length > 0 && (
onClick={copyGeneratedCode} <Stack>
iconProps={{ iconName: "Copy" }} <Text
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }} style={{
> width: "100%",
Copy code fontSize: 14,
</CommandBarButton> fontWeight: 600,
<CommandBarButton color: "#0078D4",
onClick={() => { marginLeft: 16,
setShowDeletePopup(true); padding: "4px 0",
}} }}
iconProps={{ iconName: "Delete" }} >
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }} Recent
> </Text>
Delete code {filteredHistories.map((history, i) => (
</CommandBarButton> <DefaultButton
key={i}
onClick={() => {
setUserPrompt(history);
setShowSamplePrompts(false);
}}
onRenderIcon={() => <Image src={RecentIcon} />}
styles={promptStyles}
>
{history}
</DefaultButton>
))}
</Stack>
)}
{filteredSuggestedPrompts?.length > 0 && (
<Stack>
<Text
style={{
width: "100%",
fontSize: 14,
fontWeight: 600,
color: "#0078D4",
marginLeft: 16,
padding: "4px 0",
}}
>
Suggested Prompts
</Text>
{filteredSuggestedPrompts.map((prompt) => (
<DefaultButton
key={prompt.id}
onClick={() => {
setUserPrompt(prompt.text);
setShowSamplePrompts(false);
}}
onRenderIcon={() => <Image src={HintIcon} />}
styles={promptStyles}
>
{prompt.text}
</DefaultButton>
))}
</Stack>
)}
{(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && (
<Stack>
<Separator
styles={{
root: {
selectors: { "::before": { background: "#E1DFDD" } },
padding: 0,
},
}}
/>
<Text
style={{
width: "100%",
fontSize: 14,
marginLeft: 16,
padding: "4px 0",
}}
>
Learn about{" "}
<Link target="_blank" href="http://aka.ms/cdb-copilot-writing">
writing effective prompts
</Link>
</Text>
</Stack>
)}
</Stack>
</Callout>
)}
</Stack> </Stack>
)}
<Stack className="tabPaneContentContainer"> <Stack style={{ marginTop: 8, marginBottom: 24 }}>
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}> <Text style={{ fontSize: 12 }}>
<EditorReact AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "}
language={"sql"} <Link href="http://aka.ms/cdb-copilot-preview-terms" target="_blank">
content={query} Read preview terms
isReadOnly={false} </Link>
ariaLabel={"Editing Query"} {showErrorMessageBar && (
lineNumbers={"on"} <Stack style={{ backgroundColor: "#FEF0F1", padding: "4px 8px" }} horizontal verticalAlign="center">
onContentChanged={(newQuery: string) => setQuery(newQuery)} <Image src={XErrorMessage} style={{ marginRight: "8px" }} />
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)} <Text style={{ fontSize: 12 }}>
We ran into an error and were not able to execute query. Please try again after sometime
</Text>
</Stack>
)}
</Text>
</Stack>
{showFeedbackBar && (
<Stack style={{ backgroundColor: "#FFF8F0", padding: "2px 8px" }} horizontal verticalAlign="center">
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
{showCallout && !hideFeedbackModalForLikedQueries && (
<Callout
style={{ padding: 8 }}
target="#likeBtn"
onDismiss={() => {
setShowCallout(false);
SubmitFeedback({
params: {
generatedQuery: generatedQuery,
likeQuery: likeQuery,
description: "",
userPrompt: userPrompt,
},
explorer: explorer,
});
}}
directionalHint={DirectionalHint.topCenter}
>
<Text>
Thank you. Need to give{" "}
<Link
onClick={() => {
setShowCallout(false);
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
}}
>
more feedback?
</Link>
</Text>
</Callout>
)}
<IconButton
id="likeBtn"
style={{ marginLeft: 20 }}
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
onClick={() => {
setShowCallout(!likeQuery);
setLikeQuery(!likeQuery);
if (dislikeQuery) {
setDislikeQuery(!dislikeQuery);
}
}}
/>
<IconButton
style={{ margin: "0 10px" }}
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
onClick={() => {
if (!dislikeQuery) {
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt);
setLikeQuery(false);
}
setDislikeQuery(!dislikeQuery);
setShowCallout(false);
}}
/>
<Separator vertical style={{ color: "#EDEBE9" }} />
<CommandBarButton
onClick={copyGeneratedCode}
iconProps={{ iconName: "Copy" }}
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
>
Copy code
</CommandBarButton>
<CommandBarButton
onClick={() => {
setShowDeletePopup(true);
}}
iconProps={{ iconName: "Delete" }}
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
>
Delete code
</CommandBarButton>
</Stack>
)}
<Stack className="tabPaneContentContainer">
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
<EditorReact
language={"sql"}
content={query}
isReadOnly={false}
ariaLabel={"Editing Query"}
lineNumbers={"on"}
onContentChanged={(newQuery: string) => setQuery(newQuery)}
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
/>
<QueryResultSection
isMongoDB={false}
queryEditorContent={selectedQuery || query}
error={errorMessage}
queryResults={queryResults}
isExecuting={isExecuting}
executeQueryDocumentsPage={(firstItemIndex: number) =>
queryDocumentsPerPage(firstItemIndex, queryIterator)
}
/>
</SplitterLayout>
</Stack>
<WelcomeModal visible={localStorage.getItem("hideWelcomeModal") !== "true"} />
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
{query !== "" && query.trim().length !== 0 && (
<DeletePopup
showDeletePopup={showDeletePopup}
setShowDeletePopup={setShowDeletePopup}
setQuery={setQuery}
clearFeedback={resetButtonState}
showFeedbackBar={setShowFeedbackBar}
/> />
<QueryResultSection )}
isMongoDB={false} <CopyPopup showCopyPopup={showCopyPopup} setShowCopyPopup={setshowCopyPopup} />
queryEditorContent={selectedQuery || query} </div>
error={errorMessage}
queryResults={queryResults}
isExecuting={isExecuting}
executeQueryDocumentsPage={(firstItemIndex: number) => queryDocumentsPerPage(firstItemIndex, queryIterator)}
/>
</SplitterLayout>
</Stack>
<WelcomeModal visible={localStorage.getItem("hideWelcomeModal") !== "true"} />
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
{query !== "" && query.trim().length !== 0 && (
<DeletePopup
showDeletePopup={showDeletePopup}
setShowDeletePopup={setShowDeletePopup}
setQuery={setQuery}
clearFeedback={resetButtonState}
showFeedbackBar={setShowFeedbackBar}
/>
)}
<CopyPopup showCopyPopup={showCopyPopup} setShowCopyPopup={setshowCopyPopup} />
</Stack> </Stack>
); );
}; };

View File

@@ -0,0 +1,154 @@
import { FeedOptions } from "@azure/cosmos";
import { handleError } from "Common/ErrorHandlingUtils";
import { sampleDataClient } from "Common/SampleDataClient";
import * as commonUtils from "Common/dataAccess/queryDocuments";
import DocumentId from "Explorer/Tree/DocumentId";
import { querySampleDocuments, readSampleDocument } from "./QueryCopilotUtilities";
jest.mock("Explorer/Tree/DocumentId", () => {
return jest.fn().mockImplementation(() => {
return {
id: jest.fn(),
loadDocument: jest.fn(),
};
});
});
jest.mock("Utils/NotificationConsoleUtils", () => ({
logConsoleProgress: jest.fn().mockReturnValue((): void => undefined),
logConsoleError: jest.fn(),
}));
jest.mock("@azure/cosmos", () => ({
FeedOptions: jest.fn(),
QueryIterator: jest.fn(),
Constants: {
HttpHeaders: {},
},
}));
jest.mock("Common/ErrorHandlingUtils", () => ({
handleError: jest.fn(),
}));
jest.mock("Common/dataAccess/queryDocuments", () => ({
getCommonQueryOptions: jest.fn((options) => options),
}));
jest.mock("Common/SampleDataClient");
jest.mock("node-fetch");
jest.mock("Explorer/Explorer", () => {
class MockExplorer {
allocateContainer = jest.fn().mockResolvedValueOnce({});
}
return MockExplorer;
});
jest.mock("hooks/useQueryCopilot", () => {
const mockQueryCopilotStore = {
shouldAllocateContainer: true,
setShouldAllocateContainer: jest.fn(),
correlationId: "mocked-correlation-id",
};
return {
useQueryCopilot: jest.fn(() => mockQueryCopilotStore),
};
});
describe("QueryCopilotUtilities", () => {
beforeEach(() => jest.clearAllMocks());
describe("querySampleDocuments", () => {
(sampleDataClient as jest.Mock).mockReturnValue({
database: jest.fn().mockReturnValue({
container: jest.fn().mockReturnValue({
items: {
query: jest.fn().mockReturnValue([]),
},
}),
}),
});
it("calls getCommonQueryOptions with the provided options", () => {
const query = "sample query";
const options: FeedOptions = { maxItemCount: 10 };
querySampleDocuments(query, options);
expect(commonUtils.getCommonQueryOptions).toHaveBeenCalledWith(options);
});
it("returns the result of items.query method", () => {
const query = "sample query";
const options: FeedOptions = { maxItemCount: 10 };
const mockResult = [
{ id: 1, name: "Document 1" },
{ id: 2, name: "Document 2" },
];
// Mock the items.query method to return the mockResult
(sampleDataClient().database("CopilotSampleDb").container("SampleContainer").items
.query as jest.Mock).mockReturnValue(mockResult);
const result = querySampleDocuments(query, options);
expect(result).toEqual(mockResult);
});
});
describe("readSampleDocument", () => {
it("should call the read method with the correct parameters", async () => {
(sampleDataClient as jest.Mock).mockReturnValue({
database: jest.fn().mockReturnValue({
container: jest.fn().mockReturnValue({
item: jest.fn().mockReturnValue({
read: jest.fn().mockResolvedValue({
resource: {},
}),
}),
}),
}),
});
const documentId = new DocumentId(null, "DocumentId", []);
const expectedResponse = {};
const result = await readSampleDocument(documentId);
expect(sampleDataClient).toHaveBeenCalled();
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDb");
expect(sampleDataClient().database("CopilotSampleDb").container).toHaveBeenCalledWith("SampleContainer");
expect(
sampleDataClient().database("CopilotSampleDb").container("SampleContainer").item("DocumentId", undefined).read
).toHaveBeenCalled();
expect(result).toEqual(expectedResponse);
});
it("should handle an error and re-throw it", async () => {
(sampleDataClient as jest.Mock).mockReturnValue({
database: jest.fn().mockReturnValue({
container: jest.fn().mockReturnValue({
item: jest.fn().mockReturnValue({
read: jest.fn().mockRejectedValue(new Error("Mock error")),
}),
}),
}),
});
const errorMock = new Error("Mock error");
const documentId = new DocumentId(null, "DocumentId", []);
await expect(readSampleDocument(documentId)).rejects.toStrictEqual(errorMock);
expect(sampleDataClient).toHaveBeenCalled();
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDb");
expect(sampleDataClient().database("CopilotSampleDb").container).toHaveBeenCalledWith("SampleContainer");
expect(
sampleDataClient().database("CopilotSampleDb").container("SampleContainer").item("DocumentId", undefined).read
).toHaveBeenCalled();
expect(handleError).toHaveBeenCalledWith(errorMock, "ReadDocument", expect.any(String));
});
});
});

View File

@@ -1,36 +1,35 @@
import { QueryCopilotSampleContainerSchema } from "Common/Constants"; import { FeedOptions, Item, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils"; import { handleError } from "Common/ErrorHandlingUtils";
import { sampleDataClient } from "Common/SampleDataClient";
import { getPartitionKeyValue } from "Common/dataAccess/getPartitionKeyValue";
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
import DocumentId from "Explorer/Tree/DocumentId";
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
interface FeedbackParams { export const querySampleDocuments = (query: string, options: FeedOptions): QueryIterator<ItemDefinition & Resource> => {
likeQuery: boolean; options = getCommonQueryOptions(options);
generatedQuery: string; return sampleDataClient()
userPrompt: string; .database(QueryCopilotSampleDatabaseId)
description?: string; .container(QueryCopilotSampleContainerId)
contact?: string; .items.query(query, options);
} };
export const readSampleDocument = async (documentId: DocumentId): Promise<Item> => {
const clearMessage = logConsoleProgress(`Reading item ${documentId.id()}`);
export const submitFeedback = async (params: FeedbackParams): Promise<void> => {
try { try {
const { likeQuery, generatedQuery, userPrompt, description, contact } = params; const response = await sampleDataClient()
const payload = { .database(QueryCopilotSampleDatabaseId)
containerSchema: QueryCopilotSampleContainerSchema, .container(QueryCopilotSampleContainerId)
like: likeQuery ? "like" : "dislike", .item(documentId.id(), getPartitionKeyValue(documentId))
generatedSql: generatedQuery, .read();
userPrompt,
description: description || "",
contact: contact || "",
};
const response = await fetch("https://copilotorchestrater.azurewebsites.net/feedback", { return response?.resource;
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify(payload),
});
// eslint-disable-next-line no-console
console.log(response);
} catch (error) { } catch (error) {
handleError(error, "copilotSubmitFeedback"); handleError(error, "ReadDocument", `Failed to read item ${documentId.id()}`);
throw error;
} finally {
clearMessage();
} }
}; };

View File

@@ -1,26 +0,0 @@
import { shallow } from "enzyme";
import React from "react";
import { SamplePrompts, SamplePromptsProps } from "./SamplePrompts";
describe("Sample Prompts snapshot test", () => {
it("should render properly if isSamplePromptsOpen is true", () => {
const sampleProps: SamplePromptsProps = {
isSamplePromptsOpen: true,
setIsSamplePromptsOpen: () => undefined,
setTextBox: () => undefined,
};
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
expect(wrapper).toMatchSnapshot();
});
it("should render properly if isSamplePromptsOpen is false", () => {
const sampleProps: SamplePromptsProps = {
isSamplePromptsOpen: false,
setIsSamplePromptsOpen: () => undefined,
setTextBox: () => undefined,
};
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,151 @@
import { QueryCopilotSampleContainerSchema, ShortenedQueryCopilotSampleContainerSchema } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils";
import { createUri } from "Common/UrlUtility";
import Explorer from "Explorer/Explorer";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { userContext } from "UserContext";
jest.mock("@azure/cosmos", () => ({
Constants: {
HttpHeaders: {},
},
}));
jest.mock("Common/ErrorHandlingUtils", () => ({
handleError: jest.fn(),
}));
jest.mock("Common/SampleDataClient");
jest.mock("node-fetch");
jest.mock("Explorer/Explorer", () => {
class MockExplorer {
allocateContainer = jest.fn().mockResolvedValueOnce({});
}
return MockExplorer;
});
jest.mock("hooks/useQueryCopilot", () => {
const mockQueryCopilotStore = {
shouldAllocateContainer: true,
setShouldAllocateContainer: jest.fn(),
correlationId: "mocked-correlation-id",
};
return {
useQueryCopilot: jest.fn(() => mockQueryCopilotStore),
};
});
describe("Query Copilot Client", () => {
beforeEach(() => jest.clearAllMocks());
describe("submitFeedback", () => {
const payload = {
like: "like",
generatedSql: "GeneratedQuery",
userPrompt: "UserPrompt",
description: "Description",
contact: "Contact",
containerSchema: userContext.features.enableCopilotFullSchema
? QueryCopilotSampleContainerSchema
: ShortenedQueryCopilotSampleContainerSchema,
};
const mockStore = useNotebook.getState();
beforeEach(() => {
mockStore.notebookServerInfo = {
notebookServerEndpoint: "mocked-endpoint",
authToken: "mocked-token",
forwardingId: "mocked-forwarding-id",
};
});
const feedbackUri = userContext.features.enableCopilotPhoenixGateaway
? createUri(useNotebook.getState().notebookServerInfo.notebookServerEndpoint, "feedback")
: createUri("https://copilotorchestrater.azurewebsites.net/", "feedback");
it("should call fetch with the payload with like", async () => {
const mockFetch = jest.fn().mockResolvedValueOnce({});
globalThis.fetch = mockFetch;
await SubmitFeedback({
params: {
likeQuery: true,
generatedQuery: "GeneratedQuery",
userPrompt: "UserPrompt",
description: "Description",
contact: "Contact",
},
explorer: new Explorer(),
});
expect(mockFetch).toHaveBeenCalledWith(
feedbackUri,
expect.objectContaining({
headers: expect.objectContaining({
"x-ms-correlationid": "mocked-correlation-id",
}),
})
);
const actualBody = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(actualBody).toEqual(payload);
});
it("should call fetch with the payload with unlike and empty parameters", async () => {
payload.like = "dislike";
payload.description = "";
payload.contact = "";
const mockFetch = jest.fn().mockResolvedValueOnce({});
globalThis.fetch = mockFetch;
await SubmitFeedback({
params: {
likeQuery: false,
generatedQuery: "GeneratedQuery",
userPrompt: "UserPrompt",
description: undefined,
contact: undefined,
},
explorer: new Explorer(),
});
expect(mockFetch).toHaveBeenCalledWith(
feedbackUri,
expect.objectContaining({
method: "POST",
headers: {
"content-type": "application/json",
"x-ms-correlationid": "mocked-correlation-id",
},
})
);
const actualBody = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(actualBody).toEqual(payload);
});
it("should handle errors and call handleError", async () => {
globalThis.fetch = jest.fn().mockRejectedValueOnce(new Error("Mock error"));
await SubmitFeedback({
params: {
likeQuery: true,
generatedQuery: "GeneratedQuery",
userPrompt: "UserPrompt",
description: "Description",
contact: "Contact",
},
explorer: new Explorer(),
}).catch((error) => {
expect(error.message).toEqual("Mock error");
});
expect(handleError).toHaveBeenCalledWith(new Error("Mock error"), expect.any(String));
});
});
});

View File

@@ -0,0 +1,132 @@
import {
PoolIdType,
QueryCopilotSampleContainerSchema,
ShortenedQueryCopilotSampleContainerSchema,
} from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils";
import { createUri } from "Common/UrlUtility";
import Explorer from "Explorer/Explorer";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useTabs } from "hooks/useTabs";
export const SendQueryRequest = async ({
userPrompt,
explorer,
}: {
userPrompt: string;
explorer: Explorer;
}): Promise<void> => {
if (userPrompt.trim() !== "") {
useQueryCopilot.getState().setIsGeneratingQuery(true);
useTabs.getState().setIsTabExecuting(true);
useTabs.getState().setIsQueryErrorThrown(false);
useQueryCopilot
.getState()
.setChatMessages([...useQueryCopilot.getState().chatMessages, { source: 0, message: userPrompt }]);
try {
if (useQueryCopilot.getState().shouldAllocateContainer && userContext.features.enableCopilotPhoenixGateaway) {
await explorer.allocateContainer(PoolIdType.DefaultPoolId);
useQueryCopilot.getState().setShouldAllocateContainer(false);
}
useQueryCopilot.getState().refreshCorrelationId();
const serverInfo = useNotebook.getState().notebookServerInfo;
const queryUri = userContext.features.enableCopilotPhoenixGateaway
? createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery")
: createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery");
const payload = {
containerSchema: userContext.features.enableCopilotFullSchema
? QueryCopilotSampleContainerSchema
: ShortenedQueryCopilotSampleContainerSchema,
userPrompt: userPrompt,
};
const response = await fetch(queryUri, {
method: "POST",
headers: {
"content-type": "application/json",
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
},
body: JSON.stringify(payload),
});
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
if (response.status === 404) {
useQueryCopilot.getState().setShouldAllocateContainer(true);
}
if (response.ok) {
if (generateSQLQueryResponse?.sql) {
let query = `Here is a query which will help you with provided prompt.\r\n **Prompt:** ${userPrompt}`;
query += `\r\n${generateSQLQueryResponse.sql}`;
useQueryCopilot
.getState()
.setChatMessages([
...useQueryCopilot.getState().chatMessages,
{ source: 1, message: query, explanation: generateSQLQueryResponse.explanation },
]);
useQueryCopilot.getState().setGeneratedQuery(generateSQLQueryResponse.sql);
useQueryCopilot.getState().setGeneratedQueryComments(generateSQLQueryResponse.explanation);
}
} else {
handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
useTabs.getState().setIsQueryErrorThrown(true);
}
} catch (error) {
handleError(error, "executeNaturalLanguageQuery");
useTabs.getState().setIsQueryErrorThrown(true);
throw error;
} finally {
useQueryCopilot.getState().setUserPrompt("");
useQueryCopilot.getState().setIsGeneratingQuery(false);
useTabs.getState().setIsTabExecuting(false);
}
}
};
export const SubmitFeedback = async ({
params,
explorer,
}: {
params: FeedbackParams;
explorer: Explorer;
}): Promise<void> => {
try {
const { likeQuery, generatedQuery, userPrompt, description, contact } = params;
const { correlationId, shouldAllocateContainer, setShouldAllocateContainer } = useQueryCopilot();
const payload = {
containerSchema: userContext.features.enableCopilotFullSchema
? QueryCopilotSampleContainerSchema
: ShortenedQueryCopilotSampleContainerSchema,
like: likeQuery ? "like" : "dislike",
generatedSql: generatedQuery,
userPrompt,
description: description || "",
contact: contact || "",
};
if (shouldAllocateContainer && userContext.features.enableCopilotPhoenixGateaway) {
await explorer.allocateContainer(PoolIdType.DefaultPoolId);
setShouldAllocateContainer(false);
}
const serverInfo = useNotebook.getState().notebookServerInfo;
const feedbackUri = userContext.features.enableCopilotPhoenixGateaway
? createUri(serverInfo.notebookServerEndpoint, "feedback")
: createUri("https://copilotorchestrater.azurewebsites.net/", "feedback");
const response = await fetch(feedbackUri, {
method: "POST",
headers: {
"content-type": "application/json",
"x-ms-correlationid": correlationId,
},
body: JSON.stringify(payload),
});
if (response.status === 404) {
setShouldAllocateContainer(true);
}
} catch (error) {
handleError(error, "copilotSubmitFeedback");
}
};

View File

@@ -0,0 +1,32 @@
import Explorer from "Explorer/Explorer";
export interface GenerateSQLQueryResponse {
apiVersion: string;
sql: string;
explanation: string;
generateStart: string;
generateEnd: string;
}
enum MessageSource {
User,
AI,
}
export interface CopilotMessage {
source: MessageSource;
message: string;
explanation?: string;
}
export interface FeedbackParams {
likeQuery: boolean;
generatedQuery: string;
userPrompt: string;
description?: string;
contact?: string;
}
export interface QueryCopilotProps {
explorer: Explorer;
}

View File

@@ -0,0 +1,91 @@
import { DefaultButton, IconButton } from "@fluentui/react";
import { shallow } from "enzyme";
import React from "react";
import { SamplePrompts, SamplePromptsProps } from "./SamplePrompts";
describe("Sample Prompts snapshot test", () => {
const setTextBoxMock = jest.fn();
const setIsSamplePromptsOpenMock = jest.fn();
const sampleProps: SamplePromptsProps = {
isSamplePromptsOpen: true,
setIsSamplePromptsOpen: setIsSamplePromptsOpenMock,
setTextBox: setTextBoxMock,
};
beforeEach(() => jest.clearAllMocks());
it("should render properly if isSamplePromptsOpen is true", () => {
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
expect(wrapper).toMatchSnapshot();
});
it("should render properly if isSamplePromptsOpen is false", () => {
sampleProps.isSamplePromptsOpen = false;
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
expect(wrapper).toMatchSnapshot();
});
it("should call setTextBox and setIsSamplePromptsOpen(false) when a button is clicked", () => {
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
wrapper.find(DefaultButton).at(0).simulate("click");
expect(setTextBoxMock).toHaveBeenCalledWith("Show me products less than 100 dolars");
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
wrapper.find(DefaultButton).at(3).simulate("click");
expect(setTextBoxMock).toHaveBeenCalledWith(
"Write a query to return all records in this table created in the last thirty days"
);
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
});
it("should call setIsSamplePromptsOpen(false) when the close button is clicked", () => {
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
wrapper.find(IconButton).first().simulate("click");
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
});
it("should call setTextBox and setIsSamplePromptsOpen(false) when a simple prompt button is clicked", () => {
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
wrapper.find(DefaultButton).at(0).simulate("click");
expect(setTextBoxMock).toHaveBeenCalledWith("Show me products less than 100 dolars");
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
wrapper.find(DefaultButton).at(1).simulate("click");
expect(setTextBoxMock).toHaveBeenCalledWith("Show schema");
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
});
it("should call setTextBox and setIsSamplePromptsOpen(false) when an intermediate prompt button is clicked", () => {
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
wrapper.find(DefaultButton).at(2).simulate("click");
expect(setTextBoxMock).toHaveBeenCalledWith(
"Show items with a description that contains a number between 0 and 99 inclusive."
);
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
wrapper.find(DefaultButton).at(3).simulate("click");
expect(setTextBoxMock).toHaveBeenCalledWith(
"Write a query to return all records in this table created in the last thirty days"
);
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
});
it("should call setTextBox and setIsSamplePromptsOpen(false) when a complex prompt button is clicked", () => {
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
wrapper.find(DefaultButton).at(4).simulate("click");
expect(setTextBoxMock).toHaveBeenCalledWith("Show all the products that customer Bob has reviewed.");
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
wrapper.find(DefaultButton).at(5).simulate("click");
expect(setTextBoxMock).toHaveBeenCalledWith("Which computers are more than 300 dollars and less than 400 dollars?");
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
});
});

View File

@@ -1,8 +1,8 @@
import { DefaultButton, FontIcon, IconButton, Image, Modal, Stack, Text } from "@fluentui/react"; import { DefaultButton, FontIcon, IconButton, Image, Modal, Stack, Text } from "@fluentui/react";
import React, { Dispatch, SetStateAction } from "react"; import React, { Dispatch, SetStateAction } from "react";
import ComplexPrompts from "../../../../images/ComplexPrompts.svg"; import ComplexPrompts from "../../../../../images/ComplexPrompts.svg";
import IntermediatePrompts from "../../../../images/IntermediatePrompts.svg"; import IntermediatePrompts from "../../../../../images/IntermediatePrompts.svg";
import SimplePrompts from "../../../../images/SimplePrompts.svg"; import SimplePrompts from "../../../../../images/SimplePrompts.svg";
export interface SamplePromptsProps { export interface SamplePromptsProps {
isSamplePromptsOpen: boolean; isSamplePromptsOpen: boolean;

View File

@@ -0,0 +1,8 @@
@keyframes loadingAnimation {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}

View File

@@ -0,0 +1,62 @@
import { DefaultButton } from "@fluentui/react";
import { shallow } from "enzyme";
import React from "react";
import { RetrievingBubble } from "./RetrievingBubble";
const mockUseQueryCopilot = {
isGeneratingQuery: false,
setIsGeneratingQuery: jest.fn(),
isGeneratingExplanation: false,
setIsGeneratingExplanation: jest.fn(),
shouldIncludeInMessages: true,
setShouldIncludeInMessages: jest.fn(),
};
jest.mock("hooks/useQueryCopilot", () => ({
useQueryCopilot: jest.fn(() => mockUseQueryCopilot),
}));
describe("RetrievingBubble", () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseQueryCopilot.isGeneratingQuery = false;
mockUseQueryCopilot.isGeneratingExplanation = false;
mockUseQueryCopilot.setIsGeneratingQuery.mockClear();
mockUseQueryCopilot.setIsGeneratingExplanation.mockClear();
mockUseQueryCopilot.setShouldIncludeInMessages.mockClear();
});
it("should render properly when isGeneratingQuery is true", () => {
mockUseQueryCopilot.isGeneratingQuery = true;
const wrapper = shallow(<RetrievingBubble />);
expect(wrapper).toMatchSnapshot();
});
it("should render properly when isGeneratingExplanation is true", () => {
mockUseQueryCopilot.isGeneratingExplanation = true;
const wrapper = shallow(<RetrievingBubble />);
expect(wrapper).toMatchSnapshot();
});
it("when isGeneratingQuery is true clicking stop generating button invokes the correct callbacks", () => {
mockUseQueryCopilot.isGeneratingQuery = true;
const wrapper = shallow(<RetrievingBubble />);
wrapper.find(DefaultButton).at(0).simulate("click");
expect(mockUseQueryCopilot.setIsGeneratingQuery).toHaveBeenCalledWith(false);
expect(mockUseQueryCopilot.setIsGeneratingExplanation).toHaveBeenCalledTimes(0);
expect(mockUseQueryCopilot.setShouldIncludeInMessages).toHaveBeenCalledWith(false);
});
it("when isGeneratingExplanation is true clicking stop generating button invokes the correct callbacks", () => {
mockUseQueryCopilot.isGeneratingExplanation = true;
const wrapper = shallow(<RetrievingBubble />);
wrapper.find(DefaultButton).at(0).simulate("click");
expect(mockUseQueryCopilot.setIsGeneratingQuery).toHaveBeenCalledTimes(0);
expect(mockUseQueryCopilot.setIsGeneratingExplanation).toHaveBeenCalledWith(false);
expect(mockUseQueryCopilot.setShouldIncludeInMessages).toHaveBeenCalledWith(false);
});
});

View File

@@ -0,0 +1,98 @@
import { DefaultButton, Image, Stack, Text } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import StopGeneratingIcon from "../../../../../../images/StopGenerating.svg";
import "./RetrievingBubble.css";
export const RetrievingBubble = (): JSX.Element => {
const {
isGeneratingQuery,
setIsGeneratingQuery,
isGeneratingExplanation,
setIsGeneratingExplanation,
shouldIncludeInMessages,
setShouldIncludeInMessages,
} = useQueryCopilot();
const stopGenerating = () => {
if (isGeneratingQuery) {
setIsGeneratingQuery(false);
}
if (isGeneratingExplanation) {
setIsGeneratingExplanation(false);
}
if (shouldIncludeInMessages) {
setShouldIncludeInMessages(false);
}
};
const bubbleContent = (bubbleType: string) => {
return (
<Stack
horizontalAlign="end"
verticalAlign="end"
style={{
display: "flex",
alignItems: "center",
padding: "10px",
margin: "10px",
backgroundColor: "#FAFAFA",
borderRadius: "8px",
}}
>
<Text
style={{
width: "100%",
height: "46px",
backgroundColor: "white",
padding: "12px 16px 16px 16px",
gap: "12px",
borderRadius: "8px",
fontWeight: "bold",
}}
>
Retriveing {bubbleType}
</Text>
<div
style={{
width: "100%",
height: "4px",
backgroundColor: "#E6E6E6",
borderRadius: "4px",
overflow: "hidden",
}}
>
<div
style={{
width: "50%",
height: "100%",
backgroundColor: "#0078D4",
animation: "loadingAnimation 2s linear infinite",
}}
></div>
</div>
<Stack
horizontalAlign="center"
verticalAlign="center"
style={{ marginTop: "8px", gap: "8px", alignItems: "center" }}
>
<DefaultButton
onClick={stopGenerating}
styles={{ root: { border: "none", background: "none", padding: 0, color: "#424242" } }}
style={{ color: "#424242" }}
onRenderIcon={() => <Image src={StopGeneratingIcon} />}
>
Stop generating
</DefaultButton>
</Stack>
</Stack>
);
};
return (
<>
{isGeneratingQuery && bubbleContent("queries")}
{isGeneratingExplanation && bubbleContent("explanation")}
</>
);
};

View File

@@ -0,0 +1,183 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RetrievingBubble should render properly when isGeneratingExplanation is true 1`] = `
<Fragment>
<Stack
horizontalAlign="end"
style={
Object {
"alignItems": "center",
"backgroundColor": "#FAFAFA",
"borderRadius": "8px",
"display": "flex",
"margin": "10px",
"padding": "10px",
}
}
verticalAlign="end"
>
<Text
style={
Object {
"backgroundColor": "white",
"borderRadius": "8px",
"fontWeight": "bold",
"gap": "12px",
"height": "46px",
"padding": "12px 16px 16px 16px",
"width": "100%",
}
}
>
Retriveing
explanation
</Text>
<div
style={
Object {
"backgroundColor": "#E6E6E6",
"borderRadius": "4px",
"height": "4px",
"overflow": "hidden",
"width": "100%",
}
}
>
<div
style={
Object {
"animation": "loadingAnimation 2s linear infinite",
"backgroundColor": "#0078D4",
"height": "100%",
"width": "50%",
}
}
/>
</div>
<Stack
horizontalAlign="center"
style={
Object {
"alignItems": "center",
"gap": "8px",
"marginTop": "8px",
}
}
verticalAlign="center"
>
<CustomizedDefaultButton
onClick={[Function]}
onRenderIcon={[Function]}
style={
Object {
"color": "#424242",
}
}
styles={
Object {
"root": Object {
"background": "none",
"border": "none",
"color": "#424242",
"padding": 0,
},
}
}
>
Stop generating
</CustomizedDefaultButton>
</Stack>
</Stack>
</Fragment>
`;
exports[`RetrievingBubble should render properly when isGeneratingQuery is true 1`] = `
<Fragment>
<Stack
horizontalAlign="end"
style={
Object {
"alignItems": "center",
"backgroundColor": "#FAFAFA",
"borderRadius": "8px",
"display": "flex",
"margin": "10px",
"padding": "10px",
}
}
verticalAlign="end"
>
<Text
style={
Object {
"backgroundColor": "white",
"borderRadius": "8px",
"fontWeight": "bold",
"gap": "12px",
"height": "46px",
"padding": "12px 16px 16px 16px",
"width": "100%",
}
}
>
Retriveing
queries
</Text>
<div
style={
Object {
"backgroundColor": "#E6E6E6",
"borderRadius": "4px",
"height": "4px",
"overflow": "hidden",
"width": "100%",
}
}
>
<div
style={
Object {
"animation": "loadingAnimation 2s linear infinite",
"backgroundColor": "#0078D4",
"height": "100%",
"width": "50%",
}
}
/>
</div>
<Stack
horizontalAlign="center"
style={
Object {
"alignItems": "center",
"gap": "8px",
"marginTop": "8px",
}
}
verticalAlign="center"
>
<CustomizedDefaultButton
onClick={[Function]}
onRenderIcon={[Function]}
style={
Object {
"color": "#424242",
}
}
styles={
Object {
"root": Object {
"background": "none",
"border": "none",
"color": "#424242",
"padding": 0,
},
}
}
>
Stop generating
</CustomizedDefaultButton>
</Stack>
</Stack>
</Fragment>
`;

View File

@@ -0,0 +1,26 @@
import { Text } from "@fluentui/react";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import { SampleBubble } from "./SampleBubble";
describe("Sample Bubble snapshot test", () => {
it("should render", () => {
const wrapper = shallow(<SampleBubble />);
const sampleInputs = wrapper.find(Text);
expect(sampleInputs.length).toEqual(2);
expect(useQueryCopilot.getState().userPrompt).toEqual("");
expect(wrapper).toMatchSnapshot();
});
it("should render and be clicked", () => {
const wrapper = shallow(<SampleBubble />);
const firstSampleInput = wrapper.find(Text).first();
firstSampleInput.simulate("click", {}, "");
expect(useQueryCopilot.getState().userPrompt).toEqual(expect.any(String));
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,42 @@
import { Stack, Text } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
export const SampleBubble: React.FC = (): JSX.Element => {
const { setUserPrompt } = useQueryCopilot();
const sampleChatMessages: string[] = [
"Write a query to return last 10 records in the database",
'Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"',
];
return (
<Stack
horizontalAlign="end"
verticalAlign="end"
style={{
display: "flex",
alignItems: "center",
padding: "10px",
margin: "10px",
}}
>
{sampleChatMessages.map((text, index) => (
<Text
key={index}
onClick={() => setUserPrompt(text)}
style={{
cursor: "pointer",
border: "1.5px solid #B0BEFF",
width: "100%",
padding: "2px",
borderRadius: "4px",
marginBottom: "5px",
}}
>
{text}
</Text>
))}
</Stack>
);
};

View File

@@ -0,0 +1,97 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Sample Bubble snapshot test should render 1`] = `
<Stack
horizontalAlign="end"
style={
Object {
"alignItems": "center",
"display": "flex",
"margin": "10px",
"padding": "10px",
}
}
verticalAlign="end"
>
<Text
key="0"
onClick={[Function]}
style={
Object {
"border": "1.5px solid #B0BEFF",
"borderRadius": "4px",
"cursor": "pointer",
"marginBottom": "5px",
"padding": "2px",
"width": "100%",
}
}
>
Write a query to return last 10 records in the database
</Text>
<Text
key="1"
onClick={[Function]}
style={
Object {
"border": "1.5px solid #B0BEFF",
"borderRadius": "4px",
"cursor": "pointer",
"marginBottom": "5px",
"padding": "2px",
"width": "100%",
}
}
>
Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"
</Text>
</Stack>
`;
exports[`Sample Bubble snapshot test should render and be clicked 1`] = `
<Stack
horizontalAlign="end"
style={
Object {
"alignItems": "center",
"display": "flex",
"margin": "10px",
"padding": "10px",
}
}
verticalAlign="end"
>
<Text
key="0"
onClick={[Function]}
style={
Object {
"border": "1.5px solid #B0BEFF",
"borderRadius": "4px",
"cursor": "pointer",
"marginBottom": "5px",
"padding": "2px",
"width": "100%",
}
}
>
Write a query to return last 10 records in the database
</Text>
<Text
key="1"
onClick={[Function]}
style={
Object {
"border": "1.5px solid #B0BEFF",
"borderRadius": "4px",
"cursor": "pointer",
"marginBottom": "5px",
"padding": "2px",
"width": "100%",
}
}
>
Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"
</Text>
</Stack>
`;

View File

@@ -0,0 +1,13 @@
import { shallow } from "enzyme";
import React from "react";
import { WelcomeBubble } from "./WelcomeBubble";
const mockedDate = new Date(2023, 7, 15);
jest.spyOn(global, "Date").mockImplementation(() => (mockedDate as unknown) as string);
describe("Welcome Bubble snapshot test", () => {
it("should render ", () => {
const wrapper = shallow(<WelcomeBubble />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,52 @@
import { Stack, Text } from "@fluentui/react";
import React from "react";
export const WelcomeBubble: React.FC = (): JSX.Element => {
return (
<Stack>
<Stack horizontalAlign="center" style={{ color: "#707070" }} tokens={{ padding: 8, childrenGap: 8 }}>
{new Date().toLocaleDateString("en-US", {
month: "long",
day: "numeric",
})}{" "}
{new Date().toLocaleTimeString("en-US", {
hour: "numeric",
minute: "numeric",
hour12: true,
})}
</Stack>
<Stack
tokens={{ padding: 16, childrenGap: 12 }}
style={{
backgroundColor: "white",
margin: "5px 10px",
borderRadius: "8px",
}}
>
<Text variant="medium">
Hello, I am Cosmos Db&apos;s copilot assistant. I can help you do the following things:
</Text>
<Stack tokens={{ childrenGap: 8 }}>
<Stack horizontal style={{ marginLeft: "15px" }} tokens={{ childrenGap: 8 }}>
<Text style={{ fontSize: "16px", lineHeight: "16px", verticalAlign: "middle" }}></Text>
<Text style={{ verticalAlign: "middle" }}>Generate queries based upon prompt you suggest</Text>
</Stack>
<Stack horizontal style={{ marginLeft: "15px" }} tokens={{ childrenGap: 8 }}>
<Text style={{ fontSize: "16px", lineHeight: "16px", verticalAlign: "middle" }}></Text>
<Text style={{ verticalAlign: "middle" }}>
Explain and provide alternate queries for a query suggested by you
</Text>
</Stack>
<Stack horizontal style={{ marginLeft: "15px" }} tokens={{ childrenGap: 8 }}>
<Text style={{ fontSize: "16px", lineHeight: "16px", verticalAlign: "middle" }}></Text>
<Text style={{ verticalAlign: "middle" }}>Help answer questions about Cosmos DB</Text>
</Stack>
</Stack>
<Text variant="medium">
To get started, ask me a question or use one of the sample prompts to generate a query. AI-generated content
may be incorrect.
</Text>
</Stack>
</Stack>
);
};

View File

@@ -0,0 +1,160 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Welcome Bubble snapshot test should render 1`] = `
<Stack>
<Stack
horizontalAlign="center"
style={
Object {
"color": "#707070",
}
}
tokens={
Object {
"childrenGap": 8,
"padding": 8,
}
}
>
August 15
12:00 AM
</Stack>
<Stack
style={
Object {
"backgroundColor": "white",
"borderRadius": "8px",
"margin": "5px 10px",
}
}
tokens={
Object {
"childrenGap": 12,
"padding": 16,
}
}
>
<Text
variant="medium"
>
Hello, I am Cosmos Db's copilot assistant. I can help you do the following things:
</Text>
<Stack
tokens={
Object {
"childrenGap": 8,
}
}
>
<Stack
horizontal={true}
style={
Object {
"marginLeft": "15px",
}
}
tokens={
Object {
"childrenGap": 8,
}
}
>
<Text
style={
Object {
"fontSize": "16px",
"lineHeight": "16px",
"verticalAlign": "middle",
}
}
>
</Text>
<Text
style={
Object {
"verticalAlign": "middle",
}
}
>
Generate queries based upon prompt you suggest
</Text>
</Stack>
<Stack
horizontal={true}
style={
Object {
"marginLeft": "15px",
}
}
tokens={
Object {
"childrenGap": 8,
}
}
>
<Text
style={
Object {
"fontSize": "16px",
"lineHeight": "16px",
"verticalAlign": "middle",
}
}
>
</Text>
<Text
style={
Object {
"verticalAlign": "middle",
}
}
>
Explain and provide alternate queries for a query suggested by you
</Text>
</Stack>
<Stack
horizontal={true}
style={
Object {
"marginLeft": "15px",
}
}
tokens={
Object {
"childrenGap": 8,
}
}
>
<Text
style={
Object {
"fontSize": "16px",
"lineHeight": "16px",
"verticalAlign": "middle",
}
}
>
</Text>
<Text
style={
Object {
"verticalAlign": "middle",
}
}
>
Help answer questions about Cosmos DB
</Text>
</Stack>
</Stack>
<Text
variant="medium"
>
To get started, ask me a question or use one of the sample prompts to generate a query. AI-generated content may be incorrect.
</Text>
</Stack>
</Stack>
`;

View File

@@ -0,0 +1,112 @@
import { IconButton, Image, TextField } from "@fluentui/react";
import Explorer from "Explorer/Explorer";
import { SendQueryRequest } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import { act } from "react-dom/test-utils";
import { Footer } from "./Footer";
jest.mock("@azure/cosmos", () => ({
Constants: {
HttpHeaders: {},
},
}));
jest.mock("Common/SampleDataClient");
jest.mock("Explorer/Explorer");
jest.mock("node-fetch");
jest.mock("Common/ErrorHandlingUtils", () => ({
handleError: jest.fn(),
getErrorMessage: jest.fn(),
}));
jest.mock("Explorer/QueryCopilot/Shared/QueryCopilotClient");
describe("Footer snapshot test", () => {
const testMessage = "test message";
const initialStoreState = useQueryCopilot.getState();
beforeEach(() => {
useQueryCopilot.setState(initialStoreState, true);
});
it("should open sample prompts on button click", () => {
const wrapper = shallow(<Footer explorer={new Explorer()} />);
const samplePromptsImage = wrapper.find(Image).first();
samplePromptsImage.simulate("click", {});
expect(useQueryCopilot.getState().isSamplePromptsOpen).toBeTruthy();
expect(wrapper).toMatchSnapshot();
});
it("should update user input", () => {
const wrapper = shallow(<Footer explorer={new Explorer()} />);
const newInput = "some new input";
const textInput = wrapper.find(TextField).first();
textInput.simulate("change", {}, newInput);
expect(useQueryCopilot.getState().userPrompt).toEqual(newInput);
expect(wrapper).toMatchSnapshot();
});
it("should pass text with enter key", async () => {
useQueryCopilot.getState().setUserPrompt(testMessage);
const wrapper = shallow(<Footer explorer={new Explorer()} />);
const textInput = wrapper.find(TextField).first();
await act(async () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
textInput.simulate("keydown", { key: "Enter", shiftKey: false, preventDefault: () => {} });
});
await Promise.resolve();
expect(SendQueryRequest).toHaveBeenCalledWith({ userPrompt: testMessage, explorer: expect.any(Explorer) });
expect(wrapper).toMatchSnapshot();
});
it("should not pass text with non enter key", () => {
const wrapper = shallow(<Footer explorer={new Explorer()} />);
const textInput = wrapper.find(TextField).first();
// eslint-disable-next-line @typescript-eslint/no-empty-function
textInput.simulate("keydown", { key: "K", shiftKey: false, preventDefault: () => {} });
expect(useQueryCopilot.getState().chatMessages).toEqual([]);
expect(useQueryCopilot.getState().userPrompt).toEqual("");
expect(wrapper).toMatchSnapshot();
});
it("should not pass if no text", () => {
const wrapper = shallow(<Footer explorer={new Explorer()} />);
const textInput = wrapper.find(TextField).first();
// eslint-disable-next-line @typescript-eslint/no-empty-function
textInput.simulate("keydown", { key: "Enter", shiftKey: false, preventDefault: () => {} });
expect(useQueryCopilot.getState().chatMessages).toEqual([]);
expect(useQueryCopilot.getState().userPrompt).toEqual("");
expect(wrapper).toMatchSnapshot();
});
it("should pass text with icon button", async () => {
useQueryCopilot.getState().setUserPrompt(testMessage);
const wrapper = shallow(<Footer explorer={new Explorer()} />);
const iconButton = wrapper.find(IconButton).first();
await act(async () => {
iconButton.simulate("click", {});
});
await Promise.resolve();
expect(SendQueryRequest).toHaveBeenCalledWith({ userPrompt: testMessage, explorer: expect.any(Explorer) });
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,82 @@
import { IButtonStyles, IconButton, Image, Stack, TextField } from "@fluentui/react";
import { SendQueryRequest } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import HintIcon from "../../../../../images/Hint.svg";
import { SamplePrompts, SamplePromptsProps } from "../../Shared/SamplePrompts/SamplePrompts";
export const Footer: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
const {
userPrompt,
setUserPrompt,
isSamplePromptsOpen,
setIsSamplePromptsOpen,
isGeneratingQuery,
} = useQueryCopilot();
const promptStyles: IButtonStyles = {
root: { border: "5px", selectors: { ":hover": { outline: "1px dashed #605e5c" } } },
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
};
const sampleProps: SamplePromptsProps = {
isSamplePromptsOpen: isSamplePromptsOpen,
setIsSamplePromptsOpen: setIsSamplePromptsOpen,
setTextBox: setUserPrompt,
};
const handleEnterKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
startSentMessageProcess();
}
};
const startSentMessageProcess = async () => {
await SendQueryRequest({ userPrompt, explorer });
};
return (
<Stack
horizontal
horizontalAlign="end"
verticalAlign="end"
style={{
display: "flex",
alignItems: "center",
borderRadius: "20px",
background: "white",
padding: "5px",
margin: "5px",
}}
>
<Stack>
<Image src={HintIcon} styles={promptStyles} onClick={() => setIsSamplePromptsOpen(true)} />
<SamplePrompts sampleProps={sampleProps} />
</Stack>
<TextField
placeholder="Write your own prompt or ask a question"
value={userPrompt}
onChange={(_, newValue) => setUserPrompt(newValue)}
onKeyDown={handleEnterKeyPress}
multiline
resizable={false}
disabled={isGeneratingQuery}
styles={{
root: {
width: "100%",
height: "80px",
borderRadius: "20px",
padding: "8px",
border: "none",
outline: "none",
marginLeft: "10px",
},
fieldGroup: { border: "none" },
}}
/>
<IconButton disabled={isGeneratingQuery} iconProps={{ iconName: "Send" }} onClick={startSentMessageProcess} />
</Stack>
);
};

View File

@@ -0,0 +1,511 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Footer snapshot test should not pass if no text 1`] = `
<Stack
horizontal={true}
horizontalAlign="end"
style={
Object {
"alignItems": "center",
"background": "white",
"borderRadius": "20px",
"display": "flex",
"margin": "5px",
"padding": "5px",
}
}
verticalAlign="end"
>
<Stack>
<Image
onClick={[Function]}
src=""
styles={
Object {
"label": Object {
"fontWeight": 400,
"paddingLeft": 8,
"textAlign": "left",
},
"root": Object {
"border": "5px",
"selectors": Object {
":hover": Object {
"outline": "1px dashed #605e5c",
},
},
},
}
}
/>
<SamplePrompts
sampleProps={
Object {
"isSamplePromptsOpen": false,
"setIsSamplePromptsOpen": [Function],
"setTextBox": [Function],
}
}
/>
</Stack>
<StyledTextFieldBase
disabled={false}
multiline={true}
onChange={[Function]}
onKeyDown={[Function]}
placeholder="Write your own prompt or ask a question"
resizable={false}
styles={
Object {
"fieldGroup": Object {
"border": "none",
},
"root": Object {
"border": "none",
"borderRadius": "20px",
"height": "80px",
"marginLeft": "10px",
"outline": "none",
"padding": "8px",
"width": "100%",
},
}
}
value=""
/>
<CustomizedIconButton
disabled={false}
iconProps={
Object {
"iconName": "Send",
}
}
onClick={[Function]}
/>
</Stack>
`;
exports[`Footer snapshot test should not pass text with non enter key 1`] = `
<Stack
horizontal={true}
horizontalAlign="end"
style={
Object {
"alignItems": "center",
"background": "white",
"borderRadius": "20px",
"display": "flex",
"margin": "5px",
"padding": "5px",
}
}
verticalAlign="end"
>
<Stack>
<Image
onClick={[Function]}
src=""
styles={
Object {
"label": Object {
"fontWeight": 400,
"paddingLeft": 8,
"textAlign": "left",
},
"root": Object {
"border": "5px",
"selectors": Object {
":hover": Object {
"outline": "1px dashed #605e5c",
},
},
},
}
}
/>
<SamplePrompts
sampleProps={
Object {
"isSamplePromptsOpen": false,
"setIsSamplePromptsOpen": [Function],
"setTextBox": [Function],
}
}
/>
</Stack>
<StyledTextFieldBase
disabled={false}
multiline={true}
onChange={[Function]}
onKeyDown={[Function]}
placeholder="Write your own prompt or ask a question"
resizable={false}
styles={
Object {
"fieldGroup": Object {
"border": "none",
},
"root": Object {
"border": "none",
"borderRadius": "20px",
"height": "80px",
"marginLeft": "10px",
"outline": "none",
"padding": "8px",
"width": "100%",
},
}
}
value=""
/>
<CustomizedIconButton
disabled={false}
iconProps={
Object {
"iconName": "Send",
}
}
onClick={[Function]}
/>
</Stack>
`;
exports[`Footer snapshot test should open sample prompts on button click 1`] = `
<Stack
horizontal={true}
horizontalAlign="end"
style={
Object {
"alignItems": "center",
"background": "white",
"borderRadius": "20px",
"display": "flex",
"margin": "5px",
"padding": "5px",
}
}
verticalAlign="end"
>
<Stack>
<Image
onClick={[Function]}
src=""
styles={
Object {
"label": Object {
"fontWeight": 400,
"paddingLeft": 8,
"textAlign": "left",
},
"root": Object {
"border": "5px",
"selectors": Object {
":hover": Object {
"outline": "1px dashed #605e5c",
},
},
},
}
}
/>
<SamplePrompts
sampleProps={
Object {
"isSamplePromptsOpen": false,
"setIsSamplePromptsOpen": [Function],
"setTextBox": [Function],
}
}
/>
</Stack>
<StyledTextFieldBase
disabled={false}
multiline={true}
onChange={[Function]}
onKeyDown={[Function]}
placeholder="Write your own prompt or ask a question"
resizable={false}
styles={
Object {
"fieldGroup": Object {
"border": "none",
},
"root": Object {
"border": "none",
"borderRadius": "20px",
"height": "80px",
"marginLeft": "10px",
"outline": "none",
"padding": "8px",
"width": "100%",
},
}
}
value=""
/>
<CustomizedIconButton
disabled={false}
iconProps={
Object {
"iconName": "Send",
}
}
onClick={[Function]}
/>
</Stack>
`;
exports[`Footer snapshot test should pass text with enter key 1`] = `
<Stack
horizontal={true}
horizontalAlign="end"
style={
Object {
"alignItems": "center",
"background": "white",
"borderRadius": "20px",
"display": "flex",
"margin": "5px",
"padding": "5px",
}
}
verticalAlign="end"
>
<Stack>
<Image
onClick={[Function]}
src=""
styles={
Object {
"label": Object {
"fontWeight": 400,
"paddingLeft": 8,
"textAlign": "left",
},
"root": Object {
"border": "5px",
"selectors": Object {
":hover": Object {
"outline": "1px dashed #605e5c",
},
},
},
}
}
/>
<SamplePrompts
sampleProps={
Object {
"isSamplePromptsOpen": false,
"setIsSamplePromptsOpen": [Function],
"setTextBox": [Function],
}
}
/>
</Stack>
<StyledTextFieldBase
disabled={false}
multiline={true}
onChange={[Function]}
onKeyDown={[Function]}
placeholder="Write your own prompt or ask a question"
resizable={false}
styles={
Object {
"fieldGroup": Object {
"border": "none",
},
"root": Object {
"border": "none",
"borderRadius": "20px",
"height": "80px",
"marginLeft": "10px",
"outline": "none",
"padding": "8px",
"width": "100%",
},
}
}
value="test message"
/>
<CustomizedIconButton
disabled={false}
iconProps={
Object {
"iconName": "Send",
}
}
onClick={[Function]}
/>
</Stack>
`;
exports[`Footer snapshot test should pass text with icon button 1`] = `
<Stack
horizontal={true}
horizontalAlign="end"
style={
Object {
"alignItems": "center",
"background": "white",
"borderRadius": "20px",
"display": "flex",
"margin": "5px",
"padding": "5px",
}
}
verticalAlign="end"
>
<Stack>
<Image
onClick={[Function]}
src=""
styles={
Object {
"label": Object {
"fontWeight": 400,
"paddingLeft": 8,
"textAlign": "left",
},
"root": Object {
"border": "5px",
"selectors": Object {
":hover": Object {
"outline": "1px dashed #605e5c",
},
},
},
}
}
/>
<SamplePrompts
sampleProps={
Object {
"isSamplePromptsOpen": false,
"setIsSamplePromptsOpen": [Function],
"setTextBox": [Function],
}
}
/>
</Stack>
<StyledTextFieldBase
disabled={false}
multiline={true}
onChange={[Function]}
onKeyDown={[Function]}
placeholder="Write your own prompt or ask a question"
resizable={false}
styles={
Object {
"fieldGroup": Object {
"border": "none",
},
"root": Object {
"border": "none",
"borderRadius": "20px",
"height": "80px",
"marginLeft": "10px",
"outline": "none",
"padding": "8px",
"width": "100%",
},
}
}
value="test message"
/>
<CustomizedIconButton
disabled={false}
iconProps={
Object {
"iconName": "Send",
}
}
onClick={[Function]}
/>
</Stack>
`;
exports[`Footer snapshot test should update user input 1`] = `
<Stack
horizontal={true}
horizontalAlign="end"
style={
Object {
"alignItems": "center",
"background": "white",
"borderRadius": "20px",
"display": "flex",
"margin": "5px",
"padding": "5px",
}
}
verticalAlign="end"
>
<Stack>
<Image
onClick={[Function]}
src=""
styles={
Object {
"label": Object {
"fontWeight": 400,
"paddingLeft": 8,
"textAlign": "left",
},
"root": Object {
"border": "5px",
"selectors": Object {
":hover": Object {
"outline": "1px dashed #605e5c",
},
},
},
}
}
/>
<SamplePrompts
sampleProps={
Object {
"isSamplePromptsOpen": false,
"setIsSamplePromptsOpen": [Function],
"setTextBox": [Function],
}
}
/>
</Stack>
<StyledTextFieldBase
disabled={false}
multiline={true}
onChange={[Function]}
onKeyDown={[Function]}
placeholder="Write your own prompt or ask a question"
resizable={false}
styles={
Object {
"fieldGroup": Object {
"border": "none",
},
"root": Object {
"border": "none",
"borderRadius": "20px",
"height": "80px",
"marginLeft": "10px",
"outline": "none",
"padding": "8px",
"width": "100%",
},
}
}
value=""
/>
<CustomizedIconButton
disabled={false}
iconProps={
Object {
"iconName": "Send",
}
}
onClick={[Function]}
/>
</Stack>
`;

View File

@@ -0,0 +1,17 @@
import { IconButton } from "@fluentui/react";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import { Header } from "./Header";
describe("Header snapshot test", () => {
it("should close on button click ", () => {
const wrapper = shallow(<Header />);
const iconButton = wrapper.find(IconButton).first();
iconButton.simulate("click", {});
expect(useQueryCopilot.getState().showCopilotSidebar).toBeFalsy();
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,39 @@
import { IconButton, Image, Stack, Text } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import CopilotIcon from "../../../../../images/CopilotSidebarLogo.svg";
export const Header: React.FC = (): JSX.Element => {
const { setShowCopilotSidebar } = useQueryCopilot();
return (
<Stack
style={{ margin: "15px 0px 0px 0px", padding: "5px", display: "flex", justifyContent: "space-between" }}
horizontal
verticalAlign="center"
>
<Stack horizontal verticalAlign="center">
<Image src={CopilotIcon} />
<Text style={{ marginLeft: "5px", fontWeight: "bold" }}>Copilot</Text>
<Text
style={{
background: "#f0f0f0",
fontSize: "10px",
padding: "2px 4px",
marginLeft: "5px",
borderRadius: "8px",
}}
>
Preview
</Text>
</Stack>
<IconButton
onClick={() => setShowCopilotSidebar(false)}
iconProps={{ iconName: "Cancel" }}
title="Exit"
ariaLabel="Exit"
style={{ color: "#424242", verticalAlign: "middle" }}
/>
</Stack>
);
};

View File

@@ -0,0 +1,64 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header snapshot test should close on button click 1`] = `
<Stack
horizontal={true}
style={
Object {
"display": "flex",
"justifyContent": "space-between",
"margin": "15px 0px 0px 0px",
"padding": "5px",
}
}
verticalAlign="center"
>
<Stack
horizontal={true}
verticalAlign="center"
>
<Image
src=""
/>
<Text
style={
Object {
"fontWeight": "bold",
"marginLeft": "5px",
}
}
>
Copilot
</Text>
<Text
style={
Object {
"background": "#f0f0f0",
"borderRadius": "8px",
"fontSize": "10px",
"marginLeft": "5px",
"padding": "2px 4px",
}
}
>
Preview
</Text>
</Stack>
<CustomizedIconButton
ariaLabel="Exit"
iconProps={
Object {
"iconName": "Cancel",
}
}
onClick={[Function]}
style={
Object {
"color": "#424242",
"verticalAlign": "middle",
}
}
title="Exit"
/>
</Stack>
`;

View File

@@ -0,0 +1,32 @@
import { PrimaryButton } from "@fluentui/react";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { withHooks } from "jest-react-hooks-shallow";
import React from "react";
import { WelcomeSidebarModal } from "./WelcomeSidebarModal";
describe("Welcome Sidebar Modal snapshot test", () => {
it("should close on button click ", () => {
withHooks(() => {
const wrapper = shallow(<WelcomeSidebarModal />);
const spy = jest.spyOn(localStorage, "setItem");
spy.mockClear();
const button = wrapper.find(PrimaryButton).first();
button.simulate("click", {});
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenLastCalledWith("showWelcomeSidebar", "false");
expect(useQueryCopilot.getState().showWelcomeSidebar).toBeFalsy();
expect(wrapper).toMatchSnapshot();
});
});
it("should not reneder with local storage key", () => {
withHooks(() => {
window.localStorage.setItem("showWelcomeSidebar", "false");
const wrapper = shallow(<WelcomeSidebarModal />);
expect(wrapper).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,130 @@
import { Image, Link, PrimaryButton, Stack, Text } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import Database from "../../../../../images/CopilotDatabase.svg";
import Flash from "../../../../../images/CopilotFlash.svg";
import CopilotSidebarWelcomeIllustration from "../../../../../images/CopilotSidebarWelcomeIllustration.svg";
import Thumb from "../../../../../images/CopilotThumb.svg";
export const WelcomeSidebarModal: React.FC = (): JSX.Element => {
const { showWelcomeSidebar, setShowWelcomeSidebar } = useQueryCopilot();
const hideModal = (): void => {
setShowWelcomeSidebar(false);
window.localStorage.setItem("showWelcomeSidebar", "false");
};
React.useEffect(() => {
const showWelcomeSidebar = window.localStorage.getItem("showWelcomeSidebar");
setShowWelcomeSidebar(showWelcomeSidebar && showWelcomeSidebar === "false" ? false : true);
}, []);
return (
showWelcomeSidebar && (
<Stack
style={{
width: "100%",
height: "100%",
overflow: "auto",
backgroundColor: "#FAFAFA",
flex: "1 0 100%",
}}
>
<div
style={{
margin: "20px 10px",
padding: "20px",
maxHeight: "100%",
boxSizing: "border-box",
borderRadius: "20px",
backgroundColor: "white",
}}
>
<Stack horizontalAlign="center" verticalAlign="center">
<Image src={CopilotSidebarWelcomeIllustration} />
</Stack>
<Stack>
<Stack.Item align="center" style={{ marginBottom: "10px" }}>
<Text className="title bold">Welcome to Copilot in CosmosDB</Text>
</Stack.Item>
<Stack.Item style={{ marginBottom: "15px" }}>
<Stack>
<Stack horizontal>
<Stack.Item align="start">
<Image src={Flash} />
</Stack.Item>
<Stack.Item align="center" style={{ marginLeft: "10px" }}>
<Text style={{ fontWeight: 600 }}>
Let copilot do the work for you
<br />
</Text>
</Stack.Item>
</Stack>
<Stack.Item style={{ textAlign: "start", marginLeft: "25px" }}>
<Text>
Ask Copilot to generate a query by describing the query in your words.
<br />
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
</Text>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item style={{ marginBottom: "15px" }}>
<Stack>
<Stack horizontal>
<Stack.Item align="start">
<Image src={Thumb} />
</Stack.Item>
<Stack.Item align="center" style={{ marginLeft: "10px" }}>
<Text style={{ fontWeight: 600 }}>
Use your judgement
<br />
</Text>
</Stack.Item>
</Stack>
<Stack.Item style={{ textAlign: "start", marginLeft: "25px" }}>
<Text>
AI-generated content can have mistakes. Make sure its accurate and appropriate before using it.
<br />
<Link href="http://aka.ms/cdb-copilot-preview-terms">Read preview terms</Link>
</Text>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item style={{ marginBottom: "15px" }}>
<Stack>
<Stack horizontal>
<Stack.Item align="start">
<Image src={Database} />
</Stack.Item>
<Stack.Item align="center" style={{ marginLeft: "10px" }}>
<Text style={{ fontWeight: 600 }}>
Copilot currently works only a sample database
<br />
</Text>
</Stack.Item>
</Stack>
<Stack.Item style={{ textAlign: "start", marginLeft: "25px" }}>
<Text>
Copilot is setup on a sample database we have configured for you at no cost
<br />
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
</Text>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
<Stack>
<Stack.Item align="center">
<PrimaryButton style={{ width: "224px", height: "32px" }} onClick={hideModal}>
Get Started
</PrimaryButton>
</Stack.Item>
</Stack>
</div>
</Stack>
)
);
};

View File

@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Welcome Sidebar Modal snapshot test should close on button click 1`] = `""`;
exports[`Welcome Sidebar Modal snapshot test should not reneder with local storage key 1`] = `""`;

View File

@@ -0,0 +1,55 @@
import { Stack } from "@fluentui/react";
import Explorer from "Explorer/Explorer";
import { SampleBubble } from "Explorer/QueryCopilot/V2/Bubbles/Sample/SampleBubble";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { withHooks } from "jest-react-hooks-shallow";
import React from "react";
import { QueryCopilotSidebar } from "./QueryCopilotSidebar";
describe("Query Copilot Sidebar snapshot test", () => {
const initialState = useQueryCopilot.getState();
beforeEach(() => {
useQueryCopilot.setState(initialState, true);
});
it("should render and set copilot used flag ", () => {
withHooks(() => {
useQueryCopilot.getState().setShowCopilotSidebar(true);
const wrapper = shallow(<QueryCopilotSidebar explorer={new Explorer()} />);
expect(useQueryCopilot.getState().wasCopilotUsed).toBeTruthy();
expect(wrapper).toMatchSnapshot();
});
});
it("should render and not set copilot used flag ", () => {
withHooks(() => {
const wrapper = shallow(<QueryCopilotSidebar explorer={new Explorer()} />);
expect(useQueryCopilot.getState().wasCopilotUsed).toBeFalsy();
expect(wrapper).toMatchSnapshot();
});
});
it("should render with chat messages", () => {
const message = "some test message";
useQueryCopilot.getState().setChatMessages([{ source: 0, message: message }]);
const wrapper = shallow(<QueryCopilotSidebar explorer={new Explorer()} />);
const messageContainer = wrapper.find(Stack).findWhere((x) => x.text() === message);
expect(messageContainer).toBeDefined();
expect(wrapper).toMatchSnapshot();
});
it("should render samples without messages", () => {
const wrapper = shallow(<QueryCopilotSidebar explorer={new Explorer()} />);
const sampleBubble = wrapper.find(SampleBubble);
expect(sampleBubble).toBeDefined();
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,84 @@
import { Stack } from "@fluentui/react";
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { RetrievingBubble } from "Explorer/QueryCopilot/V2/Bubbles/Retriveing/RetrievingBubble";
import { SampleBubble } from "Explorer/QueryCopilot/V2/Bubbles/Sample/SampleBubble";
import { WelcomeBubble } from "Explorer/QueryCopilot/V2/Bubbles/Welcome/WelcomeBubble";
import { Footer } from "Explorer/QueryCopilot/V2/Footer/Footer";
import { Header } from "Explorer/QueryCopilot/V2/Header/Header";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import { WelcomeSidebarModal } from "../Modal/WelcomeSidebarModal";
export const QueryCopilotSidebar: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
const {
setWasCopilotUsed,
showCopilotSidebar,
chatMessages,
showWelcomeSidebar,
isGeneratingQuery,
} = useQueryCopilot();
React.useEffect(() => {
if (showCopilotSidebar) {
setWasCopilotUsed(true);
}
}, []);
return (
<Stack style={{ width: "100%", height: "100%", backgroundColor: "#FAFAFA" }}>
<Header />
{showWelcomeSidebar ? (
<WelcomeSidebarModal />
) : (
<>
<Stack
style={{
flexGrow: 1,
display: "flex",
flexDirection: "column",
overflowY: "auto",
}}
>
<WelcomeBubble />
{chatMessages.map((message, index) =>
message.source === 0 ? (
<Stack
key={index}
horizontalAlign="center"
tokens={{ padding: 8, childrenGap: 8 }}
style={{
backgroundColor: "#E0E7FF",
borderRadius: "8px",
margin: "5px 10px",
textAlign: "start",
}}
>
{message.message}
</Stack>
) : (
<Stack
key={index}
horizontalAlign="center"
tokens={{ padding: 8, childrenGap: 8 }}
style={{
backgroundColor: "white",
borderRadius: "8px",
margin: "5px 10px",
textAlign: "start",
}}
>
{message.message}
</Stack>
)
)}
<RetrievingBubble />
{chatMessages.length === 0 && !isGeneratingQuery && <SampleBubble />}
</Stack>
<Footer explorer={explorer} />
</>
)}
</Stack>
);
};

View File

@@ -0,0 +1,61 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Query Copilot Sidebar snapshot test should render and not set copilot used flag 1`] = `
<Stack
style={
Object {
"backgroundColor": "#FAFAFA",
"height": "100%",
"width": "100%",
}
}
>
<Header />
<WelcomeSidebarModal />
</Stack>
`;
exports[`Query Copilot Sidebar snapshot test should render and set copilot used flag 1`] = `
<Stack
style={
Object {
"backgroundColor": "#FAFAFA",
"height": "100%",
"width": "100%",
}
}
>
<Header />
<WelcomeSidebarModal />
</Stack>
`;
exports[`Query Copilot Sidebar snapshot test should render samples without messages 1`] = `
<Stack
style={
Object {
"backgroundColor": "#FAFAFA",
"height": "100%",
"width": "100%",
}
}
>
<Header />
<WelcomeSidebarModal />
</Stack>
`;
exports[`Query Copilot Sidebar snapshot test should render with chat messages 1`] = `
<Stack
style={
Object {
"backgroundColor": "#FAFAFA",
"height": "100%",
"width": "100%",
}
}
>
<Header />
<WelcomeSidebarModal />
</Stack>
`;

View File

@@ -5,133 +5,149 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
className="tab-pane" className="tab-pane"
style={ style={
Object { Object {
"height": "100%",
"padding": 24, "padding": 24,
"width": "100%", "width": "100%",
} }
} }
> >
<Stack <div
horizontal={true}
verticalAlign="center"
>
<Image
src=""
/>
<Text
style={
Object {
"fontSize": 16,
"fontWeight": 600,
"marginLeft": 8,
}
}
>
Copilot
</Text>
</Stack>
<Stack
horizontal={true}
style={ style={
Object { Object {
"marginTop": 16, "height": "100%",
"position": "relative", "overflowY": "auto",
"width": "100%",
}
}
verticalAlign="center"
>
<StyledTextFieldBase
autoComplete="off"
disabled={false}
id="naturalLanguageInput"
onChange={[Function]}
onClick={[Function]}
style={
Object {
"lineHeight": 30,
}
}
styles={
Object {
"root": Object {
"width": "95%",
},
}
}
value="Write a query to return all records in this table"
/>
<CustomizedIconButton
disabled={false}
iconProps={
Object {
"iconName": "Send",
}
}
onClick={[Function]}
style={
Object {
"marginLeft": 8,
}
}
/>
</Stack>
<Text
style={
Object {
"fontSize": 12,
"marginBottom": 24,
"marginTop": 8,
} }
} }
> >
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it. <Stack
horizontal={true}
<StyledLinkBase verticalAlign="center"
href=""
target="_blank"
> >
Read preview terms <Image
</StyledLinkBase> src=""
</Text> />
<Stack <Text
className="tabPaneContentContainer" style={
> Object {
<t "fontSize": 16,
customClassName="" "fontWeight": 600,
onDragEnd={null} "marginLeft": 8,
onDragStart={null} }
onSecondaryPaneSizeChange={null} }
percentage={false} >
primaryIndex={0} Copilot
primaryMinSize={100} </Text>
secondaryMinSize={200} </Stack>
vertical={true} <Stack
horizontal={true}
style={
Object {
"marginTop": 16,
"position": "relative",
"width": "100%",
}
}
verticalAlign="center"
> >
<EditorReact <StyledTextFieldBase
ariaLabel="Editing Query" autoComplete="off"
content="" disabled={false}
isReadOnly={false} id="naturalLanguageInput"
language="sql" onChange={[Function]}
lineNumbers="on" onClick={[Function]}
onContentChanged={[Function]} onKeyDown={[Function]}
onContentSelected={[Function]} style={
Object {
"lineHeight": 30,
}
}
styles={
Object {
"root": Object {
"width": "95%",
},
}
}
value=""
/> />
<QueryResultSection <CustomizedIconButton
error="" disabled={true}
executeQueryDocumentsPage={[Function]} iconProps={
isExecuting={false} Object {
isMongoDB={false} "iconName": "Send",
queryEditorContent="" }
}
onClick={[Function]}
style={
Object {
"marginLeft": 8,
}
}
/> />
</t> </Stack>
</Stack> <Stack
<WelcomeModal style={
visible={true} Object {
/> "marginBottom": 24,
<CopyPopup "marginTop": 8,
setShowCopyPopup={[Function]} }
showCopyPopup={false} }
/> >
<Text
style={
Object {
"fontSize": 12,
}
}
>
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
<StyledLinkBase
href="http://aka.ms/cdb-copilot-preview-terms"
target="_blank"
>
Read preview terms
</StyledLinkBase>
</Text>
</Stack>
<Stack
className="tabPaneContentContainer"
>
<t
customClassName=""
onDragEnd={null}
onDragStart={null}
onSecondaryPaneSizeChange={null}
percentage={false}
primaryIndex={0}
primaryMinSize={100}
secondaryMinSize={200}
vertical={true}
>
<EditorReact
ariaLabel="Editing Query"
content=""
isReadOnly={false}
language="sql"
lineNumbers="on"
onContentChanged={[Function]}
onContentSelected={[Function]}
/>
<QueryResultSection
error=""
executeQueryDocumentsPage={[Function]}
isExecuting={false}
isMongoDB={false}
queryEditorContent=""
/>
</t>
</Stack>
<WelcomeModal
visible={true}
/>
<CopyPopup
setShowCopyPopup={[Function]}
showCopyPopup={false}
/>
</div>
</Stack> </Stack>
`; `;

View File

@@ -97,6 +97,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
() => this.setState({}), () => this.setState({}),
(state) => state.showResetPasswordBubble (state) => state.showResetPasswordBubble
), ),
},
{
dispose: useDatabases.subscribe(
() => this.setState({}),
(state) => state.sampleDataResourceTokenCollection
),
} }
); );
} }
@@ -107,7 +113,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
}; };
private getSplashScreenButtons = (): JSX.Element => { private getSplashScreenButtons = (): JSX.Element => {
if (userContext.features.enableCopilot && userContext.apiType === "SQL") { if (
useDatabases.getState().sampleDataResourceTokenCollection &&
userContext.features.enableCopilot &&
userContext.apiType === "SQL"
) {
return ( return (
<Stack style={{ width: "66%", cursor: "pointer", margin: "40px auto" }} tokens={{ childrenGap: 16 }}> <Stack style={{ width: "66%", cursor: "pointer", margin: "40px auto" }} tokens={{ childrenGap: 16 }}>
<Stack horizontal tokens={{ childrenGap: 16 }}> <Stack horizontal tokens={{ childrenGap: 16 }}>
@@ -137,7 +147,16 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
description={ 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!" "Copilot is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
} }
onClick={() => useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot)} onClick={() => {
const copilotVersion = userContext.features.copilotVersion;
if (copilotVersion === "v1.0") {
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
} else if (copilotVersion === "v2.0") {
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
sampleCollection.onNewQueryClick(sampleCollection, undefined);
}
traceOpen(Action.OpenQueryCopilotFromSplashScreen, { apiType: userContext.apiType });
}}
/> />
<SplashScreenButton <SplashScreenButton
imgSrc={ConnectIcon} imgSrc={ConnectIcon}
@@ -246,8 +265,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
<form className="connectExplorerFormContainer"> <form className="connectExplorerFormContainer">
<div className="splashScreenContainer"> <div className="splashScreenContainer">
<div className="splashScreen"> <div className="splashScreen">
<div <h1
className="title" className="title"
role="heading"
aria-label={ aria-label={
userContext.apiType === "Postgres" userContext.apiType === "Postgres"
? "Welcome to Azure Cosmos DB for PostgreSQL" ? "Welcome to Azure Cosmos DB for PostgreSQL"
@@ -258,7 +278,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
? "Welcome to Azure Cosmos DB for PostgreSQL" ? "Welcome to Azure Cosmos DB for PostgreSQL"
: "Welcome to Azure Cosmos DB"} : "Welcome to Azure Cosmos DB"}
<FeaturePanelLauncher /> <FeaturePanelLauncher />
</div> </h1>
<div className="subtitle"> <div className="subtitle">
{userContext.apiType === "Postgres" {userContext.apiType === "Postgres"
? "Get started with our sample datasets, documentation, and additional tools." ? "Get started with our sample datasets, documentation, and additional tools."
@@ -581,7 +601,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
> >
{item.title} {item.title}
</Link> </Link>
<Image src={LinkIcon} alt=" " /> <Image src={LinkIcon} alt={item.title} />
</Stack> </Stack>
<Text>{item.description}</Text> <Text>{item.description}</Text>
</Stack> </Stack>
@@ -600,7 +620,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
<li key={`${item.title}${item.description}${index}`}> <li key={`${item.title}${item.description}${index}`}>
<Stack style={{ marginBottom: 26 }}> <Stack style={{ marginBottom: 26 }}>
<Stack horizontal> <Stack horizontal>
<Image style={{ marginRight: 8 }} src={item.iconSrc} /> <Image style={{ marginRight: 8 }} src={item.iconSrc} alt={item.title} />
<Link style={{ fontSize: 14 }} onClick={item.onClick} title={item.info}> <Link style={{ fontSize: 14 }} onClick={item.onClick} title={item.info}>
{item.title} {item.title}
</Link> </Link>
@@ -720,7 +740,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
> >
{item.title} {item.title}
</Link> </Link>
<Image src={LinkIcon} /> <Image src={LinkIcon} alt={item.title} />
</Stack> </Stack>
<Text>{item.description}</Text> <Text>{item.description}</Text>
</Stack> </Stack>

View File

@@ -1,4 +1,5 @@
import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import * as ko from "knockout"; import * as ko from "knockout";
import Q from "q"; import Q from "q";
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg"; import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
@@ -7,7 +8,12 @@ import NewDocumentIcon from "../../../images/NewDocument.svg";
import SaveIcon from "../../../images/save-cosmos.svg"; import SaveIcon from "../../../images/save-cosmos.svg";
import UploadIcon from "../../../images/Upload_16x16.svg"; import UploadIcon from "../../../images/Upload_16x16.svg";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants"; import {
DocumentsGridMetrics,
KeyCodes,
QueryCopilotSampleContainerId,
QueryCopilotSampleDatabaseId,
} from "../../Common/Constants";
import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument"; import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments"; import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
@@ -71,6 +77,7 @@ export default class DocumentsTab extends TabsBase {
private _documentsIterator: QueryIterator<ItemDefinition & Resource>; private _documentsIterator: QueryIterator<ItemDefinition & Resource>;
private _resourceTokenPartitionKey: string; private _resourceTokenPartitionKey: string;
private _isQueryCopilotSampleContainer: boolean;
constructor(options: ViewModels.DocumentsTabOptions) { constructor(options: ViewModels.DocumentsTabOptions) {
super(options); super(options);
@@ -317,6 +324,9 @@ export default class DocumentsTab extends TabsBase {
this.selectedDocumentContent.subscribe((newContent: string) => this._onEditorContentChange(newContent)); this.selectedDocumentContent.subscribe((newContent: string) => this._onEditorContentChange(newContent));
this.showPartitionKey = this._shouldShowPartitionKey(); this.showPartitionKey = this._shouldShowPartitionKey();
this._isQueryCopilotSampleContainer =
this.collection?.databaseId === QueryCopilotSampleDatabaseId &&
this.collection?.id() === QueryCopilotSampleContainerId;
} }
private _shouldShowPartitionKey(): boolean { private _shouldShowPartitionKey(): boolean {
@@ -678,7 +688,6 @@ export default class DocumentsTab extends TabsBase {
} }
public createIterator(): QueryIterator<ItemDefinition & Resource> { public createIterator(): QueryIterator<ItemDefinition & Resource> {
let filters = this.lastFilterContents();
const filter: string = this.filterContent().trim(); const filter: string = this.filterContent().trim();
const query: string = this.buildQuery(filter); const query: string = this.buildQuery(filter);
let options: any = {}; let options: any = {};
@@ -688,12 +697,16 @@ export default class DocumentsTab extends TabsBase {
options.partitionKey = this._resourceTokenPartitionKey; options.partitionKey = this._resourceTokenPartitionKey;
} }
return queryDocuments(this.collection.databaseId, this.collection.id(), query, options); return this._isQueryCopilotSampleContainer
? querySampleDocuments(query, options)
: queryDocuments(this.collection.databaseId, this.collection.id(), query, options);
} }
public async selectDocument(documentId: DocumentId): Promise<void> { public async selectDocument(documentId: DocumentId): Promise<void> {
this.selectedDocumentId(documentId); this.selectedDocumentId(documentId);
const content = await readDocument(this.collection, documentId); const content = await (this._isQueryCopilotSampleContainer
? readSampleDocument(documentId)
: readDocument(this.collection, documentId));
this.initDocumentEditor(documentId, content); this.initDocumentEditor(documentId, content);
} }
@@ -810,7 +823,7 @@ export default class DocumentsTab extends TabsBase {
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
hasPopup: false, hasPopup: false,
disabled: !this.newDocumentButton.enabled(), disabled: !this.newDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(),
id: "mongoNewDocumentBtn", id: "mongoNewDocumentBtn",
}); });
} }
@@ -824,7 +837,8 @@ export default class DocumentsTab extends TabsBase {
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
hasPopup: false, hasPopup: false,
disabled: !this.saveNewDocumentButton.enabled(), disabled:
!this.saveNewDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(),
}); });
} }
@@ -837,7 +851,9 @@ export default class DocumentsTab extends TabsBase {
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
hasPopup: false, hasPopup: false,
disabled: !this.discardNewDocumentChangesButton.enabled(), disabled:
!this.discardNewDocumentChangesButton.enabled() ||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
}); });
} }
@@ -850,7 +866,8 @@ export default class DocumentsTab extends TabsBase {
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
hasPopup: false, hasPopup: false,
disabled: !this.saveExistingDocumentButton.enabled(), disabled:
!this.saveExistingDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(),
}); });
} }
@@ -863,7 +880,9 @@ export default class DocumentsTab extends TabsBase {
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
hasPopup: false, hasPopup: false,
disabled: !this.discardExisitingDocumentChangesButton.enabled(), disabled:
!this.discardExisitingDocumentChangesButton.enabled() ||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
}); });
} }
@@ -876,7 +895,9 @@ export default class DocumentsTab extends TabsBase {
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
hasPopup: false, hasPopup: false,
disabled: !this.deleteExisitingDocumentButton.enabled(), disabled:
!this.deleteExisitingDocumentButton.enabled() ||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
}); });
} }
@@ -920,7 +941,9 @@ export default class DocumentsTab extends TabsBase {
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
hasPopup: true, hasPopup: true,
disabled: useSelectedNode.getState().isDatabaseNodeOrNoneSelected(), disabled:
useSelectedNode.getState().isDatabaseNodeOrNoneSelected() ||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
}; };
} }
} }

View File

@@ -0,0 +1,34 @@
import { fireEvent, render } from "@testing-library/react";
import QueryTabComponent, { IQueryTabComponentProps } from "Explorer/Tabs/QueryTab/QueryTabComponent";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
jest.mock("Explorer/Controls/Editor/EditorReact");
describe("QueryTabComponent", () => {
const mockStore = useQueryCopilot.getState();
beforeEach(() => {
mockStore.showCopilotSidebar = false;
mockStore.setShowCopilotSidebar = jest.fn();
});
beforeEach(() => jest.clearAllMocks());
it("should launch Copilot when ALT+C is pressed", () => {
const propsMock: Readonly<IQueryTabComponentProps> = ({
collection: { databaseId: "CopilotSampleDb" },
onTabAccessor: () => jest.fn(),
isExecutionError: false,
tabId: "mockTabId",
tabsBaseInstance: {
updateNavbarWithTabsButtons: () => jest.fn(),
},
} as unknown) as IQueryTabComponentProps;
const { container } = render(<QueryTabComponent {...propsMock} />);
const launchCopilotButton = container.querySelector(".queryEditorWatermarkText");
fireEvent.keyDown(launchCopilotButton, { key: "c", altKey: true });
expect(mockStore.setShowCopilotSidebar).toHaveBeenCalledWith(true);
});
});

View File

@@ -1,11 +1,14 @@
import { FeedOptions } from "@azure/cosmos"; import { FeedOptions } from "@azure/cosmos";
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection"; import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import SplitterLayout from "react-splitter-layout"; import SplitterLayout from "react-splitter-layout";
import "react-splitter-layout/lib/index.css"; import "react-splitter-layout/lib/index.css";
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
import SaveQueryIcon from "../../../../images/save-cosmos.svg"; import SaveQueryIcon from "../../../../images/save-cosmos.svg";
import { NormalizedEventKey } from "../../../Common/Constants"; import { NormalizedEventKey, QueryCopilotSampleDatabaseId } from "../../../Common/Constants";
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../../Common/HeadersUtility"; import * as HeadersUtility from "../../../Common/HeadersUtility";
import { MinimalQueryIterator } from "../../../Common/IteratorUtilities"; import { MinimalQueryIterator } from "../../../Common/IteratorUtilities";
@@ -72,15 +75,18 @@ interface IQueryTabStates {
error: string; error: string;
isExecutionError: boolean; isExecutionError: boolean;
isExecuting: boolean; isExecuting: boolean;
showCopilotSidebar: boolean;
} }
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> { export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
public queryEditorId: string; public queryEditorId: string;
public executeQueryButton: Button; public executeQueryButton: Button;
public saveQueryButton: Button; public saveQueryButton: Button;
public launchCopilotButton: Button;
public splitterId: string; public splitterId: string;
public isPreferredApiMongoDB: boolean; public isPreferredApiMongoDB: boolean;
public isCloseClicked: boolean; public isCloseClicked: boolean;
public isCopilotTabActive: boolean;
private _iterator: MinimalQueryIterator; private _iterator: MinimalQueryIterator;
constructor(props: IQueryTabComponentProps) { constructor(props: IQueryTabComponentProps) {
@@ -94,11 +100,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
error: "", error: "",
isExecutionError: this.props.isExecutionError, isExecutionError: this.props.isExecutionError,
isExecuting: false, isExecuting: false,
showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar,
}; };
this.isCloseClicked = false; this.isCloseClicked = false;
this.splitterId = this.props.tabId + "_splitter"; this.splitterId = this.props.tabId + "_splitter";
this.queryEditorId = `queryeditor${this.props.tabId}`; this.queryEditorId = `queryeditor${this.props.tabId}`;
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB; this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
this.isCopilotTabActive = QueryCopilotSampleDatabaseId === this.props.collection.databaseId;
this.executeQueryButton = { this.executeQueryButton = {
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0, enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
@@ -111,6 +119,11 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
visible: isSaveQueryBtnEnabled, visible: isSaveQueryBtnEnabled,
}; };
this.launchCopilotButton = {
enabled: userContext.apiType === "SQL" && true,
visible: userContext.apiType === "SQL" && true,
};
this.props.tabsBaseInstance.updateNavbarWithTabsButtons(); this.props.tabsBaseInstance.updateNavbarWithTabsButtons();
props.onTabAccessor({ props.onTabAccessor({
onTabClickEvent: this.onTabClick.bind(this), onTabClickEvent: this.onTabClick.bind(this),
@@ -121,6 +134,9 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
public onCloseClick(isClicked: boolean): void { public onCloseClick(isClicked: boolean): void {
this.isCloseClicked = isClicked; this.isCloseClicked = isClicked;
if (useQueryCopilot.getState().wasCopilotUsed && this.isCopilotTabActive) {
useQueryCopilot.getState().resetQueryCopilotStates();
}
} }
public getCurrentEditorQuery(): string { public getCurrentEditorQuery(): string {
@@ -146,6 +162,10 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />); useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />);
}; };
public launchQueryCopilotChat = (): void => {
useQueryCopilot.getState().setShowCopilotSidebar(!useQueryCopilot.getState().showCopilotSidebar);
};
public onSavedQueriesClick = (): void => { public onSavedQueriesClick = (): void => {
useSidePanel useSidePanel
.getState() .getState()
@@ -164,6 +184,12 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
}); });
} }
public handleCopilotKeyDown = (event: KeyboardEvent): void => {
if (this.isCopilotTabActive && event.altKey && event.key === "c") {
this.launchQueryCopilotChat();
}
};
public onToggleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): boolean => { public onToggleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): boolean => {
if (event.key === NormalizedEventKey.LeftArrow) { if (event.key === NormalizedEventKey.LeftArrow) {
this.toggleResult(); this.toggleResult();
@@ -269,6 +295,39 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
}); });
} }
if (this.launchCopilotButton.visible && this.isCopilotTabActive) {
const mainButtonLabel = "Launch Copilot";
const chatPaneLabel = "Open Copilot in chat pane (ALT+C)";
const copilotSettingLabel = "Copilot settings";
const openCopilotChatButton: CommandButtonComponentProps = {
iconAlt: chatPaneLabel,
onCommandClick: this.launchQueryCopilotChat,
commandButtonLabel: chatPaneLabel,
ariaLabel: chatPaneLabel,
hasPopup: false,
};
const copilotSettingsButton: CommandButtonComponentProps = {
iconAlt: copilotSettingLabel,
onCommandClick: () => undefined,
commandButtonLabel: copilotSettingLabel,
ariaLabel: copilotSettingLabel,
hasPopup: false,
};
const launchCopilotButton = {
iconSrc: LaunchCopilot,
iconAlt: mainButtonLabel,
onCommandClick: this.launchQueryCopilotChat,
commandButtonLabel: mainButtonLabel,
ariaLabel: mainButtonLabel,
hasPopup: false,
children: [openCopilotChatButton, copilotSettingsButton],
};
buttons.push(launchCopilotButton);
}
return buttons; return buttons;
} }
@@ -306,11 +365,25 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
} }
private unsubscribeCopilotSidebar: () => void;
componentDidMount(): void { componentDidMount(): void {
this.unsubscribeCopilotSidebar = useQueryCopilot.subscribe((state: QueryCopilotState) => {
if (this.state.showCopilotSidebar !== state.showCopilotSidebar) {
this.setState({ showCopilotSidebar: state.showCopilotSidebar });
}
});
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
document.addEventListener("keydown", this.handleCopilotKeyDown);
} }
render(): JSX.Element { componentWillUnmount(): void {
this.unsubscribeCopilotSidebar();
document.removeEventListener("keydown", this.handleCopilotKeyDown);
}
private getEditorAndQueryResult(): JSX.Element {
return ( return (
<Fragment> <Fragment>
<div className="tab-pane" id={this.props.tabId} role="tabpanel"> <div className="tab-pane" id={this.props.tabId} role="tabpanel">
@@ -343,4 +416,20 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
</Fragment> </Fragment>
); );
} }
render(): JSX.Element {
const shouldScaleElements = this.state.showCopilotSidebar && this.isCopilotTabActive;
return (
<div style={{ display: "flex", flexDirection: "row", height: "100%" }}>
<div style={{ width: shouldScaleElements ? "70%" : "100%", height: "100%" }}>
{this.getEditorAndQueryResult()}
</div>
{shouldScaleElements && (
<div style={{ width: "30%", height: "100%" }}>
<QueryCopilotSidebar explorer={this.props.collection.container} />
</div>
)}
</div>
);
}
} }

View File

@@ -1,4 +1,5 @@
import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react"; import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
import { PoolIdType } from "Common/Constants";
import { configContext } from "ConfigContext"; import { configContext } from "ConfigContext";
import { NotebookWorkspaceConnectionInfo, PostgresFirewallRule } from "Contracts/DataModels"; import { NotebookWorkspaceConnectionInfo, PostgresFirewallRule } from "Contracts/DataModels";
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent"; import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
@@ -51,7 +52,7 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
}); });
useEffect(() => { useEffect(() => {
explorer.allocateContainer(); explorer.allocateContainer(PoolIdType.DefaultPoolId);
}, []); }, []);
return ( return (

View File

@@ -9,6 +9,7 @@ import { ConnectTab } from "Explorer/Tabs/ConnectTab";
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab"; import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab"; import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import ko from "knockout"; import ko from "knockout";
import React, { MutableRefObject, useEffect, useRef, useState } from "react"; import React, { MutableRefObject, useEffect, useRef, useState } from "react";
@@ -68,7 +69,7 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?: ReactTabKind }) { function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?: ReactTabKind }) {
const [hovering, setHovering] = useState(false); const [hovering, setHovering] = useState(false);
const focusTab = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>; const focusTab = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
const tabId = tab ? tab.tabId : "connect"; const tabId = tab ? tab.tabId : "";
const getReactTabTitle = (): ko.Observable<string> => { const getReactTabTitle = (): ko.Observable<string> => {
if (tabKind === ReactTabKind.QueryCopilot) { if (tabKind === ReactTabKind.QueryCopilot) {
@@ -126,7 +127,12 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
/> />
)} )}
</span> </span>
<span className="tabNavText">{useObservable(tab?.tabTitle || getReactTabTitle())}</span> <span
className="tabNavText"
style={active ? { fontWeight: "bolder", borderBottom: "2px solid rgba(0,120,212,1)" } : {}}
>
{useObservable(tab?.tabTitle || getReactTabTitle())}
</span>
{tabKind !== ReactTabKind.Home && ( {tabKind !== ReactTabKind.Home && (
<span className="tabIconSection"> <span className="tabIconSection">
<CloseButton tab={tab} active={active} hovering={hovering} tabKind={tabKind} /> <CloseButton tab={tab} active={active} hovering={hovering} tabKind={tabKind} />
@@ -158,6 +164,7 @@ const CloseButton = ({
onClick={(event: React.MouseEvent<HTMLSpanElement, MouseEvent>) => { onClick={(event: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
event.stopPropagation(); event.stopPropagation();
tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeReactTab(tabKind); tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeReactTab(tabKind);
tabKind === ReactTabKind.QueryCopilot && useQueryCopilot.getState().resetQueryCopilotStates();
}} }}
tabIndex={active ? 0 : undefined} tabIndex={active ? 0 : undefined}
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)} onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)}
@@ -251,7 +258,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
case ReactTabKind.Quickstart: case ReactTabKind.Quickstart:
return <QuickstartTab explorer={explorer} />; return <QuickstartTab explorer={explorer} />;
case ReactTabKind.QueryCopilot: case ReactTabKind.QueryCopilot:
return <QueryCopilotTab initialInput={useTabs.getState().queryCopilotTabInitialInput} explorer={explorer} />; return <QueryCopilotTab explorer={explorer} />;
default: default:
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`); throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
} }

View File

@@ -3,6 +3,9 @@ import { useNotebook } from "Explorer/Notebook/useNotebook";
import * as ko from "knockout"; import * as ko from "knockout";
import * as _ from "underscore"; import * as _ from "underscore";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import { bulkCreateDocument } from "../../Common/dataAccess/bulkCreateDocument"; import { bulkCreateDocument } from "../../Common/dataAccess/bulkCreateDocument";
import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument } from "../../Common/dataAccess/createDocument";
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize"; import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
@@ -10,19 +13,16 @@ import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer
import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures"; import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures";
import { readTriggers } from "../../Common/dataAccess/readTriggers"; import { readTriggers } from "../../Common/dataAccess/readTriggers";
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions"; import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { UploadDetailsRecord } from "../../Contracts/ViewModels"; import { UploadDetailsRecord } from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import { isServerlessAccount } from "../../Utils/CapabilityUtils";
import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
import { useTabs } from "../../hooks/useTabs";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient"; import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
@@ -526,7 +526,7 @@ export default class Collection implements ViewModels.Collection {
public onSchemaAnalyzerClick = async () => { public onSchemaAnalyzerClick = async () => {
if (useNotebook.getState().isPhoenixFeatures) { if (useNotebook.getState().isPhoenixFeatures) {
await this.container.allocateContainer(); await this.container.allocateContainer(Constants.PoolIdType.DefaultPoolId);
} }
useSelectedNode.getState().setSelectedNode(this); useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer); this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer);

View File

@@ -2,10 +2,10 @@ import * as ko from "knockout";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { useTabs } from "../../hooks/useTabs";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import DocumentsTab from "../Tabs/DocumentsTab"; import DocumentsTab from "../Tabs/DocumentsTab";
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab"; import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
@@ -28,8 +28,9 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
public children: ko.ObservableArray<ViewModels.TreeNode>; public children: ko.ObservableArray<ViewModels.TreeNode>;
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>; public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
public isCollectionExpanded: ko.Observable<boolean>; public isCollectionExpanded: ko.Observable<boolean>;
public isSampleCollection?: boolean;
constructor(container: Explorer, databaseId: string, data: DataModels.Collection) { constructor(container: Explorer, databaseId: string, data: DataModels.Collection, isSampleCollection?: boolean) {
this.nodeKind = "Collection"; this.nodeKind = "Collection";
this.container = container; this.container = container;
this.databaseId = databaseId; this.databaseId = databaseId;
@@ -42,6 +43,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
this.children = ko.observableArray<ViewModels.TreeNode>([]); this.children = ko.observableArray<ViewModels.TreeNode>([]);
this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>(); this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
this.isCollectionExpanded = ko.observable<boolean>(true); this.isCollectionExpanded = ko.observable<boolean>(true);
this.isSampleCollection = isSampleCollection;
} }
public expandCollection(): void { public expandCollection(): void {
@@ -139,7 +141,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
documentsTab = new DocumentsTab({ documentsTab = new DocumentsTab({
partitionKey: this.partitionKey, partitionKey: this.partitionKey,
resourceTokenPartitionKey: userContext.parsedResourceToken.partitionKey, resourceTokenPartitionKey: userContext.parsedResourceToken?.partitionKey,
documentIds: ko.observableArray<DocumentId>([]), documentIds: ko.observableArray<DocumentId>([]),
tabKind: ViewModels.CollectionTabKind.Documents, tabKind: ViewModels.CollectionTabKind.Documents,
title: "Items", title: "Items",

View File

@@ -790,14 +790,10 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
{!isNotebookEnabled && isSampleDataEnabled && ( {!isNotebookEnabled && isSampleDataEnabled && (
<> <>
<AccordionComponent> <AccordionComponent>
<AccordionItemComponent <AccordionItemComponent title={"MY DATA"} isExpanded={!gitHubNotebooksContentRoot}>
title={"MY DATA"}
isExpanded={!gitHubNotebooksContentRoot}
styles={{ maxHeight: 230 }}
>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} /> <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent> </AccordionItemComponent>
<AccordionItemComponent title={"SAMPLE DATA"}> <AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} /> <SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
</AccordionItemComponent> </AccordionItemComponent>
</AccordionComponent> </AccordionComponent>
@@ -808,14 +804,10 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
{isNotebookEnabled && isSampleDataEnabled && ( {isNotebookEnabled && isSampleDataEnabled && (
<> <>
<AccordionComponent> <AccordionComponent>
<AccordionItemComponent <AccordionItemComponent title={"MY DATA"} isExpanded={!gitHubNotebooksContentRoot}>
title={"MY DATA"}
isExpanded={!gitHubNotebooksContentRoot}
styles={{ maxHeight: 130 }}
>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} /> <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent> </AccordionItemComponent>
<AccordionItemComponent title={"SAMPLE DATA"}> <AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} /> <SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
</AccordionItemComponent> </AccordionItemComponent>
<AccordionItemComponent title={"NOTEBOOKS"}> <AccordionItemComponent title={"NOTEBOOKS"}>

View File

@@ -2,7 +2,7 @@ import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdap
import TabsBase from "Explorer/Tabs/TabsBase"; import TabsBase from "Explorer/Tabs/TabsBase";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
import React, { useEffect, useState } from "react"; import React from "react";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import CollectionIcon from "../../../images/tree-collection.svg"; import CollectionIcon from "../../../images/tree-collection.svg";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
@@ -14,51 +14,65 @@ export const SampleDataTree = ({
}: { }: {
sampleDataResourceTokenCollection: ViewModels.CollectionBase; sampleDataResourceTokenCollection: ViewModels.CollectionBase;
}): JSX.Element => { }): JSX.Element => {
const [root, setRoot] = useState<TreeNode | undefined>(undefined); const buildSampleDataTree = (): TreeNode => {
const updatedSampleTree: TreeNode = {
useEffect(() => { label: sampleDataResourceTokenCollection.databaseId,
if (sampleDataResourceTokenCollection) { isExpanded: false,
const updatedSampleTree: TreeNode = { iconSrc: CosmosDBIcon,
label: sampleDataResourceTokenCollection.databaseId, className: "databaseHeader",
isExpanded: false, children: [
iconSrc: CosmosDBIcon, {
className: "databaseHeader", label: sampleDataResourceTokenCollection.id(),
children: [ iconSrc: CollectionIcon,
{ isExpanded: false,
label: sampleDataResourceTokenCollection.id(), className: "collectionHeader",
iconSrc: CollectionIcon, contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
isExpanded: false, onClick: () => {
className: "dataResourceTree", useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection);
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(), useCommandBar.getState().setContextButtons([]);
onClick: () => { useTabs
// Rewritten version of expandCollapseCollection .getState()
useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection); .refreshActiveTab(
useCommandBar.getState().setContextButtons([]);
useTabs().refreshActiveTab(
(tab: TabsBase) => (tab: TabsBase) =>
tab.collection?.id() === sampleDataResourceTokenCollection.id() && tab.collection?.id() === sampleDataResourceTokenCollection.id() &&
tab.collection.databaseId === sampleDataResourceTokenCollection.databaseId tab.collection.databaseId === sampleDataResourceTokenCollection.databaseId
); );
},
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(
sampleDataResourceTokenCollection.databaseId,
sampleDataResourceTokenCollection.id()
),
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection),
children: [
{
label: "Items",
},
],
}, },
], isSelected: () =>
}; useSelectedNode
setRoot(updatedSampleTree); .getState()
} .isDataNodeSelected(sampleDataResourceTokenCollection.databaseId, sampleDataResourceTokenCollection.id()),
}, [sampleDataResourceTokenCollection]); onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection),
children: [
{
label: "Items",
onClick: () => sampleDataResourceTokenCollection.onDocumentDBDocumentsClick(),
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(
sampleDataResourceTokenCollection.databaseId,
sampleDataResourceTokenCollection.id(),
[ViewModels.CollectionTabKind.Documents]
),
},
],
},
],
};
return <TreeComponent className="dataResourceTree" rootNode={root || { label: "Sample data not initialized." }} />; return {
label: undefined,
isExpanded: true,
children: [updatedSampleTree],
};
};
return (
<TreeComponent
className="dataResourceTree"
rootNode={sampleDataResourceTokenCollection ? buildSampleDataTree() : { label: "Sample data not initialized." }}
/>
);
}; };

View File

@@ -66,11 +66,12 @@ export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) =>
return useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected; return useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected;
}, },
isQueryCopilotCollectionSelected: (): boolean => { isQueryCopilotCollectionSelected: (): boolean => {
const selectedNode = get().selectedNode; const selectedNode = get().selectedNode as ViewModels.CollectionBase;
if ( if (
selectedNode && selectedNode &&
selectedNode.isSampleCollection &&
selectedNode.id() === QueryCopilotSampleContainerId && selectedNode.id() === QueryCopilotSampleContainerId &&
(selectedNode as ViewModels.Collection)?.databaseId === QueryCopilotSampleDatabaseId selectedNode.databaseId === QueryCopilotSampleDatabaseId
) { ) {
return true; return true;
} }

View File

@@ -1,5 +1,5 @@
{ {
"Save": "Save", "Save": "Save",
"Discard": "Discard", "Discard": "Discard",
"Refresh": "Refesh" "Refresh": "Refresh"
} }

View File

@@ -17,7 +17,7 @@ import "../externals/jquery.typeahead.min.css";
import "../externals/jquery.typeahead.min.js"; import "../externals/jquery.typeahead.min.js";
// Image Dependencies // Image Dependencies
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel"; import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/QueryCopilotFeedbackModal"; import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import "../images/CosmosDB_rgb_ui_lighttheme.ico"; import "../images/CosmosDB_rgb_ui_lighttheme.ico";
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
@@ -83,7 +83,7 @@ const App: React.FunctionComponent = () => {
} }
return ( return (
<div className="flexContainer"> <div className="flexContainer" aria-hidden="false">
<div id="divExplorer" className="flexContainer hideOverflows"> <div id="divExplorer" className="flexContainer hideOverflows">
<div id="freeTierTeachingBubble"> </div> <div id="freeTierTeachingBubble"> </div>
{/* Main Command Bar - Start */} {/* Main Command Bar - Start */}
@@ -128,7 +128,7 @@ const App: React.FunctionComponent = () => {
{<SQLQuickstartTutorial />} {<SQLQuickstartTutorial />}
{<MongoQuickstartTutorial />} {<MongoQuickstartTutorial />}
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />} {<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
{shouldShowModal && <QueryCopilotFeedbackModal />} {shouldShowModal && <QueryCopilotFeedbackModal explorer={explorer} />}
</div> </div>
); );
}; };

View File

@@ -1,7 +1,9 @@
import { configContext } from "ConfigContext";
import { useDialog } from "Explorer/Controls/Dialog"; import { useDialog } from "Explorer/Controls/Dialog";
import promiseRetry, { AbortError } from "p-retry";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { userContext } from "UserContext";
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation"; import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
import promiseRetry, { AbortError } from "p-retry";
import { import {
Areas, Areas,
ConnectionStatusType, ConnectionStatusType,
@@ -12,7 +14,6 @@ import {
} from "../Common/Constants"; } from "../Common/Constants";
import { getErrorMessage, getErrorStack } from "../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../Common/ErrorHandlingUtils";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { configContext } from "../ConfigContext";
import { import {
ContainerConnectionInfo, ContainerConnectionInfo,
ContainerInfo, ContainerInfo,
@@ -28,7 +29,6 @@ import {
} from "../Contracts/DataModels"; } from "../Contracts/DataModels";
import { useNotebook } from "../Explorer/Notebook/useNotebook"; import { useNotebook } from "../Explorer/Notebook/useNotebook";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../UserContext";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
export class PhoenixClient { export class PhoenixClient {

View File

@@ -36,6 +36,10 @@ export type Features = {
readonly enableLegacyMongoShellV2Debug: boolean; readonly enableLegacyMongoShellV2Debug: boolean;
readonly loadLegacyMongoShellFromBE: boolean; readonly loadLegacyMongoShellFromBE: boolean;
readonly enableCopilot: boolean; readonly enableCopilot: boolean;
readonly enablePriorityBasedThrottling: boolean;
readonly copilotVersion?: string;
readonly enableCopilotPhoenixGateaway: boolean;
readonly enableCopilotFullSchema: boolean;
// can be set via both flight and feature flag // can be set via both flight and feature flag
autoscaleDefault: boolean; autoscaleDefault: boolean;
@@ -103,7 +107,11 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
enableLegacyMongoShellV2: "true" === get("enablelegacymongoshellv2"), enableLegacyMongoShellV2: "true" === get("enablelegacymongoshellv2"),
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"), enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"), loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
enablePriorityBasedThrottling: "true" === get("enableprioritybasedthrottling"),
enableCopilot: "true" === get("enablecopilot"), enableCopilot: "true" === get("enablecopilot"),
copilotVersion: get("copilotversion") ?? "v1.0",
enableCopilotPhoenixGateaway: "true" === get("enablecopilotphoenixgateaway"),
enableCopilotFullSchema: "true" === get("enablecopilotfullschema"),
}; };
} }

View File

@@ -6,6 +6,7 @@ export const createDefaultSettings = () => {
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, Constants.Queries.itemsPerPage); LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, Constants.Queries.itemsPerPage);
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "true"); LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "true");
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, Constants.Queries.DefaultMaxDegreeOfParallelism); LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, Constants.Queries.DefaultMaxDegreeOfParallelism);
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, Constants.PriorityLevel.Default);
}; };
export const hasSettingsDefined = (): boolean => { export const hasSettingsDefined = (): boolean => {
@@ -15,3 +16,9 @@ export const hasSettingsDefined = (): boolean => {
LocalStorageUtility.hasItem(StorageKey.MaxDegreeOfParellism) LocalStorageUtility.hasItem(StorageKey.MaxDegreeOfParellism)
); );
}; };
export const ensurePriorityLevel = () => {
if (!LocalStorageUtility.hasItem(StorageKey.PriorityLevel)) {
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, Constants.PriorityLevel.Default);
}
};

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