Compare commits

..

68 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
Karthik chakravarthy
cf0c51337f Change websocket authentication from url token to getting token from payload (#1487)
* Change websocket authentication from url to message body

* Add new message type

* Validation checks

* Remove flight and always send token in payload
2023-07-11 15:45:41 -04:00
victor-meng
1d6c0bbd1e Fix issue with executing queries in the query copilot tab (#1522) 2023-07-11 11:59:26 -07:00
Predrag Klepic
ed9ee07e81 Reverse cross partition query behavior in DE Query settings pane (#1521)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-10 11:30:53 -07:00
v-darkora
e9ea887fe7 [Query Copilot] Pass user email to query feedback (#1516)
* Pass user email to query feedback AB#2501550

* Await page.frame

* Make contact no by default

* Add hook to check if it sohuld render the modal in the main component
2023-07-10 10:59:05 +02:00
Predrag Klepic
b6d576b7b6 [Query Copilot] Resource tree styling and New query button (#1519)
* Styling implemented related with the work item

* Sample container New query button implementation

* Fixing related with the not rendering Sample Data

* Fix race condition when rendering sample data resource tree

* Remove export keyword for updateContextForSampleData

* Copilot New Query should open Copilot tab

* showing buttons in sample command bar

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
Co-authored-by: Victor Meng <vimeng@microsoft.com>
2023-07-10 09:14:14 +02:00
v-darkora
0eaa5d004b [Query Copilot] Pin panel footer to the bottom, remove gap between panel and console (#1511)
* Move footer when save button is enabled to the bottom, remove gap between notification console and right panel

* Change the way panel height is calculated

* Remove unnecessary operator

* Change condition

* Fix snapshot

* Update panel height after animation ends and use different css for showing save button to the bottom of the page

* Fix ts compile
2023-07-07 09:09:15 +02:00
v-darkora
3bef1ed136 [Query Copilot] Add learning bubble to query copilot input if text (#1507)
* Add learning bubble to query copilot input if user does not click for 30 seconds

* Change the way popup is shoed, on input changed update state
2023-07-07 09:08:41 +02:00
v-darkora
eb21193ae4 [Query Copilot] Disable command bar buttons when sample container is used (#1514)
* Disable command bar buttons when sample container is used and point only to copilot tab

* Make disabled assignement for buttons simmpler
2023-07-07 09:08:08 +02:00
v-darkora
90178178c4 [Query Copilot] Add toggle on feedback buttons (#1512)
* Add toggle on feedback buttons, clear state when new query generated or deleted

* Update test
2023-07-06 09:49:59 +02:00
v-darkora
ebfc9d4b36 Add welcome popop for query copilot (#1493) 2023-07-06 09:49:34 +02:00
sindhuba
4742ee0e7b Add new message type for NPS (#1520)
* Add new message type for NPS Survey

* Add new message type for NPS
2023-07-05 14:16:59 -07:00
sunghyunkang1111
b98829109e Do not show network connection message when securedbyperimeter (#1517) 2023-07-05 09:47:05 -05:00
jawelton74
ea4563f840 Initial commit. (#1518) 2023-07-05 07:36:21 -07:00
Predrag Klepic
3a961870d9 [Query Copilot] Polishing UI of Query Copilot (#1513)
* Implementation of filtering suggestion and history

* Error message if query is not received

* Exclamation mark on fail and button disabled

* Changed from hook to const for suggestions

* Test snapshots and formatting updated

* Fix based on comment

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-03 22:49:40 +02:00
132 changed files with 6655 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/Tree/ResourceTreeAdapter.tsx
__mocks__/monaco-editor.ts
src/Explorer/Tree/ResourceTree.tsx
src/Explorer/Tree/ResourceTree.tsx
src/Utils/PriorityBasedExecutionUtils.ts

View File

@@ -0,0 +1,3 @@
<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"/>
</svg>

After

Width:  |  Height:  |  Size: 363 B

3
images/CopilotFlash.svg Normal file
View File

@@ -0,0 +1,3 @@
<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"/>
</svg>

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

3
images/CopilotThumb.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5806 0.051598C6.83006 -0.157 6.24411 0.402481 6.03494 0.923322C5.79411 1.52303 5.58243 1.94395 5.32941 2.44707C5.17287 2.75833 5.00052 3.10106 4.79565 3.53704C4.32141 4.54625 3.84755 5.19347 3.5035 5.58154C3.33128 5.77579 3.19093 5.90585 3.09835 5.98435C3.05204 6.02362 3.01761 6.05005 2.99704 6.0652L2.9809 6.07684L1.109 7.18119C0.272443 7.67473 -0.0885815 8.69797 0.253045 9.6072L0.773038 10.9912C0.989444 11.5671 1.45891 12.0114 2.04591 12.1958L7.40179 13.8781C8.73654 14.2974 10.1555 13.5397 10.5497 12.1973L11.9139 7.55127C12.29 6.2705 11.3298 4.9878 9.99492 4.9878H8.60995C8.67586 4.76117 8.74339 4.50906 8.80466 4.24751C8.93612 3.68641 9.04781 3.04484 9.03753 2.51008C9.02797 2.01293 8.97781 1.49126 8.77353 1.04807C8.55437 0.572583 8.1709 0.21566 7.5806 0.051598ZM2.9768 6.07969L2.97492 6.08097L2.9768 6.07969Z" fill="#0078D4"/>
</svg>

After

Width:  |  Height:  |  Size: 952 B

View File

@@ -0,0 +1,29 @@
<svg width="287" height="225" viewBox="0 0 287 225" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M7.75456 192.647H231.829C233.524 192.647 235.156 191.988 236.381 190.764C237.605 189.539 238.265 187.907 238.265 186.211V33.061C238.265 29.5132 234.56 26.248 229.568 26.248L10.8314 29.576C9.98368 29.576 9.16738 29.733 8.38248 30.047C7.59758 30.3923 6.90687 30.8319 6.31034 31.4598C5.71381 32.0563 5.24287 32.747 4.89751 33.5319C4.55216 34.3168 4.42658 35.1645 4.42658 35.9808L1.31836 186.274C1.31836 187.969 1.97767 189.602 3.20212 190.795C4.42657 191.988 6.05917 192.647 7.75456 192.647Z" fill="#1F1D20"/>
<path d="M231.264 23.3906H6.37342C2.85705 23.3906 0 26.2477 0 29.764V183.228C0 186.745 2.85705 189.602 6.37342 189.602H231.295C234.811 189.602 237.669 186.745 237.669 183.228V29.764C237.637 26.2477 234.78 23.3906 231.264 23.3906Z" fill="url(#paint0_linear_1151_179507)"/>
<path d="M228.313 32.7148H119.62V178.707H228.313V32.7148Z" fill="white"/>
<path d="M121.158 32.7148H12.4648V178.707H121.158V32.7148Z" fill="#CDCDD0"/>
<path d="M34.1909 0V144.799C34.1909 144.799 63.2009 141.91 86.7166 153.715C110.264 165.52 121.158 178.581 121.158 178.581V33.7508C121.158 33.7508 107.595 16.483 86.7166 10.1095C69.7313 4.8978 34.1909 0 34.1909 0Z" fill="#EAEAEA"/>
<path d="M141.094 102.383L134.249 99.4006L127.374 102.383V23.3906H141.094V102.383Z" fill="url(#paint1_linear_1151_179507)"/>
<path opacity="0.15" d="M206.9 61.2871H159.304C157.231 61.2871 155.567 62.9825 155.567 65.0233C155.567 67.0954 157.263 68.7594 159.304 68.7594H206.9C208.972 68.7594 210.636 67.064 210.636 65.0233C210.668 62.9511 208.972 61.2871 206.9 61.2871Z" fill="#1F1D20"/>
<path opacity="0.15" d="M206.9 77.5498H159.304C157.231 77.5498 155.567 79.2452 155.567 81.2859C155.567 83.3581 157.263 85.0221 159.304 85.0221H206.9C208.972 85.0221 210.636 83.3267 210.636 81.2859C210.668 79.2138 208.972 77.5498 206.9 77.5498Z" fill="#1F1D20"/>
<path opacity="0.15" fill-rule="evenodd" clip-rule="evenodd" d="M159.304 93.8428H206.838C208.91 93.8428 210.574 95.5381 210.574 97.5789C210.574 99.651 208.878 101.315 206.838 101.315H159.304C157.232 101.315 155.568 99.6196 155.568 97.5789C155.536 95.5381 157.2 93.8428 159.304 93.8428Z" fill="#1F1D20"/>
<path d="M206.9 60.0303H159.304C157.231 60.0303 155.567 61.7257 155.567 63.7664C155.567 65.8385 157.263 67.5025 159.304 67.5025H206.9C208.972 67.5025 210.636 65.8072 210.636 63.7664C210.668 61.6943 208.972 60.0303 206.9 60.0303Z" fill="#50E6FF"/>
<path d="M206.9 76.3262H159.304C157.231 76.3262 155.567 78.0216 155.567 80.0623C155.567 82.1345 157.263 83.7984 159.304 83.7984H206.9C208.972 83.7984 210.636 82.1031 210.636 80.0623C210.668 77.9902 208.972 76.3262 206.9 76.3262Z" fill="#32B0E7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M159.304 92.5889H206.838C208.91 92.5889 210.574 94.2843 210.574 96.325C210.574 98.3972 208.878 100.061 206.838 100.061H159.304C157.232 100.093 155.568 98.3658 155.568 96.325C155.536 94.2843 157.2 92.5889 159.304 92.5889Z" fill="#185A97"/>
<path opacity="0.2" d="M287 143.834H192V205.096C192 207.687 194.089 209.776 196.68 209.776H206.648V224.265L221.243 209.776H282.32C284.911 209.776 287 207.687 287 205.096V143.834Z" fill="#1F1D21"/>
<path d="M283.827 140H195.173C193.428 140 192 141.428 192 143.173V202.769C192 204.514 193.428 205.942 195.173 205.942H206.648V220.431L221.243 205.942H283.827C285.572 205.942 287 204.514 287 202.769V143.173C287 141.428 285.572 140 283.827 140Z" fill="#49C8EF"/>
<path d="M254.24 161.762H224.706C223.543 161.762 222.591 162.714 222.591 163.877C222.591 165.04 223.543 165.992 224.706 165.992H254.24C255.403 165.992 256.355 165.04 256.355 163.877C256.355 162.714 255.403 161.762 254.24 161.762Z" fill="#C3F1FF"/>
<path d="M254.24 171.042H224.706C223.543 171.042 222.591 171.994 222.591 173.157C222.591 174.321 223.543 175.272 224.706 175.272H254.24C255.403 175.272 256.355 174.321 256.355 173.157C256.355 172.02 255.403 171.042 254.24 171.042Z" fill="#C3F1FF"/>
<path d="M254.24 180.373H224.706C223.543 180.373 222.591 181.325 222.591 182.488C222.591 183.652 223.543 184.604 224.706 184.604H254.24C255.403 184.604 256.355 183.652 256.355 182.488C256.355 181.298 255.403 180.373 254.24 180.373Z" fill="#C3F1FF"/>
<defs>
<linearGradient id="paint0_linear_1151_179507" x1="118.845" y1="34.0854" x2="118.845" y2="202.888" gradientUnits="userSpaceOnUse">
<stop stop-color="#007ED8"/>
<stop offset="0.7065" stop-color="#002D4C"/>
</linearGradient>
<linearGradient id="paint1_linear_1151_179507" x1="87.2694" y1="-23.1274" x2="250.063" y2="275.059" 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="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

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 0C8.73438 0 9.44271 0.0963542 10.125 0.289062C10.8073 0.476562 11.4427 0.744792 12.0312 1.09375C12.625 1.44271 13.1641 1.86198 13.6484 2.35156C14.138 2.83594 14.5573 3.375 14.9062 3.96875C15.2552 4.55729 15.5234 5.19271 15.7109 5.875C15.9036 6.55729 16 7.26562 16 8C16 8.73438 15.9036 9.44271 15.7109 10.125C15.5234 10.8073 15.2552 11.4453 14.9062 12.0391C14.5573 12.6276 14.138 13.1667 13.6484 13.6562C13.1641 14.1406 12.625 14.5573 12.0312 14.9062C11.4427 15.2552 10.8073 15.526 10.125 15.7188C9.44271 15.9062 8.73438 16 8 16C7.26562 16 6.55729 15.9062 5.875 15.7188C5.19271 15.526 4.55469 15.2552 3.96094 14.9062C3.3724 14.5573 2.83333 14.1406 2.34375 13.6562C1.85938 13.1667 1.44271 12.6276 1.09375 12.0391C0.744792 11.4453 0.473958 10.8073 0.28125 10.125C0.09375 9.44271 0 8.73438 0 8C0 7.26562 0.09375 6.55729 0.28125 5.875C0.473958 5.19271 0.744792 4.55729 1.09375 3.96875C1.44271 3.375 1.85938 2.83594 2.34375 2.35156C2.83333 1.86198 3.3724 1.44271 3.96094 1.09375C4.55469 0.744792 5.19271 0.476562 5.875 0.289062C6.55729 0.0963542 7.26562 0 8 0ZM8 15C8.64583 15 9.26562 14.9167 9.85938 14.75C10.4583 14.5833 11.0156 14.349 11.5312 14.0469C12.0521 13.7396 12.5234 13.375 12.9453 12.9531C13.3724 12.526 13.737 12.0547 14.0391 11.5391C14.3464 11.0182 14.5833 10.4609 14.75 9.86719C14.9167 9.26823 15 8.64583 15 8C15 7.35417 14.9167 6.73438 14.75 6.14062C14.5833 5.54167 14.3464 4.98438 14.0391 4.46875C13.737 3.94792 13.3724 3.47656 12.9453 3.05469C12.5234 2.6276 12.0521 2.26302 11.5312 1.96094C11.0156 1.65365 10.4583 1.41667 9.85938 1.25C9.26562 1.08333 8.64583 1 8 1C7.35417 1 6.73177 1.08333 6.13281 1.25C5.53906 1.41667 4.98177 1.65365 4.46094 1.96094C3.94531 2.26302 3.47396 2.6276 3.04688 3.05469C2.625 3.47656 2.26042 3.94792 1.95312 4.46875C1.65104 4.98438 1.41667 5.54167 1.25 6.14062C1.08333 6.73438 1 7.35417 1 8C1 8.64583 1.08333 9.26823 1.25 9.86719C1.41667 10.4609 1.65104 11.0182 1.95312 11.5391C2.26042 12.0547 2.625 12.526 3.04688 12.9531C3.47396 13.375 3.94531 13.7396 4.46094 14.0469C4.98177 14.349 5.53906 14.5833 6.13281 14.75C6.73177 14.9167 7.35417 15 8 15ZM11.4609 5.24219L8.71094 8L11.4609 10.7578L10.7578 11.4609L8 8.71094L5.24219 11.4609L4.53906 10.7578L7.28906 8L4.53906 5.24219L5.24219 4.53906L8 7.28906L10.7578 4.53906L11.4609 5.24219Z" fill="#E00B1C"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

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

View File

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

View File

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

20
package-lock.json generated
View File

@@ -164,6 +164,7 @@
"jest": "26.6.3",
"jest-canvas-mock": "2.3.1",
"jest-playwright-preset": "1.5.1",
"jest-react-hooks-shallow": "1.5.1",
"jest-trx-results-processor": "0.0.7",
"less": "3.8.1",
"less-loader": "4.1.0",
@@ -19578,6 +19579,16 @@
"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": {
"version": "24.9.0",
"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": {
"version": "24.9.0",
"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-canvas-mock": "2.3.1",
"jest-playwright-preset": "1.5.1",
"jest-react-hooks-shallow": "1.5.1",
"jest-trx-results-processor": "0.0.7",
"less": "3.8.1",
"less-loader": "4.1.0",
@@ -232,4 +233,4 @@
"prettier": {
"printWidth": 120
}
}
}

View File

@@ -358,6 +358,7 @@ export enum ContainerStatusType {
export enum PoolIdType {
DefaultPoolId = "default",
QueryCopilot = "query-copilot",
}
export const EmulatorMasterKey =
@@ -429,30 +430,101 @@ export class JunoEndpoints {
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 QueryCopilotSampleContainerId = "SampleContainer";
export const QueryCopilotSampleContainerSchema = {
product: {
sampleData: {
id: "de6fadec-0384-43c8-93ea-16c0170b5460",
name: "Premium Phone Mini (Red)",
price: 652.04,
id: "c415e70f-9bf5-4cda-aebe-a290cb8b94c2",
name: "Amazing Phone 3000 (Black)",
price: 223.33,
category: "Electronics",
description:
"This Premium Phone Mini (Red) is designed by the company under agreement with the FCC, so we'd give it a pass in either direction, but no one should be using this handset without a compatible handset. All in all, this phone is one of the most affordable Android handsets out there at $100. Check them out.\n\n9. HTC One M9 4",
stock: 74,
countryOfOrigin: "Mexico",
firstAvailable: "2018-07-07 17:42:28",
priceHistory: [592.81],
"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: 84,
countryOfOrigin: "USA",
firstAvailable: "2018-09-07 19:41:44",
priceHistory: [238.68, 234.7, 221.49, 205.88, 220.15],
customerRatings: [
{ username: "dannyhowell", stars: 1, date: "2022-03-12 17:01:23", verifiedUser: true },
{ username: "lindsay26", stars: 1, date: "2022-12-29 07:18:20", verifiedUser: false },
{ username: "smithmiguel", stars: 3, date: "2022-09-08 11:43:27", verifiedUser: false },
{ username: "julie07", stars: 3, date: "2021-03-14 23:54:10", verifiedUser: true },
{ username: "kelly93", stars: 3, date: "2022-11-05 12:45:43", verifiedUser: false },
{ username: "katherinereynolds", stars: 2, date: "2022-09-14 11:49:36", verifiedUser: false },
{ username: "chandlerkenneth", stars: 1, date: "2022-01-14 12:14:43", verifiedUser: true },
{
username: "steven66",
firstName: "Carol",
gender: "female",
lastName: "Shelton",
age: "25-35",
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,
},
@@ -494,6 +566,24 @@ export const QueryCopilotSampleContainerSchema = {
username: {
type: "string",
},
firstName: {
type: "string",
},
gender: {
type: "string",
},
lastName: {
type: "string",
},
age: {
type: "string",
},
area: {
type: "string",
},
address: {
type: "string",
},
stars: {
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 { EmulatorMasterKey, HttpHeaders } from "./Constants";
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;
@@ -105,6 +108,13 @@ export function client(): Cosmos.CosmosClient {
if (configContext.PROXY_PATH !== undefined) {
(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);
return _client;
}

View File

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

View File

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

View File

@@ -1,14 +1,14 @@
import {
allowedAadEndpoints,
allowedArcadiaEndpoints,
allowedArmEndpoints,
allowedBackendEndpoints,
allowedEmulatorEndpoints,
allowedGraphEndpoints,
allowedHostedExplorerEndpoints,
allowedJunoOrigins,
allowedMongoBackendEndpoints,
allowedMsalRedirectEndpoints,
defaultAllowedArmEndpoints,
defaultAllowedBackendEndpoints,
validateEndpoint,
} from "Utils/EndpointValidation";
@@ -20,6 +20,8 @@ export enum Platform {
export interface ConfigContext {
platform: Platform;
allowedArmEndpoints: ReadonlyArray<string>;
allowedBackendEndpoints: ReadonlyArray<string>;
allowedParentFrameOrigins: ReadonlyArray<string>;
gitSha?: string;
proxyPath?: string;
@@ -49,6 +51,8 @@ export interface ConfigContext {
// Default configuration
let configContext: Readonly<ConfigContext> = {
platform: Platform.Portal,
allowedArmEndpoints: defaultAllowedArmEndpoints,
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
allowedParentFrameOrigins: [
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
@@ -77,7 +81,7 @@ let configContext: Readonly<ConfigContext> = {
export function resetConfigContext(): void {
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;
}
@@ -87,7 +91,7 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
return;
}
if (!validateEndpoint(newContext.ARM_ENDPOINT, allowedArmEndpoints)) {
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
delete newContext.ARM_ENDPOINT;
}
@@ -107,7 +111,12 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
delete newContext.ARCADIA_ENDPOINT;
}
if (!validateEndpoint(newContext.BACKEND_ENDPOINT, allowedBackendEndpoints)) {
if (
!validateEndpoint(
newContext.BACKEND_ENDPOINT,
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints
)
) {
delete newContext.BACKEND_ENDPOINT;
}
@@ -130,7 +139,7 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
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") {
const port: string = process.env.PORT || "1234";
updateConfigContext({

View File

@@ -38,6 +38,7 @@ export enum MessageTypes {
OpenPostgreSQLPasswordReset,
OpenPostgresNetworkingBlade,
OpenCosmosDBNetworkingBlade,
DisplayNPSSurvey,
}
export { Versions, ActionContracts, Diagnostics };

View File

@@ -1,4 +1,7 @@
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
import { useDatabases } from "Explorer/useDatabases";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import React from "react";
import AddCollectionIcon from "../../images/AddCollection.svg";
import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
@@ -62,13 +65,6 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
return items;
};
const isQueryCopilotMoreMenu = (selectedCollection: ViewModels.Collection): boolean => {
return (
selectedCollection.databaseId === QueryCopilotSampleDatabaseId &&
selectedCollection.id() === QueryCopilotSampleContainerId
);
};
export const createCollectionContextMenuButton = (
container: Explorer,
selectedCollection: ViewModels.Collection
@@ -103,49 +99,74 @@ export const createCollectionContextMenuButton = (
});
}
if (!isQueryCopilotMoreMenu(selectedCollection)) {
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: AddStoredProcedureIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
},
label: "New Stored Procedure",
});
items.push({
iconSrc: AddUdfIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
},
label: "New UDF",
});
items.push({
iconSrc: AddTriggerIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined);
},
label: "New Trigger",
});
}
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: AddStoredProcedureIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
},
label: "New Stored Procedure",
});
items.push({
iconSrc: DeleteCollectionIcon,
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
"Delete " + getCollectionName(),
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />
),
label: `Delete ${getCollectionName()}`,
styleClass: "deleteCollectionMenuItem",
iconSrc: AddUdfIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
},
label: "New UDF",
});
items.push({
iconSrc: AddTriggerIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined);
},
label: "New Trigger",
});
}
items.push({
iconSrc: DeleteCollectionIcon,
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
"Delete " + getCollectionName(),
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />
),
label: `Delete ${getCollectionName()}`,
styleClass: "deleteCollectionMenuItem",
});
return items;
};
export const createSampleCollectionContextMenuButton = (): TreeNodeMenuItem[] => {
const items: TreeNodeMenuItem[] = [];
if (userContext.apiType === "SQL") {
const copilotVersion = userContext.features.copilotVersion;
if (copilotVersion === "v1.0") {
items.push({
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;
};

View File

@@ -25,6 +25,8 @@ export class AccordionComponent extends React.Component<AccordionComponentProps>
export interface AccordionItemComponentProps {
title: string;
isExpanded?: boolean;
containerStyles?: React.CSSProperties;
styles?: React.CSSProperties;
}
interface AccordionItemComponentState {
@@ -53,14 +55,19 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
}
public render(): JSX.Element {
const { containerStyles, styles } = this.props;
return (
<div className="accordionItemContainer">
<div className="accordionItemContainer" style={{ ...containerStyles }}>
<div className="accordionItemHeader" onClick={this.onHeaderClick} onKeyPress={this.onHeaderKeyPress}>
{this.renderCollapseExpandIcon()}
{this.props.title}
</div>
<div className="accordionItemContent">
<AnimateHeight duration={AccordionItemComponent.durationMS} height={this.state.isExpanded ? "auto" : 0}>
<AnimateHeight
style={{ ...styles }}
duration={AccordionItemComponent.durationMS}
height={this.state.isExpanded ? "auto" : 0}
>
{this.props.children}
</AnimateHeight>
</div>

View File

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

View File

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

View File

@@ -31,6 +31,13 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
public componentDidMount(): void {
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) {

View File

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

View File

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

View File

@@ -1,5 +1,8 @@
import { Link } from "@fluentui/react/lib/Link";
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 { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
import * as ko from "knockout";
@@ -23,7 +26,7 @@ import { PhoenixClient } from "../Phoenix/PhoenixClient";
import * as ExplorerSettings from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../UserContext";
import { isAccountNewerThanThresholdInMs, userContext } from "../UserContext";
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
import { stringToBlob } from "../Utils/BlobUtils";
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
@@ -258,6 +261,45 @@ export default class Explorer {
// 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> {
const databaseId = userContext.parsedResourceToken?.databaseId;
const collectionId = userContext.parsedResourceToken?.collectionId;
@@ -343,7 +385,7 @@ export default class Explorer {
this._isInitializingNotebooks = false;
}
public async allocateContainer(): Promise<void> {
public async allocateContainer(poolId: PoolIdType): Promise<void> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
const isAllocating = useNotebook.getState().isAllocating;
if (
@@ -353,7 +395,7 @@ export default class Explorer {
) {
const provisionData: IProvisionData = {
cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint,
poolId: PoolIdType.DefaultPoolId,
poolId: poolId === PoolIdType.DefaultPoolId ? undefined : poolId,
};
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Connecting,
@@ -608,6 +650,8 @@ export default class Explorer {
private _initSettings() {
if (!ExplorerSettings.hasSettingsDefined()) {
ExplorerSettings.createDefaultSettings();
} else {
ExplorerSettings.ensurePriorityLevel();
}
}
@@ -711,7 +755,7 @@ export default class Explorer {
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
}
if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) {
await this.allocateContainer();
await this.allocateContainer(PoolIdType.DefaultPoolId);
}
const notebookTabs = useTabs
@@ -936,7 +980,7 @@ export default class Explorer {
}
if (useNotebook.getState().isPhoenixNotebooks) {
if (isGithubTree) {
await this.allocateContainer();
await this.allocateContainer(PoolIdType.DefaultPoolId);
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
} else {
@@ -945,7 +989,7 @@ export default class Explorer {
undefined,
"Create",
async () => {
await this.allocateContainer();
await this.allocateContainer(PoolIdType.DefaultPoolId);
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
},
@@ -1024,7 +1068,7 @@ export default class Explorer {
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
if (useNotebook.getState().isPhoenixFeatures) {
await this.allocateContainer();
await this.allocateContainer(PoolIdType.DefaultPoolId);
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
this.connectToNotebookTerminal(kind);
@@ -1168,7 +1212,7 @@ export default class Explorer {
await useNotebook.getState().getPhoenixStatus();
}
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
@@ -1205,7 +1249,7 @@ export default class Explorer {
undefined,
"Upload",
async () => {
await this.allocateContainer();
await this.allocateContainer(PoolIdType.DefaultPoolId);
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePanel(parent);
},
@@ -1291,7 +1335,7 @@ export default class Explorer {
return;
}
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection);
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
useDatabases.setState({ sampleDataResourceTokenCollection });
}
}

View File

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

View File

@@ -1,3 +1,6 @@
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react";
import AddCollectionIcon from "../../../../images/AddCollection.svg";
import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
@@ -8,23 +11,23 @@ import AddUdfIcon from "../../../../images/AddUdf.svg";
import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg";
import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import GitHubIcon from "../../../../images/github.svg";
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
import GitHubIcon from "../../../../images/github.svg";
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
import SettingsIcon from "../../../../images/settings_15x15.svg";
import SynapseIcon from "../../../../images/synapse-link.svg";
import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants";
import { configContext, Platform } from "../../../ConfigContext";
import { Platform, configContext } from "../../../ConfigContext";
import * as ViewModels from "../../../Contracts/ViewModels";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { JunoClient } from "../../../Juno/JunoClient";
import { userContext } from "../../../UserContext";
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
@@ -35,7 +38,7 @@ import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel"
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
import { useDatabases } from "../../useDatabases";
import { SelectedNodeState } from "../../useSelectedNode";
import { SelectedNodeState, useSelectedNode } from "../../useSelectedNode";
let counter = 0;
@@ -144,7 +147,9 @@ export function createStaticCommandBarButtons(
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
newStoredProcedureBtn.children = createScriptCommandButtons(selectedNodeState);
@@ -289,7 +294,8 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
onCommandClick: () => container.openEnableSynapseLinkDialog(),
commandButtonLabel: label,
hasPopup: false,
disabled: useNotebook.getState().isSynapseLinkUpdating,
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() || useNotebook.getState().isSynapseLinkUpdating,
ariaLabel: label,
};
}
@@ -320,8 +326,13 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
iconSrc: AddSqlQueryIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
if (useSelectedNode.getState().isQueryCopilotCollectionSelected()) {
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
} else {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
}
},
commandButtonLabel: label,
ariaLabel: label,
@@ -366,7 +377,9 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newStoredProcedureBtn);
}
@@ -383,7 +396,9 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newUserDefinedFunctionBtn);
}
@@ -400,7 +415,9 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newTriggerBtn);
}
@@ -424,7 +441,7 @@ function createNewNotebookButton(container: Explorer): CommandButtonComponentPro
onCommandClick: () => container.onNewNotebookClicked(),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}
@@ -437,7 +454,7 @@ function createuploadNotebookButton(container: Explorer): CommandButtonComponent
onCommandClick: () => container.openUploadFilePanel(),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}
@@ -452,7 +469,7 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
};
}
@@ -465,7 +482,7 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
};
}
@@ -477,7 +494,7 @@ function createOpenTerminalButton(container: Explorer): CommandButtonComponentPr
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}
@@ -529,7 +546,8 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
function createOpenPsqlTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open PSQL Shell";
const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
(!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled) ||
useSelectedNode.getState().isQueryCopilotCollectionSelected();
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
@@ -556,7 +574,7 @@ function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonC
onCommandClick: () => container.resetNotebookWorkspace(),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}
@@ -582,7 +600,7 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
},
commandButtonLabel: label,
hasPopup: false,
disabled: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}

View File

@@ -9,7 +9,7 @@ import {
import * as React from "react";
import _ from "underscore";
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 * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
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 {
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 * as React from "react";
import "../../../../less/hostedexplorer.less";
import { ConnectionStatusType, ContainerStatusType, Notebook } from "../../../Common/Constants";
import { ConnectionStatusType, ContainerStatusType, Notebook, PoolIdType } from "../../../Common/Constants";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
import "../CommandBar/ConnectionStatusComponent.less";
interface Props {
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 [second, setSecond] = 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)
) {
return (
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer(poolId)}>
<TooltipHost content={toolTipContent}>
<Stack className="connectionStatusContainer" horizontal>
<Icon iconName="ConnectVirtualMachine" className="connectIcon" />
@@ -133,7 +134,9 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
id={buttonId}
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
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>

View File

@@ -5,18 +5,18 @@
import { Dropdown, IDropdownOption } from "@fluentui/react";
import * as React from "react";
import AnimateHeight from "react-animate-height";
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
import ClearIcon from "../../../../images/Clear-1.svg";
import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png";
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
import ErrorBlackIcon from "../../../../images/error_black.svg";
import ErrorRedIcon from "../../../../images/error_red.svg";
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
import InfoIcon from "../../../../images/info_color.svg";
import LoadingIcon from "../../../../images/loading.svg";
import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png";
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
import { userContext } from "../../../UserContext";
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
import { ConsoleData, ConsoleDataType } from "./ConsoleData";
export interface NotificationConsoleComponentProps {
@@ -122,7 +122,9 @@ export class NotificationConsoleComponent extends React.Component<
{userContext.features.pr && <PrPreview pr={userContext.features.pr} />}
<span className="consoleSplitter" />
<span className="headerStatus">
<span className="headerStatusEllipsis">{this.state.headerStatus}</span>
<span className="headerStatusEllipsis" aria-live="assertive" aria-atomic="true">
{this.state.headerStatus}
</span>
</span>
</div>
<div
@@ -256,6 +258,7 @@ export class NotificationConsoleComponent extends React.Component<
if (this.props.isConsoleExpanded && this.consoleHeaderElement) {
this.consoleHeaderElement.focus();
}
useNotificationConsole.getState().setConsoleAnimationFinished(true);
};
private updateConsoleData = (prevProps: NotificationConsoleComponentProps): void => {

View File

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

View File

@@ -1,23 +1,23 @@
import {
actions,
AppState,
castToSessionId,
ContentRef,
createKernelRef,
JupyterHostRecordProps,
ServerConfig as JupyterServerConfig,
KernelInfo,
KernelRef,
RemoteKernelProps,
actions,
castToSessionId,
createKernelRef,
selectors,
ServerConfig as JupyterServerConfig,
} from "@nteract/core";
import { Channels, childOf, createMessage, JupyterMessage, message, ofMessageType } from "@nteract/messaging";
import { Channels, childOf, createMessage, message, ofMessageType } from "@nteract/messaging";
import { defineConfigOption } from "@nteract/mythic-configuration";
import { RecordOf } from "immutable";
import { Action, AnyAction } from "redux";
import { ofType, StateObservable } from "redux-observable";
import { StateObservable, ofType } from "redux-observable";
import { kernels, sessions } from "rx-jupyter";
import { concat, EMPTY, from, interval, merge, Observable, Observer, of, Subject, Subscriber, timer } from "rxjs";
import { EMPTY, Observable, Observer, Subject, Subscriber, concat, from, interval, merge, of, timer } from "rxjs";
import {
catchError,
concatMap,
@@ -35,17 +35,17 @@ import {
import { webSocket } from "rxjs/webSocket";
import * as Constants from "../../../Common/Constants";
import { Areas } from "../../../Common/Constants";
import { useTabs } from "../../../hooks/useTabs";
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { ActionModifiers, Action as TelemetryAction } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
import { useTabs } from "../../../hooks/useTabs";
import { useDialog } from "../../Controls/Dialog";
import * as FileSystemUtil from "../FileSystemUtil";
import * as cdbActions from "../NotebookComponent/actions";
import { NotebookContentProviderType, NotebookUtil } from "../NotebookUtil";
import * as CdbActions from "./actions";
import * as TextFile from "./contents/file/text-file";
import { CdbAppState } from "./types";
import { CdbAppState, JupyterMessage } from "./types";
interface NotebookServiceConfig extends JupyterServerConfig {
userPuid?: string;
@@ -106,10 +106,8 @@ const formWebSocketURL = (serverConfig: NotebookServiceConfig, kernelId: string,
if (sessionId) {
params.append("session_id", sessionId);
}
const q = params.toString();
const suffix = q !== "" ? `?${q}` : "";
const url = (serverConfig.endpoint.slice(0, -1) || "") + `api/kernels/${kernelId}/channels${suffix}`;
return url.replace(/^http(s)?/, "ws$1");
@@ -241,10 +239,10 @@ const connect = (serverConfig: NotebookServiceConfig, kernelID: string, sessionI
...message,
header: {
session: sessionID,
token: serverConfig.token,
...message.header,
},
};
wsSubject.next(sessionizedMessage);
} else {
console.error("Message must be an object, the app sent", message);

View File

@@ -1,5 +1,6 @@
import { CellId } from "@nteract/commutable";
import { AppState } from "@nteract/core";
import { MessageType } from "@nteract/messaging";
import * as Immutable from "immutable";
import { Notebook } from "../../../Common/Constants";
@@ -53,3 +54,26 @@ export const makeCdbRecord = Immutable.Record<CdbRecordProps>({
pendingSnapshotRequest: undefined,
notebookSnapshotError: undefined,
});
export interface JupyterMessage<MT extends MessageType = MessageType, C = any> {
header: JupyterMessageHeader<MT>;
parent_header:
| JupyterMessageHeader<any>
| {
msg_id?: string;
};
metadata: object;
content: C;
channel: string;
buffers?: Uint8Array | null;
}
export interface JupyterMessageHeader<MT extends MessageType = MessageType> {
msg_id: string;
username: string;
date: string;
msg_type: MT;
version: string;
session: string;
token: string;
}

View File

@@ -1425,6 +1425,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
this.setState({ isExecuting: false });
TelemetryProcessor.traceSuccess(Action.CreateCollection, telemetryData, startKey);
useSidePanel.getState().closeSidePanel();
// open NPS Survey Dialog once the collection is created
this.props.explorer.openNPSSurveyDialog();
} catch (error) {
const errorMessage: string = getErrorMessage(error);
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 { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react";
import * as SharedConstants from "Shared/Constants";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { isServerlessAccount } from "Utils/CapabilityUtils";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer";
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
@@ -291,7 +291,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
aria-required="true"
required={true}
ariaLabel="addCollection-tableId"
ariaLabel="addCollection-table Id Create table"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"

View File

@@ -1,12 +1,12 @@
import { IDropdownOption } from "@fluentui/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 { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { useSidePanel } from "../../../hooks/useSidePanel";
import Explorer from "../../Explorer";
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
import { useNotebook } from "../../Notebook/useNotebook";
@@ -109,7 +109,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
};
isGithubTree = false;
if (useNotebook.getState().isPhoenixNotebooks) {
await container.allocateContainer();
await container.allocateContainer(PoolIdType.DefaultPoolId);
}
break;

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ export interface PanelContainerProps {
panelContent?: JSX.Element;
isConsoleExpanded: boolean;
isOpen: boolean;
isConsoleAnimationFinished?: boolean;
panelWidth?: string;
onRenderNavigationContent?: IRenderFunction<IPanelProps>;
}
@@ -28,7 +29,7 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
};
}
componentDidMount(): void {
omponentDidMount(): void {
window.addEventListener("resize", () => this.setState({ height: this.getPanelHeight() }));
}
@@ -36,6 +37,15 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
window.removeEventListener("resize", () => this.setState({ height: this.getPanelHeight() }));
}
componentDidUpdate(): void {
if (useNotificationConsole.getState().consoleAnimationFinished || this.state.height !== this.getPanelHeight()) {
this.setState({
height: this.getPanelHeight(),
});
useNotificationConsole.getState().setConsoleAnimationFinished(false);
}
}
render(): JSX.Element {
if (!this.props.panelContent) {
return <></>;
@@ -59,7 +69,7 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
header: { padding: "0 0 8px 34px" },
commands: { marginTop: 8 },
}}
style={{ height: this.getPanelHeight() }}
style={{ height: this.state.height }}
>
{this.props.panelContent}
</Panel>
@@ -75,16 +85,22 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
};
private getPanelHeight = (): string => {
const consoleHeight = this.props.isConsoleExpanded
? PanelContainerComponent.consoleContentHeight + PanelContainerComponent.consoleHeaderHeight
: PanelContainerComponent.consoleHeaderHeight;
const panelHeight = window.innerHeight - consoleHeight;
return panelHeight + "px";
const notificationConsole = document.getElementById("explorerNotificationConsole");
if (notificationConsole) {
return window.innerHeight - notificationConsole.clientHeight + "px";
}
return (
window.innerHeight -
(this.props.isConsoleExpanded
? PanelContainerComponent.consoleContentHeight + PanelContainerComponent.consoleHeaderHeight
: PanelContainerComponent.consoleHeaderHeight) +
"px"
);
};
}
export const SidePanel: React.FC = () => {
const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded);
const isConsoleAnimationFinished = useNotificationConsole((state) => state.consoleAnimationFinished);
const { isOpen, panelContent, panelWidth, headerText } = useSidePanel((state) => {
return {
isOpen: state.isOpen,
@@ -102,6 +118,7 @@ export const SidePanel: React.FC = () => {
headerText={headerText}
isConsoleExpanded={isConsoleExpanded}
panelWidth={panelWidth}
isConsoleAnimationFinished={isConsoleAnimationFinished}
/>
);
};

View File

@@ -1,16 +1,18 @@
import { PrimaryButton } from "@fluentui/react";
import React from "react";
import React, { CSSProperties } from "react";
export interface PanelFooterProps {
buttonLabel: string;
isButtonDisabled?: boolean;
style?: CSSProperties;
}
export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = ({
buttonLabel,
isButtonDisabled,
style,
}: PanelFooterProps): JSX.Element => (
<div className="panelFooter">
<div className="panelFooter" style={style}>
<PrimaryButton
type="submit"
id="sidePanelOkButton"

View File

@@ -1,4 +1,4 @@
import React, { FunctionComponent, ReactNode } from "react";
import React, { CSSProperties, FunctionComponent, ReactNode } from "react";
import { PanelFooterComponent } from "../PanelFooterComponent";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
import { PanelLoadingScreen } from "../PanelLoadingScreen";
@@ -11,6 +11,7 @@ export interface RightPaneFormProps {
isSubmitButtonHidden?: boolean;
isSubmitButtonDisabled?: boolean;
children?: ReactNode;
footerStyle?: CSSProperties;
}
export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
@@ -21,6 +22,7 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
isSubmitButtonHidden = false,
isSubmitButtonDisabled = false,
children,
footerStyle,
}: RightPaneFormProps) => {
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
@@ -33,7 +35,11 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
{formError && <PanelInfoErrorComponent messageType="error" message={formError} showErrorDetails={true} />}
{children}
{!isSubmitButtonHidden && (
<PanelFooterComponent buttonLabel={submitButtonText} isButtonDisabled={isSubmitButtonDisabled} />
<PanelFooterComponent
buttonLabel={submitButtonText}
isButtonDisabled={isSubmitButtonDisabled}
style={footerStyle}
/>
)}
</form>
{isExecuting && <PanelLoadingScreen />}

View File

@@ -139,10 +139,11 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
onSubmit: () => {
isSaveQueryEnabled() ? submit() : setupQueries();
},
footerStyle: isSaveQueryEnabled() ? { flexGrow: 0 } : {},
};
return (
<RightPaneForm {...props}>
<div className="panelFormWrapper">
<div className="panelFormWrapper" style={{ flexGrow: 1 }}>
<div className="panelMainContent">
{!isSaveQueryEnabled() ? (
<Text variant="small">{setupSaveQueriesText}</Text>

View File

@@ -2,6 +2,7 @@
exports[`Save Query Pane should render Default properly 1`] = `
<RightPaneForm
footerStyle={Object {}}
formError=""
isExecuting={false}
onSubmit={[Function]}
@@ -9,6 +10,11 @@ exports[`Save Query Pane should render Default properly 1`] = `
>
<div
className="panelFormWrapper"
style={
Object {
"flexGrow": 1,
}
}
>
<div
className="panelMainContent"

View File

@@ -2,12 +2,12 @@ import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui
import * as Constants from "Common/Constants";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { configContext } from "ConfigContext";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, MouseEvent, useState } from "react";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import * as StringUtility from "Shared/StringUtility";
import { userContext } from "UserContext";
import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, MouseEvent, useState } from "react";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export const SettingsPane: FunctionComponent = () => {
@@ -29,7 +29,7 @@ export const SettingsPane: FunctionComponent = () => {
const [crossPartitionQueryEnabled, setCrossPartitionQueryEnabled] = useState<boolean>(
LocalStorageUtility.hasItem(StorageKey.IsCrossPartitionQueryEnabled)
? LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
: true
: false
);
const [graphAutoVizDisabled, setGraphAutoVizDisabled] = useState<string>(
LocalStorageUtility.hasItem(StorageKey.IsGraphAutoVizDisabled)
@@ -41,12 +41,18 @@ export const SettingsPane: FunctionComponent = () => {
? LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism)
: Constants.Queries.DefaultMaxDegreeOfParallelism
);
const [priorityLevel, setPriorityLevel] = useState<string>(
LocalStorageUtility.hasItem(StorageKey.PriorityLevel)
? LocalStorageUtility.getEntryString(StorageKey.PriorityLevel)
: Constants.PriorityLevel.Default
);
const explorerVersion = configContext.gitSha;
const shouldShowQueryPageOptions = userContext.apiType === "SQL";
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
const shouldShowPriorityLevelOption =
userContext.features.enablePriorityBasedThrottling && userContext.apiType === "SQL";
const handlerOnSubmit = (e: MouseEvent<HTMLButtonElement>) => {
setIsExecuting(true);
@@ -58,6 +64,7 @@ export const SettingsPane: FunctionComponent = () => {
LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString());
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString());
if (shouldShowGraphAutoVizOption) {
LocalStorageUtility.setEntryBoolean(
@@ -76,6 +83,7 @@ export const SettingsPane: FunctionComponent = () => {
StorageKey.MaxDegreeOfParellism
)}`
);
logConsoleInfo(`Updated priority level setting to ${LocalStorageUtility.getEntryString(StorageKey.PriorityLevel)}`);
if (shouldShowGraphAutoVizOption) {
logConsoleInfo(
@@ -116,6 +124,18 @@ export const SettingsPane: FunctionComponent = () => {
{ 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 => {
setPageOption(option.key);
};
@@ -260,6 +280,29 @@ export const SettingsPane: FunctionComponent = () => {
</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 && (
<div className="settingsSection">
<div className="settingsSectionPart">

View File

@@ -142,7 +142,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</div>
<StyledCheckboxBase
ariaLabel="Enable cross partition query"
checked={true}
checked={false}
className="padding"
onChange={[Function]}
styles={

View File

@@ -371,6 +371,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
autoFocus={true}
id="confirmDatabaseId"
onChange={[Function]}
required={true}
styles={
Object {
"fieldGroup": Object {
@@ -385,6 +386,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
deferredValidationTime={200}
id="confirmDatabaseId"
onChange={[Function]}
required={true}
resizable={true}
styles={[Function]}
theme={
@@ -663,7 +665,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
validateOnLoad={true}
>
<div
className="ms-TextField root-58"
className="ms-TextField is-required root-58"
>
<div
className="ms-TextField-wrapper"
@@ -681,6 +683,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
onChange={[Function]}
onFocus={[Function]}
onInput={[Function]}
required={true}
type="text"
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,11 +10,13 @@ import {
Text,
TextField,
} 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 React from "react";
import { getUserEmail } from "../../../Utils/UserUtils";
export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
export const QueryCopilotFeedbackModal = ({ explorer }: { explorer: Explorer }): JSX.Element => {
const {
generatedQuery,
userPrompt,
@@ -26,6 +28,8 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
const [isContactAllowed, setIsContactAllowed] = React.useState<boolean>(true);
const [description, setDescription] = React.useState<string>("");
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
const [contact, setContact] = React.useState<string>(getUserEmail());
return (
<Modal isOpen={showFeedbackModal}>
<Stack style={{ padding: 24 }}>
@@ -69,14 +73,19 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
{ key: "no", text: "No, do not contact me." },
]}
selectedKey={isContactAllowed ? "yes" : "no"}
onChange={(_, option) => setIsContactAllowed(option.key === "yes")}
onChange={(_, option) => {
setIsContactAllowed(option.key === "yes");
setContact(option.key === "yes" ? getUserEmail() : "");
}}
></ChoiceGroup>
<Text style={{ fontSize: 12, marginBottom: 14 }}>
By pressing submit, your feedback will be used to improve Microsoft products and services. IT admins for your
organization will be able to view and manage your feedback data.{" "}
<Link href="" target="_blank">
Privacy statement
</Link>
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the{" "}
{
<Link href="https://privacy.microsoft.com/privacystatement" target="_blank">
Privacy statement
</Link>
}{" "}
for more information.
</Text>
{likeQuery && (
<Checkbox
@@ -92,7 +101,10 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
onClick={() => {
closeFeedbackModal();
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
submitFeedback({ generatedQuery, likeQuery, description, userPrompt });
SubmitFeedback({
params: { generatedQuery, likeQuery, description, userPrompt, contact },
explorer: explorer,
});
}}
>
Submit

View File

@@ -0,0 +1,47 @@
.modalContentPadding {
padding-top: 15px;
width: 513px;
height: 638px;
}
.exitPadding {
padding: 0px 7px 0px 0px;
}
.previewMargin {
margin: 8px 10px 0px 0;
}
.preview {
padding: 0px 4px;
background: #f0f0f0;
border-radius: 8px;
font-size: 10px;
}
.exitIcon {
margin: 3px 7px 0px 0px;
color: #424242;
}
.text {
width: 348px;
padding: 8px 16px 8px 16px;
margin-left: 25px;
}
.bold {
font-weight: 600;
}
.imageTextPadding {
padding: 0px 5px 0px 0px;
}
.buttonPadding {
padding: 15px 0px 0px 0px;
}
.tryButton {
border-radius: 4px;
}

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

@@ -0,0 +1,115 @@
import { IconButton, Image, Link, Modal, PrimaryButton, Stack, StackItem, Text } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import React from "react";
import Database from "../../../../images/CopilotDatabase.svg";
import Flash from "../../../../images/CopilotFlash.svg";
import Thumb from "../../../../images/CopilotThumb.svg";
import CoplilotWelcomeIllustration from "../../../../images/CopliotWelcomeIllustration.svg";
import "./WelcomeModal.css";
export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element => {
const [isModalVisible, { setFalse: hideModal }] = useBoolean(visible);
React.useEffect(() => {
if (visible) {
window.localStorage.setItem("hideWelcomeModal", "true");
}
});
return (
<>
<Modal isOpen={isModalVisible} onDismiss={hideModal} isBlocking={false}>
<Stack className="modalContentPadding">
<Stack horizontal>
<Stack horizontal grow={4} horizontalAlign="end">
<Stack.Item>
<Image src={CoplilotWelcomeIllustration} />
</Stack.Item>
</Stack>
<Stack horizontal grow={1} horizontalAlign="end" verticalAlign="start" className="exitPadding">
<Stack.Item className="previewMargin">
<Text className="preview">Preview</Text>
</Stack.Item>
<Stack.Item>
<IconButton
onClick={hideModal}
iconProps={{ iconName: "Cancel" }}
title="Exit"
ariaLabel="Exit"
className="exitIcon"
/>
</Stack.Item>
</Stack>
</Stack>
<Stack horizontalAlign="center">
<Stack.Item align="center" style={{ textAlign: "center" }}>
<Text className="title bold">Welcome to Query Copilot for Azure Cosmos DB (Private Preview)</Text>
</Stack.Item>
<Stack.Item align="center" className="text">
<Stack horizontal>
<StackItem align="start" className="imageTextPadding">
<Image src={Flash} />
</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 />
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
</Text>
</Stack.Item>
<Stack.Item align="center" className="text">
<Stack horizontal>
<StackItem align="start" className="imageTextPadding">
<Image src={Thumb} />
</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 />
<Link href="http://aka.ms/cdb-copilot-preview-terms">Read preview terms</Link>
</Text>
</Stack.Item>
<Stack.Item align="center" className="text">
<Stack horizontal>
<StackItem align="start" className="imageTextPadding">
<Image src={Database} />
</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 />
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
</Text>
</Stack.Item>
</Stack>
<Stack className="buttonPadding">
<Stack.Item align="center">
<PrimaryButton onClick={hideModal} className="tryButton">
Try Copilot
</PrimaryButton>
</Stack.Item>
</Stack>
</Stack>
</Modal>
</>
);
};

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 React from "react";
import { any } from "underscore";
import { CopyPopup } from "./CopyPopup";
describe("Copy Popup snapshot test", () => {
const setShowCopyPopupMock = jest.fn();
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();
});
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,11 +1,113 @@
import { shallow } from "enzyme";
import { mount, shallow } from "enzyme";
import React from "react";
import { any } from "underscore";
import { DeletePopup } from "./DeletePopup";
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", () => {
const wrapper = shallow(<DeletePopup showDeletePopup={true} setShowDeletePopup={() => any} setQuery={() => any} />);
const wrapper = shallow(
<DeletePopup
showDeletePopup={true}
setShowDeletePopup={setShowDeletePopupMock}
setQuery={setQueryMock}
clearFeedback={clearFeedbackMock}
showFeedbackBar={showFeedbackBarMock}
/>
);
expect(wrapper.find("Modal").prop("isOpen")).toBeTruthy();
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

@@ -5,14 +5,20 @@ export const DeletePopup = ({
showDeletePopup,
setShowDeletePopup,
setQuery,
clearFeedback,
showFeedbackBar,
}: {
showDeletePopup: boolean;
setShowDeletePopup: Dispatch<SetStateAction<boolean>>;
setQuery: Dispatch<SetStateAction<string>>;
clearFeedback: Dispatch<SetStateAction<void>>;
showFeedbackBar: Dispatch<SetStateAction<boolean>>;
}): JSX.Element => {
const deleteCode = () => {
setQuery("");
setShowDeletePopup(false);
clearFeedback();
showFeedbackBar(false);
};
return (

View File

@@ -1,5 +1,7 @@
// 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`] = `
<Stack
style={

View File

@@ -1,5 +1,83 @@
// 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`] = `
<Modal
isOpen={true}

View File

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

View File

@@ -12,55 +12,55 @@ import {
Separator,
Spinner,
Stack,
TeachingBubble,
Text,
TextField,
} from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import {
PoolIdType,
QueryCopilotSampleContainerId,
QueryCopilotSampleContainerSchema,
QueryCopilotSampleDatabaseId,
ShortenedQueryCopilotSampleContainerSchema,
} from "Common/Constants";
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
import { MinimalQueryIterator } from "Common/IteratorUtilities";
import { queryDocuments } from "Common/dataAccess/queryDocuments";
import { createUri } from "Common/UrlUtility";
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
import { QueryResults } from "Contracts/ViewModels";
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal";
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/SamplePrompts/SamplePrompts";
import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
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 { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel";
import React, { useState } from "react";
import React, { useRef, useState } from "react";
import SplitterLayout from "react-splitter-layout";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import HintIcon from "../../../images/Hint.svg";
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
import RecentIcon from "../../../images/Recent.svg";
import SamplePromptsIcon from "../../../images/SamplePromptsIcon.svg";
import XErrorMessage from "../../../images/X-errorMessage.svg";
import SaveQueryIcon from "../../../images/save-cosmos.svg";
import { useTabs } from "../../hooks/useTabs";
interface QueryCopilotTabProps {
initialInput: string;
explorer: Explorer;
}
interface GenerateSQLQueryResponse {
apiVersion: string;
sql: string;
explanation: string;
generateStart: string;
generateEnd: string;
interface SuggestedPrompt {
id: number;
text: string;
}
const promptStyles: IButtonStyles = {
@@ -68,27 +68,52 @@ const promptStyles: IButtonStyles = {
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
};
export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
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 [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 [isSamplePromptsOpen, setIsSamplePromptsOpen] = useState<boolean>(false);
const [showDeletePopup, setShowDeletePopup] = useState<boolean>(false);
const [showFeedbackBar, setShowFeedbackBar] = useState<boolean>(false);
const [showCopyPopup, setshowCopyPopup] = useState<boolean>(false);
export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
const inputEdited = useRef(false);
const {
hideFeedbackModalForLikedQueries,
userPrompt,
setUserPrompt,
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 = {
isSamplePromptsOpen: isSamplePromptsOpen,
@@ -114,43 +139,101 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
};
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
const cachedHistories = cachedHistoriesString?.split(",");
const cachedHistories = cachedHistoriesString?.split("|");
const [histories, setHistories] = useState<string[]>(cachedHistories || []);
const suggestedPrompts: SuggestedPrompt[] = [
{ id: 1, text: 'Show all products that have the word "ultra" in the name or description' },
{ id: 2, text: "What are all of the possible categories for the products, and their counts?" },
{ 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 [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts);
const handleUserPromptChange = (event: React.ChangeEvent<HTMLInputElement>) => {
inputEdited.current = true;
const { value } = event.target;
setUserPrompt(value);
// Filter history prompts
const filteredHistory = histories.filter((history) => history.toLowerCase().includes(value.toLowerCase()));
setFilteredHistories(filteredHistory);
// Filter suggested prompts
const filteredSuggested = suggestedPrompts.filter((prompt) =>
prompt.text.toLowerCase().includes(value.toLowerCase())
);
setFilteredSuggestedPrompts(filteredSuggested);
};
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);
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join(","));
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join("|"));
};
const generateSQLQuery = async (): Promise<void> => {
try {
if (shouldAllocateContainer && userContext.features.enableCopilotPhoenixGateaway) {
await explorer.allocateContainer(PoolIdType.QueryCopilot);
setShouldAllocateContainer(false);
}
setIsGeneratingQuery(true);
useTabs.getState().setIsTabExecuting(true);
useTabs.getState().setIsQueryErrorThrown(false);
const payload = {
containerSchema: QueryCopilotSampleContainerSchema,
containerSchema: userContext.features.enableCopilotFullSchema
? QueryCopilotSampleContainerSchema
: ShortenedQueryCopilotSampleContainerSchema,
userPrompt: userPrompt,
};
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",
headers: {
"content-type": "application/json",
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
},
body: JSON.stringify(payload),
});
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
if (generateSQLQueryResponse?.sql) {
let query = `-- **Prompt:** ${userPrompt}\r\n`;
if (generateSQLQueryResponse.explanation) {
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
if (response.status === 404) {
setShouldAllocateContainer(true);
}
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;
setQuery(query);
setGeneratedQuery(generateSQLQueryResponse.sql);
} else {
handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
useTabs.getState().setIsQueryErrorThrown(true);
setShowErrorMessageBar(true);
}
} catch (error) {
handleError(error, "executeNaturalLanguageQuery");
useTabs.getState().setIsQueryErrorThrown(true);
setShowErrorMessageBar(true);
throw error;
} finally {
setIsGeneratingQuery(false);
@@ -160,8 +243,15 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
};
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 queryIterator = queryDocuments(QueryCopilotSampleDatabaseId, QueryCopilotSampleContainerId, queryToExecute, {
const queryIterator = querySampleDocuments(queryToExecute, {
enableCrossPartitionQuery: shouldEnableCrossPartitionKey(),
} as FeedOptions);
setQueryIterator(queryIterator);
@@ -175,6 +265,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
try {
setIsExecuting(true);
useTabs.getState().setIsTabExecuting(true);
useTabs.getState().setIsQueryErrorThrown(false);
const queryResults: QueryResults = await queryPagesUntilContentPresent(
firstItemIndex,
async (firstItemIndex: number) =>
@@ -183,10 +274,20 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
setQueryResults(queryResults);
setErrorMessage("");
setShowErrorMessageBar(false);
traceSuccess(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId,
});
} catch (error) {
const errorMessage = getErrorMessage(error);
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId,
errorMessage: errorMessage,
});
setErrorMessage(errorMessage);
handleError(errorMessage, "executeQueryCopilotTab");
useTabs.getState().setIsQueryErrorThrown(true);
setShowErrorMessageBar(true);
} finally {
setIsExecuting(false);
useTabs.getState().setIsTabExecuting(false);
@@ -202,6 +303,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
commandButtonLabel: executeQueryBtnLabel,
ariaLabel: executeQueryBtnLabel,
hasPopup: false,
disabled: query?.trim() === "",
};
const saveQueryBtn = {
@@ -212,248 +314,341 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
commandButtonLabel: "Save Query",
ariaLabel: "Save Query",
hasPopup: false,
disabled: query?.trim() === "",
};
const samplePromptsBtn = {
iconSrc: SamplePromptsIcon,
iconAlt: "Sample Prompts",
onCommandClick: () => setIsSamplePromptsOpen(true),
commandButtonLabel: "Sample Prompts",
ariaLabel: "Sample Prompts",
hasPopup: false,
};
// Sample Prompts temporary disabled due current design
// const samplePromptsBtn = {
// iconSrc: SamplePromptsIcon,
// iconAlt: "Sample Prompts",
// onCommandClick: () => setIsSamplePromptsOpen(true),
// commandButtonLabel: "Sample Prompts",
// ariaLabel: "Sample Prompts",
// hasPopup: false,
// };
return [executeQueryBtn, saveQueryBtn, samplePromptsBtn];
return [executeQueryBtn, saveQueryBtn];
};
const showTeachingBubble = (): void => {
const shouldShowTeachingBubble = !inputEdited.current && userPrompt.trim() === "";
if (shouldShowTeachingBubble) {
setTimeout(() => {
if (shouldShowTeachingBubble) {
toggleCopilotTeachingBubbleVisible();
inputEdited.current = true;
}
}, 30000);
}
};
const resetButtonState = () => {
setDislikeQuery(false);
setLikeQuery(false);
setShowCallout(false);
};
const startGenerateQueryProcess = () => {
updateHistories();
generateSQLQuery();
resetButtonState();
};
React.useEffect(() => {
useCommandBar.getState().setContextButtons(getCommandbarButtons());
}, [query, selectedQuery]);
return (
<Stack className="tab-pane" style={{ padding: 24, width: "100%", height: "100%" }}>
<Stack horizontal verticalAlign="center">
<Image src={CopilotIcon} />
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
</Stack>
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%" }}>
<TextField
id="naturalLanguageInput"
value={userPrompt}
onChange={(_, newValue) => setUserPrompt(newValue)}
style={{ lineHeight: 30 }}
styles={{ root: { width: "95%" } }}
disabled={isGeneratingQuery}
onClick={() => setShowSamplePrompts(true)}
/>
<IconButton
iconProps={{ iconName: "Send" }}
disabled={isGeneratingQuery}
style={{ marginLeft: 8 }}
onClick={() => {
updateHistories();
generateSQLQuery();
}}
/>
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
{showSamplePrompts && (
<Callout
styles={{ root: { minWidth: 400 } }}
style={{ padding: "8px 0" }}
target="#naturalLanguageInput"
isBeakVisible={false}
onDismiss={() => setShowSamplePrompts(false)}
directionalHint={DirectionalHint.bottomLeftEdge}
>
<Stack>
{histories?.length > 0 && (
<Stack>
<Text
style={{
width: "100%",
fontSize: 14,
fontWeight: 600,
color: "#0078D4",
marginLeft: 16,
padding: "4px 0",
}}
>
Recent
</Text>
{histories.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",
}}
>
Suggested Prompts
</Text>
<DefaultButton
onClick={() => {
setUserPrompt("Give me all customers whose names start with C");
setShowSamplePrompts(false);
}}
onRenderIcon={() => <Image src={HintIcon} />}
styles={promptStyles}
>
Give me all customers whose names start with C
</DefaultButton>
<DefaultButton
onClick={() => {
setUserPrompt("Show me all customers");
setShowSamplePrompts(false);
}}
onRenderIcon={() => <Image src={HintIcon} />}
styles={promptStyles}
>
Show me all customers
</DefaultButton>
<DefaultButton
onClick={() => {
setUserPrompt("Show me all customers who bought a bike in 2019");
setShowSamplePrompts(false);
}}
onRenderIcon={() => <Image src={HintIcon} />}
styles={promptStyles}
>
Show me all customers who bought a bike in 2019
</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>
</Text>
React.useEffect(() => {
showTeachingBubble();
useTabs.getState().setIsQueryErrorThrown(false);
}, []);
{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}
return (
<Stack className="tab-pane" style={{ padding: 24, width: "100%" }}>
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
<Stack horizontal verticalAlign="center">
<Image src={CopilotIcon} />
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
</Stack>
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%", position: "relative" }}>
<TextField
id="naturalLanguageInput"
value={userPrompt}
onChange={handleUserPromptChange}
onClick={() => {
inputEdited.current = true;
setShowSamplePrompts(true);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
startGenerateQueryProcess();
}
}}
style={{ lineHeight: 30 }}
styles={{ root: { width: "95%" } }}
disabled={isGeneratingQuery}
autoComplete="off"
/>
{copilotTeachingBubbleVisible && (
<TeachingBubble
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
target="#naturalLanguageInput"
hasCloseButton={true}
closeButtonAriaLabel="Close"
onDismiss={toggleCopilotTeachingBubbleVisible}
hasSmallHeadline={true}
headline="Write a prompt"
>
<Text>
Thank you. Need to give{" "}
<Link
onClick={() => {
setShowCallout(false);
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
}}
>
more feedback?
</Link>
</Text>
</Callout>
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
<Link
onClick={() => {
setShowSamplePrompts(true);
toggleCopilotTeachingBubbleVisible();
}}
style={{ color: "white", fontWeight: 600 }}
>
sample prompts
</Link>{" "}
or write your own query
</TeachingBubble>
)}
<IconButton
id="likeBtn"
style={{ marginLeft: 20 }}
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
onClick={() => {
setLikeQuery(true);
setShowCallout(true);
}}
iconProps={{ iconName: "Send" }}
disabled={isGeneratingQuery || !userPrompt.trim()}
style={{ marginLeft: 8 }}
onClick={() => startGenerateQueryProcess()}
/>
<IconButton
style={{ margin: "0 10px" }}
iconProps={{ iconName: likeQuery === false ? "DislikeSolid" : "Dislike" }}
onClick={() => {
setLikeQuery(false);
setShowCallout(false);
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt);
}}
/>
<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>
{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>
)}
{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 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)}
<Stack style={{ marginTop: 8, marginBottom: 24 }}>
<Text style={{ fontSize: 12 }}>
AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "}
<Link href="http://aka.ms/cdb-copilot-preview-terms" 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>
</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}
queryEditorContent={selectedQuery || query}
error={errorMessage}
queryResults={queryResults}
isExecuting={isExecuting}
executeQueryDocumentsPage={(firstItemIndex: number) => queryDocumentsPerPage(firstItemIndex, queryIterator)}
/>
</SplitterLayout>
</Stack>
{isSamplePromptsOpen ? <SamplePrompts sampleProps={sampleProps} /> : <></>}
{query !== "" && query.trim().length !== 0 ? (
<DeletePopup showDeletePopup={showDeletePopup} setShowDeletePopup={setShowDeletePopup} setQuery={setQuery} />
) : (
<></>
)}
<CopyPopup showCopyPopup={showCopyPopup} setShowCopyPopup={setshowCopyPopup} />
)}
<CopyPopup showCopyPopup={showCopyPopup} setShowCopyPopup={setshowCopyPopup} />
</div>
</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 { 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 {
likeQuery: boolean;
generatedQuery: string;
userPrompt: string;
description?: string;
contact?: string;
}
export const querySampleDocuments = (query: string, options: FeedOptions): QueryIterator<ItemDefinition & Resource> => {
options = getCommonQueryOptions(options);
return sampleDataClient()
.database(QueryCopilotSampleDatabaseId)
.container(QueryCopilotSampleContainerId)
.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 {
const { likeQuery, generatedQuery, userPrompt, description, contact } = params;
const payload = {
containerSchema: QueryCopilotSampleContainerSchema,
like: likeQuery ? "like" : "dislike",
generatedSql: generatedQuery,
userPrompt,
description: description || "",
contact: contact || "",
};
const response = await sampleDataClient()
.database(QueryCopilotSampleDatabaseId)
.container(QueryCopilotSampleContainerId)
.item(documentId.id(), getPartitionKeyValue(documentId))
.read();
const response = await fetch("https://copilotorchestrater.azurewebsites.net/feedback", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify(payload),
});
// eslint-disable-next-line no-console
console.log(response);
return response?.resource;
} 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 React, { Dispatch, SetStateAction } from "react";
import ComplexPrompts from "../../../../images/ComplexPrompts.svg";
import IntermediatePrompts from "../../../../images/IntermediatePrompts.svg";
import SimplePrompts from "../../../../images/SimplePrompts.svg";
import ComplexPrompts from "../../../../../images/ComplexPrompts.svg";
import IntermediatePrompts from "../../../../../images/IntermediatePrompts.svg";
import SimplePrompts from "../../../../../images/SimplePrompts.svg";
export interface SamplePromptsProps {
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,128 +5,149 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
className="tab-pane"
style={
Object {
"height": "100%",
"padding": 24,
"width": "100%",
}
}
>
<Stack
horizontal={true}
verticalAlign="center"
>
<Image
src=""
/>
<Text
style={
Object {
"fontSize": 16,
"fontWeight": 600,
"marginLeft": 8,
}
}
>
Copilot
</Text>
</Stack>
<Stack
horizontal={true}
<div
style={
Object {
"marginTop": 16,
"width": "100%",
}
}
verticalAlign="center"
>
<StyledTextFieldBase
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,
"height": "100%",
"overflowY": "auto",
}
}
>
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
<StyledLinkBase
href=""
target="_blank"
<Stack
horizontal={true}
verticalAlign="center"
>
Read preview terms
</StyledLinkBase>
</Text>
<Stack
className="tabPaneContentContainer"
>
<t
customClassName=""
onDragEnd={null}
onDragStart={null}
onSecondaryPaneSizeChange={null}
percentage={false}
primaryIndex={0}
primaryMinSize={100}
secondaryMinSize={200}
vertical={true}
<Image
src=""
/>
<Text
style={
Object {
"fontSize": 16,
"fontWeight": 600,
"marginLeft": 8,
}
}
>
Copilot
</Text>
</Stack>
<Stack
horizontal={true}
style={
Object {
"marginTop": 16,
"position": "relative",
"width": "100%",
}
}
verticalAlign="center"
>
<EditorReact
ariaLabel="Editing Query"
content=""
isReadOnly={false}
language="sql"
lineNumbers="on"
onContentChanged={[Function]}
onContentSelected={[Function]}
<StyledTextFieldBase
autoComplete="off"
disabled={false}
id="naturalLanguageInput"
onChange={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"lineHeight": 30,
}
}
styles={
Object {
"root": Object {
"width": "95%",
},
}
}
value=""
/>
<QueryResultSection
error=""
executeQueryDocumentsPage={[Function]}
isExecuting={false}
isMongoDB={false}
queryEditorContent=""
<CustomizedIconButton
disabled={true}
iconProps={
Object {
"iconName": "Send",
}
}
onClick={[Function]}
style={
Object {
"marginLeft": 8,
}
}
/>
</t>
</Stack>
<CopyPopup
setShowCopyPopup={[Function]}
showCopyPopup={false}
/>
</Stack>
<Stack
style={
Object {
"marginBottom": 24,
"marginTop": 8,
}
}
>
<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>
`;

View File

@@ -97,6 +97,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
() => this.setState({}),
(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 => {
if (userContext.features.enableCopilot && userContext.apiType === "SQL") {
if (
useDatabases.getState().sampleDataResourceTokenCollection &&
userContext.features.enableCopilot &&
userContext.apiType === "SQL"
) {
return (
<Stack style={{ width: "66%", cursor: "pointer", margin: "40px auto" }} tokens={{ childrenGap: 16 }}>
<Stack horizontal tokens={{ childrenGap: 16 }}>
@@ -137,7 +147,16 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
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!"
}
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
imgSrc={ConnectIcon}
@@ -246,8 +265,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
<form className="connectExplorerFormContainer">
<div className="splashScreenContainer">
<div className="splashScreen">
<div
<h1
className="title"
role="heading"
aria-label={
userContext.apiType === "Postgres"
? "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"}
<FeaturePanelLauncher />
</div>
</h1>
<div className="subtitle">
{userContext.apiType === "Postgres"
? "Get started with our sample datasets, documentation, and additional tools."
@@ -581,7 +601,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
>
{item.title}
</Link>
<Image src={LinkIcon} alt=" " />
<Image src={LinkIcon} alt={item.title} />
</Stack>
<Text>{item.description}</Text>
</Stack>
@@ -600,7 +620,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
<li key={`${item.title}${item.description}${index}`}>
<Stack style={{ marginBottom: 26 }}>
<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}>
{item.title}
</Link>
@@ -720,7 +740,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
>
{item.title}
</Link>
<Image src={LinkIcon} />
<Image src={LinkIcon} alt={item.title} />
</Stack>
<Text>{item.description}</Text>
</Stack>

View File

@@ -1,4 +1,5 @@
import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import * as ko from "knockout";
import Q from "q";
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
@@ -7,7 +8,12 @@ import NewDocumentIcon from "../../../images/NewDocument.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import UploadIcon from "../../../images/Upload_16x16.svg";
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 { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
@@ -71,6 +77,7 @@ export default class DocumentsTab extends TabsBase {
private _documentsIterator: QueryIterator<ItemDefinition & Resource>;
private _resourceTokenPartitionKey: string;
private _isQueryCopilotSampleContainer: boolean;
constructor(options: ViewModels.DocumentsTabOptions) {
super(options);
@@ -317,6 +324,9 @@ export default class DocumentsTab extends TabsBase {
this.selectedDocumentContent.subscribe((newContent: string) => this._onEditorContentChange(newContent));
this.showPartitionKey = this._shouldShowPartitionKey();
this._isQueryCopilotSampleContainer =
this.collection?.databaseId === QueryCopilotSampleDatabaseId &&
this.collection?.id() === QueryCopilotSampleContainerId;
}
private _shouldShowPartitionKey(): boolean {
@@ -678,7 +688,6 @@ export default class DocumentsTab extends TabsBase {
}
public createIterator(): QueryIterator<ItemDefinition & Resource> {
let filters = this.lastFilterContents();
const filter: string = this.filterContent().trim();
const query: string = this.buildQuery(filter);
let options: any = {};
@@ -688,12 +697,16 @@ export default class DocumentsTab extends TabsBase {
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> {
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);
}
@@ -810,7 +823,7 @@ export default class DocumentsTab extends TabsBase {
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.newDocumentButton.enabled(),
disabled: !this.newDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(),
id: "mongoNewDocumentBtn",
});
}
@@ -824,7 +837,8 @@ export default class DocumentsTab extends TabsBase {
commandButtonLabel: label,
ariaLabel: label,
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,
ariaLabel: label,
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,
ariaLabel: label,
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,
ariaLabel: label,
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,
ariaLabel: label,
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,
ariaLabel: label,
hasPopup: true,
disabled: useSelectedNode.getState().isDatabaseNodeOrNoneSelected(),
disabled:
useSelectedNode.getState().isDatabaseNodeOrNoneSelected() ||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
};
}
}

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