Compare commits
58 Commits
upload_doc
...
users/lang
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa865f99c8 | ||
|
|
f8ff0626d9 | ||
|
|
c8e7e69aa5 | ||
|
|
b992742e20 | ||
|
|
0207f3cc04 | ||
|
|
6a8e87f45f | ||
|
|
f7370fd341 | ||
|
|
425e375d50 | ||
|
|
55846b98bd | ||
|
|
3d02f07262 | ||
|
|
449118a1bf | ||
|
|
8405dbe8da | ||
|
|
19100ec437 | ||
|
|
ebd40cb9b0 | ||
|
|
986dbe7d54 | ||
|
|
143f7d8f2c | ||
|
|
b646f9f4cb | ||
|
|
0f52db73e7 | ||
|
|
9c1b9e6ff6 | ||
|
|
19041ffedd | ||
|
|
ceb66ed5b8 | ||
|
|
96b88ac344 | ||
|
|
bcedf0a29f | ||
|
|
67133017ce | ||
|
|
b5d7ab0a30 | ||
|
|
6dba4937ce | ||
|
|
1a3ca94efb | ||
|
|
9f7783f3f9 | ||
|
|
daa26d289b | ||
|
|
879cb08949 | ||
|
|
92f43c28a7 | ||
|
|
5f0c7bcea2 | ||
|
|
fa55d528ad | ||
|
|
c873fed7aa | ||
|
|
7ec5290293 | ||
|
|
4005128211 | ||
|
|
38d13cc74e | ||
|
|
4ab93a5a32 | ||
|
|
44e25c0769 | ||
|
|
8ea8f0230f | ||
|
|
655b998b84 | ||
|
|
9e2f6a9c89 | ||
|
|
42e11d5160 | ||
|
|
10037d844e | ||
|
|
13434715b3 | ||
|
|
676434cac5 | ||
|
|
3d5580865a | ||
|
|
7375cc717c | ||
|
|
de3e56bb99 | ||
|
|
53cd78452b | ||
|
|
fb6eb635c1 | ||
|
|
fb6c0caca6 | ||
|
|
e9f3c64239 | ||
|
|
bb6ee5deec | ||
|
|
5796b28045 | ||
|
|
9635d14599 | ||
|
|
c58d5765c2 | ||
|
|
708f4316b4 |
@@ -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
|
||||
3
images/CopilotCopy.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="19" height="17" viewBox="0 0 19 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 0C5.89543 0 5 0.895431 5 2V12C5 13.1046 5.89543 14 7 14H13C14.1046 14 15 13.1046 15 12V2C15 0.89543 14.1046 0 13 0H7ZM6 2C6 1.44772 6.44772 1 7 1H13C13.5523 1 14 1.44772 14 2V12C14 12.5523 13.5523 13 13 13H7C6.44772 13 6 12.5523 6 12V2ZM3 4.00001C3 3.25973 3.4022 2.61339 4 2.26758V12.5C4 13.8807 5.11929 15 6.5 15H12.7324C12.3866 15.5978 11.7403 16 11 16H6.5C4.567 16 3 14.433 3 12.5V4.00001Z" fill="#242424"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 527 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="10" height="14" viewBox="0 0 10 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 2.5C10 3.88071 7.76142 5 5 5C2.23858 5 0 3.88071 0 2.5C0 1.11929 2.23858 0 5 0C7.76142 0 10 1.11929 10 2.5ZM0 11.5V4.487C1.057 5.413 2.864 6 5 6C7.136 6 8.943 5.413 10 4.487V11.5C10 12.925 7.851 14 5 14C2.149 14 0 12.925 0 11.5Z" fill="#0078D4"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 363 B After Width: | Height: | Size: 363 B |
3
images/CopilotExplain.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.25028 7.30723C9.08872 7.49072 9.00001 7.74463 9.00001 8C9.00001 8.27614 8.77615 8.5 8.50001 8.5C8.22387 8.5 8.00001 8.27614 8.00001 8C8.00001 7.52689 8.1613 7.0308 8.49974 6.64641C8.84684 6.25219 9.3597 6 10 6C10.6403 6 11.1532 6.25219 11.5003 6.64641C11.8387 7.0308 12 7.52689 12 8C12 8.48947 11.8839 8.86964 11.6976 9.18921C11.5347 9.46855 11.3225 9.68963 11.1528 9.86652L11.1115 9.90956C10.9247 10.1051 10.7821 10.2639 10.6773 10.4641C10.5773 10.6551 10.5 10.9085 10.5 11.2929C10.5 11.5691 10.2762 11.7929 10 11.7929C9.72387 11.7929 9.50001 11.5691 9.50001 11.2929C9.50001 10.7611 9.61018 10.3464 9.79143 10.0002C9.96788 9.66319 10.2003 9.41576 10.3885 9.21878L10.4106 9.19559C10.5985 8.99908 10.7328 8.85858 10.8337 8.68547C10.9286 8.52273 11 8.31707 11 8C11 7.74463 10.9113 7.49072 10.7497 7.30723C10.5968 7.13358 10.3597 7 10 7C9.64033 7 9.40318 7.13358 9.25028 7.30723ZM9.99991 14.2122C10.3863 14.2122 10.6995 13.899 10.6995 13.5126C10.6995 13.1262 10.3863 12.813 9.99991 12.813C9.61353 12.813 9.3003 13.1262 9.3003 13.5126C9.3003 13.899 9.61353 14.2122 9.99991 14.2122ZM2.00001 10C2.00001 5.58172 5.58173 2 10 2C14.4183 2 18 5.58172 18 10C18 14.4183 14.4183 18 10 18C8.65078 18 7.37829 17.6656 6.26225 17.0748L2.62128 17.9851C2.45089 18.0277 2.27065 17.9777 2.14646 17.8536C2.02227 17.7294 1.97234 17.5491 2.01494 17.3787L2.92518 13.7378C2.33442 12.6217 2.00001 11.3492 2.00001 10ZM10 3C6.13402 3 3.00001 6.13401 3.00001 10C3.00001 11.245 3.32462 12.4128 3.89345 13.4247C3.95602 13.536 3.97363 13.6671 3.94266 13.791L3.18719 16.8128L6.20904 16.0574C6.33294 16.0264 6.46399 16.044 6.57531 16.1066C7.58726 16.6754 8.75497 17 10 17C13.866 17 17 13.866 17 10C17 6.13401 13.866 3 10 3Z" fill="#424242"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -1,3 +1,3 @@
|
||||
<svg width="13" height="17" viewBox="0 0 13 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.21207 0C3.73772 0 3.32085 0.314446 3.19054 0.770537L0.940937 8.64415C0.747029 9.32283 1.25663 9.99841 1.96246 9.99841H3.22974L2.06002 14.6773C1.79611 15.7329 3.10089 16.4551 3.85526 15.6726L12.5318 6.81506L12.5354 6.81137C13.1762 6.1436 12.7152 5 11.7688 5H9.20509L10.4665 1.40582L10.469 1.39836C10.6983 0.710426 10.1863 0 9.46114 0H4.21207Z" fill="#0078D4"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 475 B After Width: | Height: | Size: 475 B |
3
images/CopilotInsert.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 4C4.89543 4 4 4.89543 4 6V7C4 8.10457 4.89543 9 6 9H14C15.1046 9 16 8.10457 16 7V6C16 4.89543 15.1046 4 14 4H6ZM5 6C5 5.44772 5.44772 5 6 5H14C14.5523 5 15 5.44772 15 6V7C15 7.55228 14.5523 8 14 8H6C5.44772 8 5 7.55228 5 7V6ZM6 11C4.89543 11 4 11.8954 4 13V14C4 15.1046 4.89543 16 6 16H14C15.1046 16 16 15.1046 16 14V13C16 11.8954 15.1046 11 14 11H6ZM5 13C5 12.4477 5.44772 12 6 12H14C14.5523 12 15 12.4477 15 13V14C15 14.5523 14.5523 15 14 15H6C5.44772 15 5 14.5523 5 14V13Z" fill="#242424"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 609 B |
3
images/CopilotLikeHover.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.4829 0.703737C9.68406 -0.133389 8.39129 0.316883 8.05198 1.29418C7.77205 2.10043 7.4084 3.06594 7.05406 3.77684C5.99442 5.90276 5.37583 7.11234 3.66974 8.62586C3.44337 8.82668 3.15163 8.9885 2.82905 9.11601C1.69991 9.56233 0.638089 10.7321 0.915812 12.1207L1.26885 13.8859C1.45455 14.8144 2.14894 15.5583 3.06251 15.8075L8.66224 17.3347C11.2078 18.0289 13.8017 16.3942 14.2737 13.7983L14.9576 10.0365C15.2924 8.19503 13.8777 6.49989 12.006 6.49989H11.1225L11.1328 6.44766C11.2129 6.03948 11.3093 5.47735 11.3738 4.86473C11.438 4.25446 11.4721 3.58034 11.4218 2.9522C11.3725 2.33584 11.2379 1.70305 10.9176 1.22254C10.8081 1.05832 10.6455 0.874161 10.4829 0.703737Z" fill="#605E5C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 799 B |
3
images/CopilotLikePressed.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.4829 0.703737C9.68406 -0.133389 8.39129 0.316883 8.05198 1.29418C7.77205 2.10043 7.4084 3.06594 7.05406 3.77684C5.99442 5.90276 5.37583 7.11234 3.66974 8.62586C3.44337 8.82668 3.15163 8.9885 2.82905 9.11601C1.69991 9.56233 0.638089 10.7321 0.915812 12.1207L1.26885 13.8859C1.45455 14.8144 2.14894 15.5583 3.06251 15.8075L8.66224 17.3347C11.2078 18.0289 13.8017 16.3942 14.2737 13.7983L14.9576 10.0365C15.2924 8.19503 13.8777 6.49989 12.006 6.49989H11.1225L11.1328 6.44766C11.2129 6.03948 11.3093 5.47735 11.3738 4.86473C11.438 4.25446 11.4721 3.58034 11.4218 2.9522C11.3725 2.33584 11.2379 1.70305 10.9176 1.22254C10.8081 1.05832 10.6455 0.874161 10.4829 0.703737Z" fill="#0078D4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 799 B |
3
images/CopilotLikeRest.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="15" height="18" viewBox="0 0 15 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.55198 1.29418C7.89129 0.316883 9.18406 -0.133389 9.98289 0.703737C10.1455 0.874162 10.3081 1.05832 10.4176 1.22254C10.7379 1.70305 10.8725 2.33584 10.9218 2.9522C10.9721 3.58034 10.938 4.25446 10.8738 4.86473C10.8093 5.47735 10.7129 6.03948 10.6328 6.44766C10.6294 6.46535 10.6259 6.48277 10.6225 6.49989H11.506C13.3777 6.49989 14.7924 8.19503 14.4576 10.0365L13.7737 13.7983C13.3017 16.3942 10.7078 18.0289 8.16224 17.3347L2.56251 15.8075C1.64894 15.5583 0.954555 14.8144 0.768846 13.8859L0.415812 12.1207C0.138089 10.7321 1.19991 9.56233 2.32905 9.11601C2.65163 8.9885 2.94337 8.82668 3.16974 8.62586C4.87583 7.11234 5.49442 5.90276 6.55406 3.77684C6.9084 3.06594 7.27205 2.10043 7.55198 1.29418ZM9.51651 6.87851L9.51689 6.87696L9.51869 6.86962L9.5262 6.83852C9.53284 6.81068 9.54264 6.76892 9.55487 6.71482C9.57935 6.60658 9.61349 6.44919 9.65152 6.25525C9.72773 5.86655 9.81878 5.33493 9.8793 4.76005C9.94006 4.18282 9.96852 3.57569 9.92502 3.03195C9.88058 2.47644 9.76518 2.04673 9.58552 1.77724C9.52643 1.68859 9.41385 1.55593 9.25942 1.3941C9.06051 1.18565 8.63137 1.23417 8.49666 1.62217C8.21411 2.43598 7.83339 3.45183 7.44904 4.22294C6.38216 6.36338 5.69326 7.72396 3.83336 9.37392C3.49304 9.67583 3.08878 9.89099 2.69665 10.046C1.81631 10.394 1.25035 11.1944 1.39639 11.9246L1.74943 13.6898C1.86085 14.2469 2.27748 14.6932 2.82562 14.8427L8.42536 16.3699C10.4052 16.9099 12.4227 15.6384 12.7898 13.6194L13.4738 9.85766C13.697 8.62998 12.7538 7.49989 11.506 7.49989H10.0015C9.84758 7.49989 9.7022 7.42895 9.60745 7.3076C9.51272 7.18627 9.47921 7.02785 9.51651 6.87851C9.51651 6.87847 9.5165 6.87855 9.51651 6.87851Z" fill="#424242"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
3
images/CopilotOptimize.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 2.5C5 2.22386 4.77614 2 4.5 2C4.22386 2 4 2.22386 4 2.5V4H2.5C2.22386 4 2 4.22386 2 4.5C2 4.77614 2.22386 5 2.5 5H4.5C4.77614 5 5 4.77614 5 4.5V2.5ZM16 2.5C16 2.22386 15.7761 2 15.5 2C15.2239 2 15 2.22386 15 2.5V4.5C15 4.77614 15.2239 5 15.5 5H17.5C17.7761 5 18 4.77614 18 4.5C18 4.22386 17.7761 4 17.5 4H16V2.5ZM7 5C6.44771 5 6 5.44772 6 6V14C6 14.5523 6.44772 15 7 15H13C13.5523 15 14 14.5523 14 14V6C14 5.44772 13.5523 5 13 5H7ZM7 6H13V14H7V6ZM4.5 18C4.77614 18 5 17.7761 5 17.5V15.5C5 15.2239 4.77614 15 4.5 15H2.5C2.22386 15 2 15.2239 2 15.5C2 15.7761 2.22386 16 2.5 16H4V17.5C4 17.7761 4.22386 18 4.5 18ZM15.5 18C15.7761 18 16 17.7761 16 17.5V16H17.5C17.7761 16 18 15.7761 18 15.5C18 15.2239 17.7761 15 17.5 15H15.5C15.2239 15 15 15.2239 15 15.5V17.5C15 17.7761 15.2239 18 15.5 18ZM8.5 8C8.22386 8 8 8.22386 8 8.5C8 8.77614 8.22386 9 8.5 9H11.5C11.7761 9 12 8.77614 12 8.5C12 8.22386 11.7761 8 11.5 8H8.5ZM8.5 10C8.22386 10 8 10.2239 8 10.5C8 10.7761 8.22386 11 8.5 11H10.5C10.7761 11 11 10.7761 11 10.5C11 10.2239 10.7761 10 10.5 10H8.5Z" fill="#424242"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
images/CopilotRegenerate.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.14645 2.64645C9.34171 2.45118 9.65829 2.45118 9.85355 2.64645L11.3536 4.14645C11.5488 4.34171 11.5488 4.65829 11.3536 4.85355L9.85355 6.35355C9.65829 6.54882 9.34171 6.54882 9.14645 6.35355C8.95118 6.15829 8.95118 5.84171 9.14645 5.64645L9.7885 5.00439C7.12517 5.11522 5 7.30943 5 10C5 11.568 5.72118 12.9672 6.85185 13.8847C7.06627 14.0587 7.09904 14.3736 6.92503 14.588C6.75103 14.8024 6.43615 14.8352 6.22172 14.6612C4.86712 13.5619 4 11.882 4 10C4 6.75447 6.57689 4.1108 9.79629 4.00339L9.14645 3.35355C8.95118 3.15829 8.95118 2.84171 9.14645 2.64645ZM13.075 5.41199C13.249 5.19756 13.5639 5.1648 13.7783 5.3388C15.1329 6.43806 16 8.11795 16 10C16 13.2455 13.4231 15.8892 10.2037 15.9966L10.8536 16.6464C11.0488 16.8417 11.0488 17.1583 10.8536 17.3536C10.6583 17.5488 10.3417 17.5488 10.1464 17.3536L8.64645 15.8536C8.55268 15.7598 8.5 15.6326 8.5 15.5C8.5 15.3674 8.55268 15.2402 8.64645 15.1464L10.1464 13.6464C10.3417 13.4512 10.6583 13.4512 10.8536 13.6464C11.0488 13.8417 11.0488 14.1583 10.8536 14.3536L10.2115 14.9956C12.8748 14.8848 15 12.6906 15 10C15 8.43201 14.2788 7.03283 13.1482 6.1153C12.9337 5.94129 12.901 5.62641 13.075 5.41199Z" fill="#242424"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
34
images/CopilotSidebarLogo.svg
Normal 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 |
29
images/CopilotSidebarWelcomeIllustration.svg
Normal 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 |
3
images/CopilotSimplify.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.5 4C4.67157 4 4 4.67157 4 5.5V6.5C4 6.77614 3.77614 7 3.5 7C3.22386 7 3 6.77614 3 6.5V5.5C3 4.11929 4.11929 3 5.5 3H6.5C6.77614 3 7 3.22386 7 3.5C7 3.77614 6.77614 4 6.5 4H5.5ZM16 5.5C16 4.67157 15.3284 4 14.5 4H13.5C13.2239 4 13 3.77614 13 3.5C13 3.22386 13.2239 3 13.5 3H14.5C15.8807 3 17 4.11929 17 5.5V6.5C17 6.77614 16.7761 7 16.5 7C16.2239 7 16 6.77614 16 6.5V5.5ZM16 14.5C16 15.3284 15.3284 16 14.5 16H13.5C13.2239 16 13 16.2239 13 16.5C13 16.7761 13.2239 17 13.5 17H14.5C15.8807 17 17 15.8807 17 14.5V13.5C17 13.2239 16.7761 13 16.5 13C16.2239 13 16 13.2239 16 13.5V14.5ZM4 14.5C4 15.3284 4.67157 16 5.5 16H6.75C7.02614 16 7.25 16.2239 7.25 16.5C7.25 16.7761 7.02614 17 6.75 17H5.5C4.11929 17 3 15.8807 3 14.5V13.25C3 12.9739 3.22386 12.75 3.5 12.75C3.77614 12.75 4 12.9739 4 13.25V14.5ZM8.5 7C7.67157 7 7 7.67157 7 8.5V11.5C7 12.3284 7.67157 13 8.5 13H11.5C12.3284 13 13 12.3284 13 11.5V8.5C13 7.67157 12.3284 7 11.5 7H8.5ZM8 8.5C8 8.22386 8.22386 8 8.5 8H11.5C11.7761 8 12 8.22386 12 8.5V11.5C12 11.7761 11.7761 12 11.5 12H8.5C8.22386 12 8 11.7761 8 11.5V8.5Z" fill="#424242"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
images/CopilotTabIcon.svg
Normal 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/StopGenerating.svg
Normal 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 |
@@ -68,7 +68,8 @@ module.exports = {
|
||||
|
||||
// A map from regular expressions to module names that allow to stub out resources with a single module
|
||||
moduleNameMapper: {
|
||||
"^.*[.](svg|png|gif|less|css)$": "<rootDir>/mockModule",
|
||||
"^.*[.](png|gif|less|css)$": "<rootDir>/mockModule",
|
||||
"(.*)$[.](svg)": "<rootDir>/mockModule/$1",
|
||||
"@nteract/stateful-components/(.*)$": "<rootDir>/mockModule",
|
||||
"@fluentui/react/lib/(.*)$": "@fluentui/react/lib-commonjs/$1", // https://github.com/microsoft/fluentui/wiki/Version-8-release-notes
|
||||
"monaco-editor/(.*)$": "<rootDir>/__mocks__/monaco-editor",
|
||||
@@ -165,6 +166,7 @@ module.exports = {
|
||||
transform: {
|
||||
"^.+\\.html?$": "html-loader-jest",
|
||||
"^.+\\.[t|j]sx?$": "babel-jest",
|
||||
"^.+\\.svg$": "<rootDir>/svgTransform.js",
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -28,4 +28,4 @@
|
||||
.clickDisabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33016
package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||
"@fluentui/react": "8.14.3",
|
||||
"@fluentui/react-components": "9.30.1",
|
||||
"@jupyterlab/services": "6.0.2",
|
||||
"@jupyterlab/terminal": "3.0.3",
|
||||
"@microsoft/applicationinsights-web": "2.6.1",
|
||||
@@ -160,6 +161,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 +234,4 @@
|
||||
"prettier": {
|
||||
"printWidth": 120
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import React from "react";
|
||||
import AddCollectionIcon from "../../images/AddCollection.svg";
|
||||
@@ -144,11 +147,24 @@ export const createCollectionContextMenuButton = (
|
||||
export const createSampleCollectionContextMenuButton = (): TreeNodeMenuItem[] => {
|
||||
const items: TreeNodeMenuItem[] = [];
|
||||
if (userContext.apiType === "SQL") {
|
||||
items.push({
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
onClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot),
|
||||
label: "New SQL Query",
|
||||
});
|
||||
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;
|
||||
|
||||
@@ -25,6 +25,7 @@ export class AccordionComponent extends React.Component<AccordionComponentProps>
|
||||
export interface AccordionItemComponentProps {
|
||||
title: string;
|
||||
isExpanded?: boolean;
|
||||
containerStyles?: React.CSSProperties;
|
||||
styles?: React.CSSProperties;
|
||||
}
|
||||
|
||||
@@ -54,9 +55,9 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { styles } = this.props;
|
||||
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}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -4,7 +4,6 @@ exports[`CollapsibleSectionComponent renders 1`] = `
|
||||
<Fragment>
|
||||
<Stack
|
||||
aria-expanded={true}
|
||||
aria-name="Advanced"
|
||||
className="collapsibleSection"
|
||||
horizontal={true}
|
||||
onClick={[Function]}
|
||||
|
||||
@@ -13,8 +13,14 @@ export interface EditorReactProps {
|
||||
ariaLabel: string; // Sets what will be read to the user to define the control
|
||||
onContentSelected?: (selectedContent: string) => void; // Called when text is selected
|
||||
onContentChanged?: (newContent: string) => void; // Called when text is changed
|
||||
lineNumbers?: monaco.editor.IEditorOptions["lineNumbers"];
|
||||
theme?: string; // Monaco editor theme
|
||||
wordWrap?: monaco.editor.IEditorOptions["wordWrap"];
|
||||
lineNumbers?: monaco.editor.IEditorOptions["lineNumbers"];
|
||||
lineNumbersMinChars?: monaco.editor.IEditorOptions["lineNumbersMinChars"];
|
||||
lineDecorationsWidth?: monaco.editor.IEditorOptions["lineDecorationsWidth"];
|
||||
minimap?: monaco.editor.IEditorOptions["minimap"];
|
||||
scrollBeyondLastLine?: monaco.editor.IEditorOptions["scrollBeyondLastLine"];
|
||||
monacoContainerStyles?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> {
|
||||
@@ -31,6 +37,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) {
|
||||
@@ -47,7 +60,11 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
return (
|
||||
<React.Fragment>
|
||||
{!this.state.showEditor && <Spinner size={SpinnerSize.large} className="spinner" />}
|
||||
<div className="jsonEditor" ref={(elt: HTMLElement) => this.setRef(elt)} />
|
||||
<div
|
||||
className="jsonEditor"
|
||||
style={this.props.monacoContainerStyles}
|
||||
ref={(elt: HTMLElement) => this.setRef(elt)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -77,14 +94,19 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
*/
|
||||
private async createEditor(createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) {
|
||||
const options: monaco.editor.IEditorConstructionOptions = {
|
||||
value: this.props.content,
|
||||
language: this.props.language,
|
||||
value: this.props.content,
|
||||
readOnly: this.props.isReadOnly,
|
||||
lineNumbers: this.props.lineNumbers || "off",
|
||||
fontSize: 12,
|
||||
ariaLabel: this.props.ariaLabel,
|
||||
theme: this.props.theme,
|
||||
fontSize: 12,
|
||||
automaticLayout: true,
|
||||
theme: this.props.theme,
|
||||
wordWrap: this.props.wordWrap || "off",
|
||||
lineNumbers: this.props.lineNumbers || "off",
|
||||
lineNumbersMinChars: this.props.lineNumbersMinChars,
|
||||
lineDecorationsWidth: this.props.lineDecorationsWidth,
|
||||
minimap: this.props.minimap,
|
||||
scrollBeyondLastLine: this.props.scrollBeyondLastLine,
|
||||
};
|
||||
|
||||
this.rootNode.innerHTML = "";
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { TreeComponent, TreeNode, TreeNodeComponent } from "./TreeComponent";
|
||||
import React from "react";
|
||||
import { TreeComponent, TreeNode, TreeNodeComponent_old } from "./TreeComponent";
|
||||
|
||||
const buildChildren = (): TreeNode[] => {
|
||||
const grandChild11: TreeNode = {
|
||||
@@ -98,7 +98,7 @@ describe("TreeNodeComponent", () => {
|
||||
generation: 12,
|
||||
paddingLeft: 23,
|
||||
};
|
||||
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||
const wrapper = shallow(<TreeNodeComponent_old {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -113,7 +113,7 @@ describe("TreeNodeComponent", () => {
|
||||
generation: 2,
|
||||
paddingLeft: 9,
|
||||
};
|
||||
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||
const wrapper = shallow(<TreeNodeComponent_old {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -128,7 +128,7 @@ describe("TreeNodeComponent", () => {
|
||||
generation: 2,
|
||||
paddingLeft: 9,
|
||||
};
|
||||
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||
const wrapper = shallow(<TreeNodeComponent_old {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -156,7 +156,7 @@ describe("TreeNodeComponent", () => {
|
||||
generation: 12,
|
||||
paddingLeft: 23,
|
||||
};
|
||||
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||
const wrapper = shallow(<TreeNodeComponent_old {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -172,7 +172,7 @@ describe("TreeNodeComponent", () => {
|
||||
generation: 2,
|
||||
paddingLeft: 9,
|
||||
};
|
||||
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||
const wrapper = shallow(<TreeNodeComponent_old {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
IContextualMenuItemProps,
|
||||
IContextualMenuProps,
|
||||
} from "@fluentui/react";
|
||||
import { Button, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, Spinner, Tree, TreeItem, TreeItemLayout } from "@fluentui/react-components";
|
||||
import { MoreHorizontal20Regular } from "@fluentui/react-icons";
|
||||
import * as React from "react";
|
||||
import AnimateHeight from "react-animate-height";
|
||||
import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
|
||||
@@ -20,7 +22,6 @@ import TriangleRightIcon from "../../../../images/Triangle-right.svg";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
export interface TreeNodeMenuItem {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
@@ -58,13 +59,90 @@ export interface TreeComponentProps {
|
||||
export class TreeComponent extends React.Component<TreeComponentProps> {
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div style={this.props.style} className={`treeComponent ${this.props.className}`} role="tree">
|
||||
<TreeNodeComponent paddingLeft={0} node={this.props.rootNode} generation={0} />
|
||||
</div>
|
||||
|
||||
<Tree>
|
||||
<TreeNodeComponent node={this.props.rootNode} />
|
||||
</Tree>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const TreeNodeComponent: React.FC<{ node: TreeNode, className?: string }> = ({ node }): JSX.Element => {
|
||||
const { children } = node;
|
||||
const defaultOpenItems = node.isExpanded ? children?.map((child: TreeNode) => child.label) : undefined;
|
||||
|
||||
const getSortedChildren = (treeNode: TreeNode): TreeNode[] => {
|
||||
if (!treeNode || !treeNode.children) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const compareFct = (a: TreeNode, b: TreeNode) => a.label.localeCompare(b.label);
|
||||
|
||||
let unsortedChildren;
|
||||
if (treeNode.isLeavesParentsSeparate) {
|
||||
// Separate parents and leave
|
||||
const parents: TreeNode[] = treeNode.children.filter((node) => node.children);
|
||||
const leaves: TreeNode[] = treeNode.children.filter((node) => !node.children);
|
||||
|
||||
if (treeNode.isAlphaSorted) {
|
||||
parents.sort(compareFct);
|
||||
leaves.sort(compareFct);
|
||||
}
|
||||
|
||||
unsortedChildren = parents.concat(leaves);
|
||||
} else {
|
||||
unsortedChildren = treeNode.isAlphaSorted ? treeNode.children.sort(compareFct) : treeNode.children;
|
||||
}
|
||||
|
||||
return unsortedChildren;
|
||||
};
|
||||
|
||||
return (
|
||||
<TreeItem key={node.label} value={node.label} itemType={node.children?.length > 0 || node.isLoading ? "branch" : "leaf"}>
|
||||
<TreeItemLayout
|
||||
className={node.className}
|
||||
onClick={() => node.onClick && node.onClick(false)}
|
||||
actions={node.contextMenu &&
|
||||
<Menu>
|
||||
<MenuTrigger disableButtonEnhancement>
|
||||
<Button aria-label="More options" appearance="subtle" icon={<MoreHorizontal20Regular />} />
|
||||
</MenuTrigger>
|
||||
<MenuPopover>
|
||||
<MenuList>{node.contextMenu.map(menuItem => (
|
||||
<MenuItem disabled={menuItem.isDisabled} key={menuItem.label} onClick={menuItem.onClick}>{menuItem.label}</MenuItem>
|
||||
))}</MenuList>
|
||||
</MenuPopover>
|
||||
</Menu>
|
||||
}
|
||||
expandIcon={node.isLoading ? <Spinner size="tiny" /> : undefined}
|
||||
>{node.label}</TreeItemLayout>
|
||||
{!node.isLoading && node.children?.length > 0 && <Tree defaultOpenItems={defaultOpenItems}>
|
||||
{getSortedChildren(node).map((childNode: TreeNode) => (
|
||||
<TreeNodeComponent key={childNode.id} node={childNode} />
|
||||
))}
|
||||
</Tree>}
|
||||
</TreeItem>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ------------------------------------------------------------
|
||||
/* Tree node is a react component */
|
||||
interface TreeNodeComponentProps {
|
||||
node: TreeNode;
|
||||
@@ -76,7 +154,7 @@ interface TreeNodeComponentState {
|
||||
isExpanded: boolean;
|
||||
isMenuShowing: boolean;
|
||||
}
|
||||
export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, TreeNodeComponentState> {
|
||||
export class TreeNodeComponent_old extends React.Component<TreeNodeComponentProps, TreeNodeComponentState> {
|
||||
private static readonly paddingPerGenerationPx = 16;
|
||||
private static readonly iconOffset = 22;
|
||||
private static readonly transitionDurationMS = 200;
|
||||
@@ -97,9 +175,9 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
// Only call when expand has actually changed
|
||||
if (this.state.isExpanded !== prevState.isExpanded) {
|
||||
if (this.state.isExpanded) {
|
||||
this.props.node.onExpanded && setTimeout(this.props.node.onExpanded, TreeNodeComponent.callbackDelayMS);
|
||||
this.props.node.onExpanded && setTimeout(this.props.node.onExpanded, TreeNodeComponent_old.callbackDelayMS);
|
||||
} else {
|
||||
this.props.node.onCollapsed && setTimeout(this.props.node.onCollapsed, TreeNodeComponent.callbackDelayMS);
|
||||
this.props.node.onCollapsed && setTimeout(this.props.node.onCollapsed, TreeNodeComponent_old.callbackDelayMS);
|
||||
}
|
||||
}
|
||||
if (this.props.node.isExpanded !== this.isExpanded) {
|
||||
@@ -145,13 +223,13 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
}
|
||||
|
||||
private renderNode(node: TreeNode, generation: number): JSX.Element {
|
||||
let paddingLeft = generation * TreeNodeComponent.paddingPerGenerationPx;
|
||||
let paddingLeft = generation * TreeNodeComponent_old.paddingPerGenerationPx;
|
||||
let additionalOffsetPx = 15;
|
||||
|
||||
if (node.children) {
|
||||
const childrenWithSubChildren = node.children.filter((child: TreeNode) => !!child.children);
|
||||
if (childrenWithSubChildren.length > 0) {
|
||||
additionalOffsetPx = TreeNodeComponent.iconOffset;
|
||||
additionalOffsetPx = TreeNodeComponent_old.iconOffset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,10 +237,10 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
const showSelected =
|
||||
this.props.node.isSelected &&
|
||||
this.props.node.isSelected() &&
|
||||
!TreeNodeComponent.isAnyDescendantSelected(this.props.node);
|
||||
!TreeNodeComponent_old.isAnyDescendantSelected(this.props.node);
|
||||
|
||||
const headerStyle: React.CSSProperties = { paddingLeft: this.props.paddingLeft };
|
||||
if (TreeNodeComponent.isNodeHeaderBlank(node)) {
|
||||
if (TreeNodeComponent_old.isNodeHeaderBlank(node)) {
|
||||
headerStyle.height = 0;
|
||||
headerStyle.padding = 0;
|
||||
}
|
||||
@@ -194,10 +272,10 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
<img className="loadingIcon" src={LoadingIndicator_3Squares} hidden={!this.props.node.isLoading} />
|
||||
</div>
|
||||
{node.children && (
|
||||
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
|
||||
<AnimateHeight duration={TreeNodeComponent_old.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
|
||||
<div className="nodeChildren" data-test={node.label} role="group">
|
||||
{TreeNodeComponent.getSortedChildren(node).map((childNode: TreeNode) => (
|
||||
<TreeNodeComponent
|
||||
{TreeNodeComponent_old.getSortedChildren(node).map((childNode: TreeNode) => (
|
||||
<TreeNodeComponent_old
|
||||
key={`${childNode.label}-${generation + 1}-${childNode.timestamp}`}
|
||||
node={childNode}
|
||||
generation={generation + 1}
|
||||
@@ -220,7 +298,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
node.children &&
|
||||
node.children.reduce(
|
||||
(previous: boolean, child: TreeNode) =>
|
||||
previous || (child.isSelected && child.isSelected()) || TreeNodeComponent.isAnyDescendantSelected(child),
|
||||
previous || (child.isSelected && child.isSelected()) || TreeNodeComponent_old.isAnyDescendantSelected(child),
|
||||
false
|
||||
)
|
||||
);
|
||||
@@ -231,7 +309,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
}
|
||||
|
||||
private onRightClick = (): void => {
|
||||
this.contextMenuRef.current.firstChild.dispatchEvent(TreeNodeComponent.createClickEvent());
|
||||
this.contextMenuRef.current.firstChild.dispatchEvent(TreeNodeComponent_old.createClickEvent());
|
||||
};
|
||||
|
||||
private renderContextMenuButton(node: TreeNode): JSX.Element {
|
||||
@@ -264,7 +342,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
<div
|
||||
data-test={`treeComponentMenuItemContainer`}
|
||||
className="treeComponentMenuItemContainer"
|
||||
onContextMenu={(e) => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())}
|
||||
onContextMenu={(e) => e.target.dispatchEvent(TreeNodeComponent_old.createClickEvent())}
|
||||
>
|
||||
{props.item.onRenderIcon()}
|
||||
<span
|
||||
@@ -318,7 +396,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
if (node.children) {
|
||||
const isExpanded = !this.state.isExpanded;
|
||||
// Prevent collapsing if node header is blank
|
||||
if (!(TreeNodeComponent.isNodeHeaderBlank(node) && !isExpanded)) {
|
||||
if (!(TreeNodeComponent_old.isNodeHeaderBlank(node) && !isExpanded)) {
|
||||
this.setState({ isExpanded });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
|
||||
className="expandCollapseIcon"
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
src=""
|
||||
src={Object {}}
|
||||
tabIndex={0}
|
||||
/>
|
||||
<span
|
||||
@@ -158,7 +158,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||
className="expandCollapseIcon"
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
src=""
|
||||
src={Object {}}
|
||||
tabIndex={0}
|
||||
/>
|
||||
<span
|
||||
@@ -309,7 +309,7 @@ exports[`TreeNodeComponent renders loading icon 1`] = `
|
||||
className="expandCollapseIcon"
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
src=""
|
||||
src={Object {}}
|
||||
tabIndex={0}
|
||||
/>
|
||||
<span
|
||||
@@ -383,7 +383,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
||||
className="expandCollapseIcon"
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
src=""
|
||||
src={Object {}}
|
||||
tabIndex={0}
|
||||
/>
|
||||
<span
|
||||
@@ -553,7 +553,7 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
|
||||
className="expandCollapseIcon"
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
src=""
|
||||
src={Object {}}
|
||||
tabIndex={0}
|
||||
/>
|
||||
<span
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
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 { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import * as ko from "knockout";
|
||||
import React from "react";
|
||||
import _ from "underscore";
|
||||
@@ -23,7 +27,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 +262,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,9 +386,15 @@ export default class Explorer {
|
||||
this._isInitializingNotebooks = false;
|
||||
}
|
||||
|
||||
public async allocateContainer(): Promise<void> {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
const isAllocating = useNotebook.getState().isAllocating;
|
||||
public async allocateContainer(poolId: PoolIdType): Promise<void> {
|
||||
const shouldUseNotebookStates = poolId === PoolIdType.DefaultPoolId ? true : false;
|
||||
const notebookServerInfo = shouldUseNotebookStates
|
||||
? useNotebook.getState().notebookServerInfo
|
||||
: useQueryCopilot.getState().notebookServerInfo;
|
||||
|
||||
const isAllocating = shouldUseNotebookStates
|
||||
? useNotebook.getState().isAllocating
|
||||
: useQueryCopilot.getState().isAllocatingContainer;
|
||||
if (
|
||||
isAllocating === false &&
|
||||
(notebookServerInfo === undefined ||
|
||||
@@ -353,23 +402,28 @@ export default class Explorer {
|
||||
) {
|
||||
const provisionData: IProvisionData = {
|
||||
cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint,
|
||||
poolId: PoolIdType.DefaultPoolId,
|
||||
poolId: shouldUseNotebookStates ? undefined : poolId,
|
||||
};
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.Connecting,
|
||||
};
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
|
||||
shouldUseNotebookStates && useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
|
||||
let connectionInfo;
|
||||
try {
|
||||
TelemetryProcessor.traceStart(Action.PhoenixConnection, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
});
|
||||
useNotebook.getState().setIsAllocating(true);
|
||||
shouldUseNotebookStates
|
||||
? useNotebook.getState().setIsAllocating(true)
|
||||
: useQueryCopilot.getState().setIsAllocatingContainer(true);
|
||||
|
||||
connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
|
||||
if (!connectionInfo?.data?.phoenixServiceUrl) {
|
||||
throw new Error(`PhoenixServiceUrl is invalid!`);
|
||||
}
|
||||
await this.setNotebookInfo(connectionInfo, connectionStatus);
|
||||
await this.setNotebookInfo(shouldUseNotebookStates, connectionInfo, connectionStatus);
|
||||
TelemetryProcessor.traceSuccess(Action.PhoenixConnection, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
});
|
||||
@@ -381,7 +435,9 @@ export default class Explorer {
|
||||
errorStack: getErrorStack(error),
|
||||
});
|
||||
connectionStatus.status = ConnectionStatusType.Failed;
|
||||
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||
shouldUseNotebookStates
|
||||
? useNotebook.getState().resetContainerConnection(connectionStatus)
|
||||
: useQueryCopilot.getState().resetContainerConnection();
|
||||
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
|
||||
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
|
||||
} else {
|
||||
@@ -394,7 +450,9 @@ export default class Explorer {
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
useNotebook.getState().setIsAllocating(false);
|
||||
shouldUseNotebookStates
|
||||
? useNotebook.getState().setIsAllocating(false)
|
||||
: useQueryCopilot.getState().setIsAllocatingContainer(false);
|
||||
this.refreshCommandBarButtons();
|
||||
this.refreshNotebookList();
|
||||
this._isInitializingNotebooks = false;
|
||||
@@ -403,6 +461,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
private async setNotebookInfo(
|
||||
shouldUseNotebookStates: boolean,
|
||||
connectionInfo: IResponse<IPhoenixServiceInfo>,
|
||||
connectionStatus: DataModels.ContainerConnectionInfo
|
||||
) {
|
||||
@@ -410,21 +469,26 @@ export default class Explorer {
|
||||
forwardingId: connectionInfo.data.forwardingId,
|
||||
dbAccountName: userContext.databaseAccount.name,
|
||||
};
|
||||
await this.phoenixClient.initiateContainerHeartBeat(containerData);
|
||||
await this.phoenixClient.initiateContainerHeartBeat(shouldUseNotebookStates, containerData);
|
||||
|
||||
connectionStatus.status = ConnectionStatusType.Connected;
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
useNotebook.getState().setNotebookServerInfo({
|
||||
shouldUseNotebookStates && useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
|
||||
const noteBookServerInfo = {
|
||||
notebookServerEndpoint:
|
||||
(validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
|
||||
userContext.features.notebookServerUrl) ||
|
||||
connectionInfo.data.phoenixServiceUrl,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.data.authToken,
|
||||
forwardingId: connectionInfo.data.forwardingId,
|
||||
});
|
||||
this.notebookManager?.notebookClient
|
||||
.getMemoryUsage()
|
||||
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
|
||||
};
|
||||
shouldUseNotebookStates
|
||||
? useNotebook.getState().setNotebookServerInfo(noteBookServerInfo)
|
||||
: useQueryCopilot.getState().setNotebookServerInfo(noteBookServerInfo);
|
||||
shouldUseNotebookStates &&
|
||||
this.notebookManager?.notebookClient
|
||||
.getMemoryUsage()
|
||||
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
|
||||
}
|
||||
|
||||
public resetNotebookWorkspace(): void {
|
||||
@@ -498,7 +562,7 @@ export default class Explorer {
|
||||
throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`);
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
await this.setNotebookInfo(connectionInfo, connectionStatus);
|
||||
await this.setNotebookInfo(true, connectionInfo, connectionStatus);
|
||||
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||
}
|
||||
logConsoleInfo("Successfully reset notebook workspace");
|
||||
@@ -608,6 +672,8 @@ export default class Explorer {
|
||||
private _initSettings() {
|
||||
if (!ExplorerSettings.hasSettingsDefined()) {
|
||||
ExplorerSettings.createDefaultSettings();
|
||||
} else {
|
||||
ExplorerSettings.ensurePriorityLevel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,7 +777,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 +1002,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 +1011,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 +1090,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 +1234,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 +1271,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 +1357,7 @@ export default class Explorer {
|
||||
return;
|
||||
}
|
||||
|
||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection);
|
||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
||||
useDatabases.setState({ sampleDataResourceTokenCollection });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ exports[`<EditorNodePropertiesComponent /> renders component 1`] = `
|
||||
>
|
||||
<img
|
||||
alt="Delete"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
</AccessibleElement>
|
||||
</td>
|
||||
@@ -185,7 +185,7 @@ exports[`<EditorNodePropertiesComponent /> renders component 1`] = `
|
||||
>
|
||||
<img
|
||||
alt="Delete"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
</AccessibleElement>
|
||||
</td>
|
||||
@@ -203,7 +203,7 @@ exports[`<EditorNodePropertiesComponent /> renders component 1`] = `
|
||||
>
|
||||
<img
|
||||
alt="Add"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
Add Property
|
||||
</AccessibleElement>
|
||||
@@ -317,7 +317,7 @@ exports[`<EditorNodePropertiesComponent /> renders proper unicode 1`] = `
|
||||
>
|
||||
<img
|
||||
alt="Delete"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
</AccessibleElement>
|
||||
</td>
|
||||
@@ -379,7 +379,7 @@ exports[`<EditorNodePropertiesComponent /> renders proper unicode 1`] = `
|
||||
>
|
||||
<img
|
||||
alt="Delete"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
</AccessibleElement>
|
||||
</td>
|
||||
@@ -397,7 +397,7 @@ exports[`<EditorNodePropertiesComponent /> renders proper unicode 1`] = `
|
||||
>
|
||||
<img
|
||||
alt="Add"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
Add Property
|
||||
</AccessibleElement>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
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";
|
||||
@@ -268,7 +270,6 @@ function createNewCollectionGroup(container: Explorer): CommandButtonComponentPr
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
id: "createNewContainerCommandButton",
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -314,7 +315,6 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -328,6 +328,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
||||
onCommandClick: () => {
|
||||
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);
|
||||
@@ -493,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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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} />,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,7 +22,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
>
|
||||
<img
|
||||
alt="in progress items"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
<span
|
||||
className="numInProgress"
|
||||
@@ -35,7 +35,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
>
|
||||
<img
|
||||
alt="error items"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
<span
|
||||
className="numErroredItems"
|
||||
@@ -48,7 +48,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
>
|
||||
<img
|
||||
alt="info items"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
<span
|
||||
className="numInfoItems"
|
||||
@@ -64,6 +64,8 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
className="headerStatus"
|
||||
>
|
||||
<span
|
||||
aria-atomic="true"
|
||||
aria-live="assertive"
|
||||
className="headerStatusEllipsis"
|
||||
/>
|
||||
</span>
|
||||
@@ -148,7 +150,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
>
|
||||
<img
|
||||
alt="clear notifications image"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
Clear Notifications
|
||||
</span>
|
||||
@@ -183,7 +185,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
>
|
||||
<img
|
||||
alt="in progress items"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
<span
|
||||
className="numInProgress"
|
||||
@@ -196,7 +198,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
>
|
||||
<img
|
||||
alt="error items"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
<span
|
||||
className="numErroredItems"
|
||||
@@ -209,7 +211,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
>
|
||||
<img
|
||||
alt="info items"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
<span
|
||||
className="numInfoItems"
|
||||
@@ -225,6 +227,8 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
className="headerStatus"
|
||||
>
|
||||
<span
|
||||
aria-atomic="true"
|
||||
aria-live="assertive"
|
||||
className="headerStatusEllipsis"
|
||||
>
|
||||
message
|
||||
@@ -311,7 +315,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
>
|
||||
<img
|
||||
alt="clear notifications image"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
Clear Notifications
|
||||
</span>
|
||||
@@ -326,7 +330,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
<img
|
||||
alt="info"
|
||||
className="infoIcon"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
<span
|
||||
className="date"
|
||||
|
||||
@@ -100,9 +100,6 @@ const addInitialCodeCellEpic = (
|
||||
*/
|
||||
const formWebSocketURL = (serverConfig: NotebookServiceConfig, kernelId: string, sessionId?: string): string => {
|
||||
const params = new URLSearchParams();
|
||||
if (serverConfig.token) {
|
||||
params.append("token", serverConfig.token);
|
||||
}
|
||||
if (sessionId) {
|
||||
params.append("session_id", sessionId);
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 '\' '/' '#' '?'"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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() && (
|
||||
|
||||
@@ -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=""
|
||||
/>
|
||||
|
||||
@@ -140,6 +140,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
setDatabaseInput(newInput);
|
||||
}}
|
||||
ariaLabel={confirmDatabase}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{isLastNonEmptyDatabase() && (
|
||||
|
||||
@@ -4340,7 +4340,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
id="deleteparam"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
src=""
|
||||
src={Object {}}
|
||||
width={20}
|
||||
>
|
||||
<ImageBase
|
||||
@@ -4350,7 +4350,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
id="deleteparam"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
src=""
|
||||
src={Object {}}
|
||||
styles={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
@@ -4640,12 +4640,12 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
alt="Delete param"
|
||||
className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-87"
|
||||
id="deleteparam"
|
||||
key="fabricImage"
|
||||
key="fabricImage[object Object]"
|
||||
onClick={[Function]}
|
||||
onError={[Function]}
|
||||
onLoad={[Function]}
|
||||
role="button"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
</div>
|
||||
</ImageBase>
|
||||
@@ -4661,7 +4661,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
id="addparam"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
src=""
|
||||
src={Object {}}
|
||||
width={20}
|
||||
>
|
||||
<ImageBase
|
||||
@@ -4671,7 +4671,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
id="addparam"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
src=""
|
||||
src={Object {}}
|
||||
styles={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
@@ -4961,12 +4961,12 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
alt="Add param"
|
||||
className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-87"
|
||||
id="addparam"
|
||||
key="fabricImage"
|
||||
key="fabricImage[object Object]"
|
||||
onClick={[Function]}
|
||||
onError={[Function]}
|
||||
onLoad={[Function]}
|
||||
role="button"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
</div>
|
||||
</ImageBase>
|
||||
@@ -4989,13 +4989,13 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
alt="Add param"
|
||||
height={30}
|
||||
key=".0:$.0"
|
||||
src=""
|
||||
src={Object {}}
|
||||
width={20}
|
||||
>
|
||||
<ImageBase
|
||||
alt="Add param"
|
||||
height={30}
|
||||
src=""
|
||||
src={Object {}}
|
||||
styles={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
@@ -5284,10 +5284,10 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
<img
|
||||
alt="Add param"
|
||||
className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-87"
|
||||
key="fabricImage"
|
||||
key="fabricImage[object Object]"
|
||||
onError={[Function]}
|
||||
onLoad={[Function]}
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
</div>
|
||||
</ImageBase>
|
||||
|
||||
@@ -39,7 +39,7 @@ exports[`Load Query Pane should render Default properly 1`] = `
|
||||
className="fileIcon"
|
||||
height={20}
|
||||
imageFit={4}
|
||||
src=""
|
||||
src={Object {}}
|
||||
width={20}
|
||||
/>
|
||||
<input
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 "None" 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">
|
||||
|
||||
@@ -44,13 +44,13 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
|
||||
alt="Add Property"
|
||||
height={30}
|
||||
key=".0:$.0"
|
||||
src=""
|
||||
src={Object {}}
|
||||
width={16}
|
||||
>
|
||||
<ImageBase
|
||||
alt="Add Property"
|
||||
height={30}
|
||||
src=""
|
||||
src={Object {}}
|
||||
styles={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
@@ -339,10 +339,10 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
|
||||
<img
|
||||
alt="Add Property"
|
||||
className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-55"
|
||||
key="fabricImage"
|
||||
key="fabricImage[object Object]"
|
||||
onError={[Function]}
|
||||
onLoad={[Function]}
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
</div>
|
||||
</ImageBase>
|
||||
|
||||
@@ -39,13 +39,13 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
|
||||
alt="Add Entity"
|
||||
height={30}
|
||||
key=".0:$.0"
|
||||
src=""
|
||||
src={Object {}}
|
||||
width={16}
|
||||
>
|
||||
<ImageBase
|
||||
alt="Add Entity"
|
||||
height={30}
|
||||
src=""
|
||||
src={Object {}}
|
||||
styles={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
@@ -334,10 +334,10 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
|
||||
<img
|
||||
alt="Add Entity"
|
||||
className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-55"
|
||||
key="fabricImage"
|
||||
key="fabricImage[object Object]"
|
||||
onError={[Function]}
|
||||
onLoad={[Function]}
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
</div>
|
||||
</ImageBase>
|
||||
|
||||
@@ -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=""
|
||||
/>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -10,12 +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";
|
||||
import { getUserEmail } from "../../../Utils/UserUtils";
|
||||
|
||||
export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
|
||||
export const QueryCopilotFeedbackModal = ({ explorer }: { explorer: Explorer }): JSX.Element => {
|
||||
const {
|
||||
generatedQuery,
|
||||
userPrompt,
|
||||
@@ -28,6 +29,7 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
|
||||
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 }}>
|
||||
@@ -77,11 +79,13 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
|
||||
}}
|
||||
></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
|
||||
@@ -97,7 +101,10 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
|
||||
onClick={() => {
|
||||
closeFeedbackModal();
|
||||
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
|
||||
submitFeedback({ generatedQuery, likeQuery, description, userPrompt, contact });
|
||||
SubmitFeedback({
|
||||
params: { generatedQuery, likeQuery, description, userPrompt, contact },
|
||||
explorer: explorer,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
32
src/Explorer/QueryCopilot/Modal/WelcomeModal.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -42,8 +42,8 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack horizontalAlign="center">
|
||||
<Stack.Item align="center">
|
||||
<Text className="title bold">Welcome to Copilot in CosmosDB</Text>
|
||||
<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>
|
||||
@@ -52,7 +52,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
||||
</StackItem>
|
||||
<StackItem align="start">
|
||||
<Text className="bold">
|
||||
Let copilot do the work for you
|
||||
Let Query Copilot do the work for you
|
||||
<br />
|
||||
</Text>
|
||||
</StackItem>
|
||||
@@ -60,7 +60,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
||||
<Text>
|
||||
Ask Copilot to generate a query by describing the query in your words.
|
||||
<br />
|
||||
<Link href="">Learn more</Link>
|
||||
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
|
||||
</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item align="center" className="text">
|
||||
@@ -78,7 +78,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
||||
<Text>
|
||||
AI-generated content can have mistakes. Make sure it’s accurate and appropriate before using it.
|
||||
<br />
|
||||
<Link href="">Read preview terms</Link>
|
||||
<Link href="http://aka.ms/cdb-copilot-preview-terms">Read preview terms</Link>
|
||||
</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item align="center" className="text">
|
||||
@@ -88,15 +88,16 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
||||
</StackItem>
|
||||
<StackItem align="start">
|
||||
<Text className="bold">
|
||||
Copilot currently works only a sample database
|
||||
Query Copilot works on a sample database.
|
||||
<br />
|
||||
</Text>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<Text>
|
||||
Copilot is setup on a sample database we have configured for you at no cost
|
||||
While in Private Preview, Query Copilot is setup to work on sample database we have configured for you
|
||||
at no cost.
|
||||
<br />
|
||||
<Link href="">Learn more</Link>
|
||||
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
|
||||
</Text>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
@@ -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={Object {}}
|
||||
/>
|
||||
</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={Object {}}
|
||||
/>
|
||||
</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={Object {}}
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem
|
||||
align="start"
|
||||
>
|
||||
<Text
|
||||
className="bold"
|
||||
>
|
||||
Use your judgement
|
||||
<br />
|
||||
</Text>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<Text>
|
||||
AI-generated content can have mistakes. Make sure it’s 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={Object {}}
|
||||
/>
|
||||
</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>
|
||||
`;
|
||||
@@ -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)",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +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}
|
||||
clearFeedback={() => any}
|
||||
showFeedbackBar={() => any}
|
||||
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" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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={
|
||||
@@ -38,7 +40,7 @@ exports[`Copy Popup snapshot test should render when showCopyPopup is true 1`] =
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
src={Object {}}
|
||||
style={
|
||||
Object {
|
||||
"height": 15,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-console */
|
||||
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
import {
|
||||
Callout,
|
||||
CommandBarButton,
|
||||
@@ -18,28 +18,32 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import {
|
||||
ContainerStatusType,
|
||||
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 { sampleDataClient } from "Common/SampleDataClient";
|
||||
import { getCommonQueryOptions } 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 { 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";
|
||||
@@ -50,7 +54,6 @@ 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";
|
||||
@@ -60,49 +63,55 @@ interface SuggestedPrompt {
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface QueryCopilotTabProps {
|
||||
initialInput: string;
|
||||
explorer: Explorer;
|
||||
}
|
||||
|
||||
interface GenerateSQLQueryResponse {
|
||||
apiVersion: string;
|
||||
sql: string;
|
||||
explanation: string;
|
||||
generateStart: string;
|
||||
generateEnd: string;
|
||||
}
|
||||
|
||||
const promptStyles: IButtonStyles = {
|
||||
root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } },
|
||||
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 [dislikeQuery, setDislikeQuery] = useState<boolean>();
|
||||
const [showCallout, setShowCallout] = useState<boolean>(false);
|
||||
const [showSamplePrompts, setShowSamplePrompts] = useState<boolean>(false);
|
||||
const [queryIterator, setQueryIterator] = useState<MinimalQueryIterator>();
|
||||
const [queryResults, setQueryResults] = useState<QueryResults>();
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
|
||||
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
|
||||
const inputEdited = useRef(false);
|
||||
const [isSamplePromptsOpen, setIsSamplePromptsOpen] = useState<boolean>(false);
|
||||
const [showDeletePopup, setShowDeletePopup] = useState<boolean>(false);
|
||||
const [showFeedbackBar, setShowFeedbackBar] = useState<boolean>(false);
|
||||
const [showCopyPopup, setshowCopyPopup] = useState<boolean>(false);
|
||||
const [showErrorMessageBar, setShowErrorMessageBar] = useState<boolean>(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,
|
||||
} = useQueryCopilot();
|
||||
|
||||
const sampleProps: SamplePromptsProps = {
|
||||
isSamplePromptsOpen: isSamplePromptsOpen,
|
||||
@@ -128,12 +137,12 @@ 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: "Give me all customers whose names start with C" },
|
||||
{ id: 2, text: "Show me all customers" },
|
||||
{ id: 3, text: "Show me all customers who bought a bike in 2019" },
|
||||
{ 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);
|
||||
@@ -155,38 +164,67 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
||||
};
|
||||
|
||||
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 {
|
||||
setIsGeneratingQuery(true);
|
||||
setShowDeletePopup(false);
|
||||
useTabs.getState().setIsTabExecuting(true);
|
||||
useTabs.getState().setIsQueryErrorThrown(false);
|
||||
if (
|
||||
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
|
||||
userContext.features.enableCopilotPhoenixGateaway
|
||||
) {
|
||||
await explorer.allocateContainer(PoolIdType.QueryCopilot);
|
||||
}
|
||||
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 = useQueryCopilot.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.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");
|
||||
@@ -200,15 +238,14 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const querySampleDocuments = (query: string, options: FeedOptions): QueryIterator<ItemDefinition & Resource> => {
|
||||
options = getCommonQueryOptions(options);
|
||||
return sampleDataClient()
|
||||
.database(QueryCopilotSampleDatabaseId)
|
||||
.container(QueryCopilotSampleContainerId)
|
||||
.items.query(query, options);
|
||||
};
|
||||
|
||||
const onExecuteQueryClick = async (): Promise<void> => {
|
||||
traceStart(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||
correlationId: useQueryCopilot.getState().correlationId,
|
||||
userPrompt: userPrompt,
|
||||
generatedQuery: generatedQuery,
|
||||
generatedQueryComments: generatedQueryComments,
|
||||
executedQuery: selectedQuery || query,
|
||||
});
|
||||
const queryToExecute = selectedQuery || query;
|
||||
const queryIterator = querySampleDocuments(queryToExecute, {
|
||||
enableCrossPartitionQuery: shouldEnableCrossPartitionKey(),
|
||||
@@ -233,8 +270,16 @@ 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);
|
||||
@@ -254,6 +299,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
||||
commandButtonLabel: executeQueryBtnLabel,
|
||||
ariaLabel: executeQueryBtnLabel,
|
||||
hasPopup: false,
|
||||
disabled: query?.trim() === "",
|
||||
};
|
||||
|
||||
const saveQueryBtn = {
|
||||
@@ -264,18 +310,20 @@ 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 => {
|
||||
if (!inputEdited.current) {
|
||||
@@ -294,282 +342,311 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
||||
setShowCallout(false);
|
||||
};
|
||||
|
||||
const startGenerateQueryProcess = () => {
|
||||
updateHistories();
|
||||
generateSQLQuery();
|
||||
resetButtonState();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
useCommandBar.getState().setContextButtons(getCommandbarButtons());
|
||||
}, [query, selectedQuery]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (initialInput) {
|
||||
generateSQLQuery();
|
||||
}
|
||||
showTeachingBubble();
|
||||
useTabs.getState().setIsQueryErrorThrown(false);
|
||||
}, []);
|
||||
|
||||
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%", position: "relative" }}>
|
||||
<TextField
|
||||
id="naturalLanguageInput"
|
||||
value={userPrompt}
|
||||
onChange={handleUserPromptChange}
|
||||
onClick={() => {
|
||||
inputEdited.current = true;
|
||||
setShowSamplePrompts(true);
|
||||
}}
|
||||
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"
|
||||
>
|
||||
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 }}
|
||||
<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") {
|
||||
inputEdited.current = true;
|
||||
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"
|
||||
>
|
||||
sample prompts
|
||||
</Link>{" "}
|
||||
or write your own query
|
||||
</TeachingBubble>
|
||||
)}
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Send" }}
|
||||
disabled={isGeneratingQuery || !userPrompt.trim()}
|
||||
style={{ marginLeft: 8 }}
|
||||
onClick={() => {
|
||||
updateHistories();
|
||||
generateSQLQuery();
|
||||
resetButtonState();
|
||||
}}
|
||||
/>
|
||||
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
|
||||
{showSamplePrompts && (
|
||||
<Callout
|
||||
styles={{ root: { minWidth: 400 } }}
|
||||
target="#naturalLanguageInput"
|
||||
isBeakVisible={false}
|
||||
onDismiss={() => setShowSamplePrompts(false)}
|
||||
directionalHintFixed={true}
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
alignTargetEdge={true}
|
||||
gapSpace={4}
|
||||
>
|
||||
<Stack>
|
||||
{filteredHistories?.length > 0 && (
|
||||
<Stack>
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: "#0078D4",
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Recent
|
||||
</Text>
|
||||
{filteredHistories.map((history, i) => (
|
||||
<DefaultButton
|
||||
key={i}
|
||||
onClick={() => {
|
||||
setUserPrompt(history);
|
||||
setShowSamplePrompts(false);
|
||||
}}
|
||||
onRenderIcon={() => <Image src={RecentIcon} />}
|
||||
styles={promptStyles}
|
||||
>
|
||||
{history}
|
||||
</DefaultButton>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: "#0078D4",
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
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 }}
|
||||
>
|
||||
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>
|
||||
))}
|
||||
<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's accurate and appropriate before using it.{" "}
|
||||
<Link href="" target="_blank">
|
||||
Read preview terms
|
||||
</Link>
|
||||
{showErrorMessageBar && (
|
||||
<Stack style={{ backgroundColor: "#FEF0F1", padding: "4px 8px" }} horizontal verticalAlign="center">
|
||||
<Image src={XErrorMessage} style={{ marginRight: "8px" }} />
|
||||
<Text style={{ fontSize: 12 }}>
|
||||
We ran into an error and were not able to execute query. Please try again after sometime
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
{showFeedbackBar && (
|
||||
<Stack style={{ backgroundColor: "#FFF8F0", padding: "2px 8px" }} horizontal verticalAlign="center">
|
||||
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
|
||||
{showCallout && !hideFeedbackModalForLikedQueries && (
|
||||
<Callout
|
||||
style={{ padding: 8 }}
|
||||
target="#likeBtn"
|
||||
onDismiss={() => {
|
||||
setShowCallout(false);
|
||||
submitFeedback({ generatedQuery, likeQuery, description: "", userPrompt: userPrompt });
|
||||
}}
|
||||
directionalHint={DirectionalHint.topCenter}
|
||||
>
|
||||
<Text>
|
||||
Thank you. Need to give{" "}
|
||||
<Link
|
||||
onClick={() => {
|
||||
setShowCallout(false);
|
||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
|
||||
}}
|
||||
>
|
||||
more feedback?
|
||||
</Link>
|
||||
</Text>
|
||||
</Callout>
|
||||
sample prompts
|
||||
</Link>{" "}
|
||||
or write your own query
|
||||
</TeachingBubble>
|
||||
)}
|
||||
<IconButton
|
||||
id="likeBtn"
|
||||
style={{ marginLeft: 20 }}
|
||||
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
||||
onClick={() => {
|
||||
setShowCallout(!likeQuery);
|
||||
setLikeQuery(!likeQuery);
|
||||
if (dislikeQuery) {
|
||||
setDislikeQuery(!dislikeQuery);
|
||||
}
|
||||
}}
|
||||
iconProps={{ iconName: "Send" }}
|
||||
disabled={isGeneratingQuery || !userPrompt.trim()}
|
||||
style={{ marginLeft: 8 }}
|
||||
onClick={() => startGenerateQueryProcess()}
|
||||
/>
|
||||
<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>
|
||||
{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);
|
||||
inputEdited.current = true;
|
||||
}}
|
||||
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);
|
||||
inputEdited.current = true;
|
||||
}}
|
||||
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'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>
|
||||
<WelcomeModal visible={localStorage.getItem("hideWelcomeModal") !== "true"} />
|
||||
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
|
||||
{query !== "" && query.trim().length !== 0 && (
|
||||
<DeletePopup
|
||||
showDeletePopup={showDeletePopup}
|
||||
setShowDeletePopup={setShowDeletePopup}
|
||||
setQuery={setQuery}
|
||||
clearFeedback={resetButtonState}
|
||||
showFeedbackBar={setShowFeedbackBar}
|
||||
/>
|
||||
)}
|
||||
<CopyPopup showCopyPopup={showCopyPopup} setShowCopyPopup={setshowCopyPopup} />
|
||||
)}
|
||||
<CopyPopup showCopyPopup={showCopyPopup} setShowCopyPopup={setshowCopyPopup} />
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
154
src/Explorer/QueryCopilot/QueryCopilotUtilities.test.tsx
Normal 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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
138
src/Explorer/QueryCopilot/Shared/QueryCopilotClient.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { QueryCopilotSampleContainerSchema, ShortenedQueryCopilotSampleContainerSchema } from "Common/Constants";
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import { createUri } from "Common/UrlUtility";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||
import { userContext } from "UserContext";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
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 = useQueryCopilot.getState();
|
||||
mockStore.correlationId = "mocked-correlation-id";
|
||||
mockStore.notebookServerInfo = {
|
||||
notebookServerEndpoint: "mocked-endpoint",
|
||||
authToken: "mocked-token",
|
||||
forwardingId: "mocked-forwarding-id",
|
||||
};
|
||||
|
||||
const feedbackUri = userContext.features.enableCopilotPhoenixGateaway
|
||||
? createUri(useQueryCopilot.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));
|
||||
});
|
||||
});
|
||||
});
|
||||
135
src/Explorer/QueryCopilot/Shared/QueryCopilotClient.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
ContainerStatusType,
|
||||
PoolIdType,
|
||||
QueryCopilotSampleContainerSchema,
|
||||
ShortenedQueryCopilotSampleContainerSchema,
|
||||
} from "Common/Constants";
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import { createUri } from "Common/UrlUtility";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
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);
|
||||
useQueryCopilot.getState().setShouldIncludeInMessages(true);
|
||||
useQueryCopilot.getState().setShowQueryExplanation(false);
|
||||
useQueryCopilot.getState().setShowExplanationBubble(false);
|
||||
useTabs.getState().setIsTabExecuting(true);
|
||||
useTabs.getState().setIsQueryErrorThrown(false);
|
||||
useQueryCopilot
|
||||
.getState()
|
||||
.setChatMessages([...useQueryCopilot.getState().chatMessages, { source: 0, message: userPrompt }]);
|
||||
try {
|
||||
if (
|
||||
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
|
||||
userContext.features.enableCopilotPhoenixGateaway
|
||||
) {
|
||||
await explorer.allocateContainer(PoolIdType.QueryCopilot);
|
||||
}
|
||||
|
||||
useQueryCopilot.getState().refreshCorrelationId();
|
||||
const serverInfo = useQueryCopilot.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.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}`;
|
||||
if (useQueryCopilot.getState().shouldIncludeInMessages) {
|
||||
useQueryCopilot
|
||||
.getState()
|
||||
.setChatMessages([
|
||||
...useQueryCopilot.getState().chatMessages,
|
||||
{ source: 1, message: query, explanation: generateSQLQueryResponse.explanation },
|
||||
]);
|
||||
useQueryCopilot.getState().setShowExplanationBubble(true);
|
||||
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 payload = {
|
||||
containerSchema: userContext.features.enableCopilotFullSchema
|
||||
? QueryCopilotSampleContainerSchema
|
||||
: ShortenedQueryCopilotSampleContainerSchema,
|
||||
like: likeQuery ? "like" : "dislike",
|
||||
generatedSql: generatedQuery,
|
||||
userPrompt,
|
||||
description: description || "",
|
||||
contact: contact || "",
|
||||
};
|
||||
if (
|
||||
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
|
||||
userContext.features.enableCopilotPhoenixGateaway
|
||||
) {
|
||||
await explorer.allocateContainer(PoolIdType.QueryCopilot);
|
||||
}
|
||||
const serverInfo = useQueryCopilot.getState().notebookServerInfo;
|
||||
const feedbackUri = userContext.features.enableCopilotPhoenixGateaway
|
||||
? createUri(serverInfo.notebookServerEndpoint, "feedback")
|
||||
: createUri("https://copilotorchestrater.azurewebsites.net/", "feedback");
|
||||
await fetch(feedbackUri, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, "copilotSubmitFeedback");
|
||||
}
|
||||
};
|
||||
32
src/Explorer/QueryCopilot/Shared/QueryCopilotInterfaces.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -87,7 +87,7 @@ exports[`Sample Prompts snapshot test should render properly if isSamplePromptsO
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
src={Object {}}
|
||||
style={
|
||||
Object {
|
||||
"height": 25,
|
||||
@@ -191,7 +191,7 @@ exports[`Sample Prompts snapshot test should render properly if isSamplePromptsO
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
src={Object {}}
|
||||
style={
|
||||
Object {
|
||||
"height": 25,
|
||||
@@ -295,7 +295,7 @@ exports[`Sample Prompts snapshot test should render properly if isSamplePromptsO
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
src={Object {}}
|
||||
style={
|
||||
Object {
|
||||
"height": 25,
|
||||
@@ -477,7 +477,7 @@ exports[`Sample Prompts snapshot test should render properly if isSamplePromptsO
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
src={Object {}}
|
||||
style={
|
||||
Object {
|
||||
"height": 25,
|
||||
@@ -581,7 +581,7 @@ exports[`Sample Prompts snapshot test should render properly if isSamplePromptsO
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
src={Object {}}
|
||||
style={
|
||||
Object {
|
||||
"height": 25,
|
||||
@@ -685,7 +685,7 @@ exports[`Sample Prompts snapshot test should render properly if isSamplePromptsO
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
src={Object {}}
|
||||
style={
|
||||
Object {
|
||||
"height": 25,
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Text } from "@fluentui/react";
|
||||
import { shallow } from "enzyme";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React from "react";
|
||||
import { ExplanationBubble } from "./ExplanationBubble";
|
||||
|
||||
describe("Explanation Bubble", () => {
|
||||
const initialStoreState = useQueryCopilot.getState();
|
||||
beforeEach(() => {
|
||||
useQueryCopilot.setState(initialStoreState, true);
|
||||
useQueryCopilot.getState().showExplanationBubble = true;
|
||||
useQueryCopilot.getState().shouldIncludeInMessages = false;
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("should render explanation bubble with generated comments", () => {
|
||||
useQueryCopilot.getState().showQueryExplanation = true;
|
||||
useQueryCopilot.getState().shouldIncludeInMessages = true;
|
||||
|
||||
const wrapper = shallow(<ExplanationBubble />);
|
||||
|
||||
expect(wrapper.find("Stack")).toHaveLength(1);
|
||||
expect(wrapper.find("Text")).toHaveLength(0);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render 'Explain this query' link", () => {
|
||||
const mockSetChatMessages = jest.fn();
|
||||
const mockSetIsGeneratingExplanation = jest.fn();
|
||||
const mockSetShouldIncludeInMessages = jest.fn();
|
||||
const mockSetShowQueryExplanation = jest.fn();
|
||||
useQueryCopilot.getState().setChatMessages = mockSetChatMessages;
|
||||
useQueryCopilot.getState().setIsGeneratingExplanation = mockSetIsGeneratingExplanation;
|
||||
useQueryCopilot.getState().setShouldIncludeInMessages = mockSetShouldIncludeInMessages;
|
||||
useQueryCopilot.getState().setShowQueryExplanation = mockSetShowQueryExplanation;
|
||||
|
||||
const wrapper = shallow(<ExplanationBubble />);
|
||||
|
||||
const textElement = wrapper.find(Text);
|
||||
textElement.simulate("click");
|
||||
|
||||
expect(mockSetChatMessages).toHaveBeenCalledWith([
|
||||
...initialStoreState.chatMessages,
|
||||
{ source: 0, message: "Explain this query to me" },
|
||||
]);
|
||||
expect(mockSetIsGeneratingExplanation).toHaveBeenCalledWith(true);
|
||||
expect(mockSetShouldIncludeInMessages).toHaveBeenCalledWith(true);
|
||||
expect(mockSetShowQueryExplanation).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(3000);
|
||||
|
||||
expect(mockSetIsGeneratingExplanation).toHaveBeenCalledWith(false);
|
||||
expect(mockSetShowQueryExplanation).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("should render nothing when conditions are not met", () => {
|
||||
useQueryCopilot.getState().showExplanationBubble = false;
|
||||
|
||||
const wrapper = shallow(<ExplanationBubble />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Stack, Text } from "@fluentui/react";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React from "react";
|
||||
|
||||
export const ExplanationBubble: React.FC = (): JSX.Element => {
|
||||
const {
|
||||
showExplanationBubble,
|
||||
isGeneratingQuery,
|
||||
showQueryExplanation,
|
||||
setShowQueryExplanation,
|
||||
chatMessages,
|
||||
setChatMessages,
|
||||
generatedQueryComments,
|
||||
isGeneratingExplanation,
|
||||
setIsGeneratingExplanation,
|
||||
shouldIncludeInMessages,
|
||||
setShouldIncludeInMessages,
|
||||
} = useQueryCopilot();
|
||||
|
||||
const showExplanation = () => {
|
||||
setChatMessages([...chatMessages, { source: 0, message: "Explain this query to me" }]);
|
||||
setIsGeneratingExplanation(true);
|
||||
setShouldIncludeInMessages(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsGeneratingExplanation(false);
|
||||
setShowQueryExplanation(true);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
showExplanationBubble &&
|
||||
!isGeneratingQuery &&
|
||||
!isGeneratingExplanation &&
|
||||
(showQueryExplanation && shouldIncludeInMessages ? (
|
||||
<Stack
|
||||
horizontalAlign="center"
|
||||
tokens={{ padding: 8, childrenGap: 8 }}
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
borderRadius: "8px",
|
||||
margin: "5px 10px",
|
||||
textAlign: "start",
|
||||
}}
|
||||
>
|
||||
{generatedQueryComments}
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "5px 5px 5px 40px",
|
||||
margin: "5px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
onClick={showExplanation}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
border: "1.5px solid #B0BEFF",
|
||||
width: "100%",
|
||||
padding: "2px",
|
||||
borderRadius: "4px",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Explain this query to me
|
||||
</Text>
|
||||
</Stack>
|
||||
))
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Explanation Bubble should render explanation bubble with generated comments 1`] = `
|
||||
<Stack
|
||||
horizontalAlign="center"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "white",
|
||||
"borderRadius": "8px",
|
||||
"margin": "5px 10px",
|
||||
"textAlign": "start",
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 8,
|
||||
"padding": 8,
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`Explanation Bubble should render nothing when conditions are not met 1`] = `""`;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { IconButton } from "@fluentui/react";
|
||||
import { CopyButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Copy/CopyButton";
|
||||
import { shallow } from "enzyme";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React from "react";
|
||||
|
||||
document.execCommand = jest.fn();
|
||||
|
||||
describe("Copy button snapshot tests", () => {
|
||||
it("should render and click copy", async () => {
|
||||
const testInput = "test input query";
|
||||
useQueryCopilot.getState().setGeneratedQuery(testInput);
|
||||
const wrapper = shallow(<CopyButton />);
|
||||
|
||||
const button = wrapper.find(IconButton).first();
|
||||
button.simulate("click", {});
|
||||
|
||||
expect(document.execCommand).toHaveBeenCalledWith("copy");
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { IconButton } from "@fluentui/react";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React from "react";
|
||||
import CopilotCopy from "../../../../../../../../images/CopilotCopy.svg";
|
||||
|
||||
export const CopyButton: React.FC = (): JSX.Element => {
|
||||
const copyGeneratedCode = (): void => {
|
||||
const queryElement = document.createElement("textarea");
|
||||
queryElement.value = useQueryCopilot.getState().generatedQuery;
|
||||
document.body.appendChild(queryElement);
|
||||
queryElement.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(queryElement);
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
iconProps={{ imageProps: { src: CopilotCopy } }}
|
||||
ariaLabel="Copy"
|
||||
onClick={copyGeneratedCode}
|
||||
></IconButton>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Copy button snapshot tests should render and click copy 1`] = `
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Copy"
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": Object {},
|
||||
},
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,205 @@
|
||||
import { Callout, IconButton, Link } from "@fluentui/react";
|
||||
import { useId } from "@fluentui/react-hooks";
|
||||
import { FeedbackButtons } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Feedback/FeedbackButtons";
|
||||
import { shallow } from "enzyme";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React from "react";
|
||||
import LikeHover from "../../../../../../../../images/CopilotLikeHover.svg";
|
||||
import LikePressed from "../../../../../../../../images/CopilotLikePressed.svg";
|
||||
import LikeRest from "../../../../../../../../images/CopilotLikeRest.svg";
|
||||
|
||||
useId as jest.Mock;
|
||||
|
||||
jest.mock("../../../../../../../../images/CopilotLikeHover.svg", () => "LikeHover");
|
||||
jest.mock("../../../../../../../../images/CopilotLikePressed.svg", () => "LikePressed");
|
||||
jest.mock("../../../../../../../../images/CopilotLikeRest.svg", () => "LikeRest");
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("Feedback buttons snapshot tests", () => {
|
||||
it("should click like and show callout", () => {
|
||||
const wrapper = shallow(<FeedbackButtons />);
|
||||
|
||||
let likeButton = wrapper.find(IconButton).first();
|
||||
const dislikeButton = wrapper.find(IconButton).last();
|
||||
likeButton.simulate("click");
|
||||
likeButton = wrapper.find(IconButton).first();
|
||||
const callout = wrapper.find(Callout).first();
|
||||
|
||||
expect(likeButton.props().iconProps.imageProps.src).toEqual(LikePressed);
|
||||
expect(dislikeButton.props().iconProps.imageProps.src).toEqual(LikeRest);
|
||||
expect(callout.exists()).toBeTruthy();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should click like and dismiss callout", () => {
|
||||
const wrapper = shallow(<FeedbackButtons />);
|
||||
|
||||
const likeButton = wrapper.find(IconButton).first();
|
||||
likeButton.simulate("click");
|
||||
let callout = wrapper.find(Callout).first();
|
||||
callout.simulate("dismiss");
|
||||
callout = wrapper.find(Callout).first();
|
||||
|
||||
expect(callout.exists()).toBeFalsy();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should click like and submit feedback", () => {
|
||||
const spy = jest.spyOn(useQueryCopilot.getState(), "openFeedbackModal");
|
||||
const wrapper = shallow(<FeedbackButtons />);
|
||||
|
||||
const likeButton = wrapper.find(IconButton).first();
|
||||
likeButton.simulate("click");
|
||||
const link = wrapper.find(Link).first();
|
||||
link.simulate("click");
|
||||
|
||||
expect(spy).toHaveBeenNthCalledWith(1, "", true, "");
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should hover over like", () => {
|
||||
const wrapper = shallow(<FeedbackButtons />);
|
||||
|
||||
let likeButton = wrapper.find(IconButton).first();
|
||||
likeButton.simulate("mouseover");
|
||||
likeButton = wrapper.find(IconButton).first();
|
||||
|
||||
expect(likeButton.props().iconProps.imageProps.src).toEqual(LikeHover);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should hover over rest like and leave", () => {
|
||||
const wrapper = shallow(<FeedbackButtons />);
|
||||
|
||||
let likeButton = wrapper.find(IconButton).first();
|
||||
likeButton.simulate("mouseover");
|
||||
likeButton.simulate("mouseleave");
|
||||
likeButton = wrapper.find(IconButton).first();
|
||||
|
||||
expect(likeButton.props().iconProps.imageProps.src).toEqual(LikeRest);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should hover over pressed like and leave", () => {
|
||||
const wrapper = shallow(<FeedbackButtons />);
|
||||
|
||||
let likeButton = wrapper.find(IconButton).first();
|
||||
likeButton.simulate("click");
|
||||
likeButton = wrapper.find(IconButton).first();
|
||||
likeButton.simulate("mouseover");
|
||||
likeButton.simulate("mouseleave");
|
||||
|
||||
expect(likeButton.props().iconProps.imageProps.src).toEqual(LikePressed);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should hover over like and click", () => {
|
||||
const wrapper = shallow(<FeedbackButtons />);
|
||||
|
||||
let likeButton = wrapper.find(IconButton).first();
|
||||
likeButton.simulate("mouseover");
|
||||
likeButton.simulate("click");
|
||||
likeButton = wrapper.find(IconButton).first();
|
||||
|
||||
expect(likeButton.props().iconProps.imageProps.src).toEqual(LikePressed);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should dobule click on like", () => {
|
||||
const wrapper = shallow(<FeedbackButtons />);
|
||||
|
||||
let likeButton = wrapper.find(IconButton).first();
|
||||
likeButton.simulate("click");
|
||||
likeButton = wrapper.find(IconButton).first();
|
||||
expect(likeButton.props().iconProps.imageProps.src).toEqual(LikePressed);
|
||||
|
||||
likeButton.simulate("click");
|
||||
likeButton = wrapper.find(IconButton).first();
|
||||
expect(likeButton.props().iconProps.imageProps.src).toEqual(LikeRest);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should click dislike and show popup", () => {
|
||||
const spy = jest.spyOn(useQueryCopilot.getState(), "openFeedbackModal");
|
||||
const wrapper = shallow(<FeedbackButtons />);
|
||||
|
||||
const likeButton = wrapper.find(IconButton).first();
|
||||
let dislikeButton = wrapper.find(IconButton).last();
|
||||
dislikeButton.simulate("click");
|
||||
const callout = wrapper.find(Callout).first();
|
||||
dislikeButton = wrapper.find(IconButton).last();
|
||||
|
||||
expect(likeButton.props().iconProps.imageProps.src).toEqual(LikeRest);
|
||||
expect(dislikeButton.props().iconProps.imageProps.src).toEqual(LikePressed);
|
||||
expect(spy).toHaveBeenNthCalledWith(1, "", false, "");
|
||||
expect(callout.exists()).toBeFalsy();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should hover over dislike", () => {
|
||||
const wrapper = shallow(<FeedbackButtons />);
|
||||
|
||||
let dislikeButton = wrapper.find(IconButton).last();
|
||||
dislikeButton.simulate("mouseover");
|
||||
dislikeButton = wrapper.find(IconButton).last();
|
||||
|
||||
expect(dislikeButton.props().iconProps.imageProps.src).toEqual(LikeHover);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should hover over rest dislike and leave", () => {
|
||||
const wrapper = shallow(<FeedbackButtons />);
|
||||
|
||||
let dislikeButton = wrapper.find(IconButton).last();
|
||||
dislikeButton.simulate("mouseover");
|
||||
dislikeButton.simulate("mouseleave");
|
||||
dislikeButton = wrapper.find(IconButton).last();
|
||||
|
||||
expect(dislikeButton.props().iconProps.imageProps.src).toEqual(LikeRest);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should hover over pressed dislike and leave", () => {
|
||||
const wrapper = shallow(<FeedbackButtons />);
|
||||
|
||||
let dislikeButton = wrapper.find(IconButton).last();
|
||||
dislikeButton.simulate("click");
|
||||
dislikeButton = wrapper.find(IconButton).last();
|
||||
expect(dislikeButton.props().iconProps.imageProps.src).toEqual(LikePressed);
|
||||
dislikeButton.simulate("mouseover");
|
||||
dislikeButton.simulate("mouseleave");
|
||||
dislikeButton = wrapper.find(IconButton).last();
|
||||
|
||||
expect(dislikeButton.props().iconProps.imageProps.src).toEqual(LikePressed);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should hover over dislike and click", () => {
|
||||
const wrapper = shallow(<FeedbackButtons />);
|
||||
|
||||
let dislikeButton = wrapper.find(IconButton).last();
|
||||
dislikeButton.simulate("mouseover");
|
||||
dislikeButton.simulate("click");
|
||||
dislikeButton = wrapper.find(IconButton).last();
|
||||
|
||||
expect(dislikeButton.props().iconProps.imageProps.src).toEqual(LikePressed);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should dobule click on dislike", () => {
|
||||
const wrapper = shallow(<FeedbackButtons />);
|
||||
|
||||
let dislikeButton = wrapper.find(IconButton).last();
|
||||
dislikeButton.simulate("click");
|
||||
dislikeButton = wrapper.find(IconButton).last();
|
||||
expect(dislikeButton.props().iconProps.imageProps.src).toEqual(LikePressed);
|
||||
|
||||
dislikeButton.simulate("click");
|
||||
dislikeButton = wrapper.find(IconButton).last();
|
||||
expect(dislikeButton.props().iconProps.imageProps.src).toEqual(LikeRest);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Callout, DirectionalHint, IconButton, Link, Stack, Text } from "@fluentui/react";
|
||||
import { useId } from "@fluentui/react-hooks";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React, { useState } from "react";
|
||||
import LikeHover from "../../../../../../../../images/CopilotLikeHover.svg";
|
||||
import LikePressed from "../../../../../../../../images/CopilotLikePressed.svg";
|
||||
import LikeRest from "../../../../../../../../images/CopilotLikeRest.svg";
|
||||
|
||||
export const FeedbackButtons: React.FC = (): JSX.Element => {
|
||||
const { generatedQuery, userPrompt } = useQueryCopilot();
|
||||
|
||||
const [likeQuery, setLikeQuery] = useState<boolean>(false);
|
||||
const [dislikeQuery, setDislikeQuery] = useState<boolean>(false);
|
||||
const [likeImageLink, setLikeImageLink] = useState<string>(LikeRest);
|
||||
const [dislikeImageLink, setDislikeImageLink] = useState<string>(LikeRest);
|
||||
const [calloutVisible, setCalloutVisible] = useState<boolean>(false);
|
||||
const likeBtnId = useId("likeBtn");
|
||||
const dislikeBtnId = useId("dislikeBtn");
|
||||
|
||||
return (
|
||||
<Stack horizontal>
|
||||
{calloutVisible && (
|
||||
<Callout
|
||||
target={`#${likeBtnId}`}
|
||||
onDismiss={() => setCalloutVisible(false)}
|
||||
directionalHint={DirectionalHint.topCenter}
|
||||
role="dialog"
|
||||
style={{ padding: "5px 12px 5px 12px", borderRadius: "4px" }}
|
||||
styles={{ beakCurtain: { borderRadius: "4px" }, root: { borderRadius: "4px" } }}
|
||||
>
|
||||
<Text>
|
||||
{" "}
|
||||
<Text>
|
||||
Thank you. Need to give{" "}
|
||||
<Link
|
||||
onClick={() => {
|
||||
setCalloutVisible(false);
|
||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
|
||||
}}
|
||||
>
|
||||
more feedback?
|
||||
</Link>
|
||||
</Text>
|
||||
</Text>
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
id={likeBtnId}
|
||||
iconProps={{
|
||||
imageProps: { src: likeImageLink },
|
||||
style: { minHeight: "18px" },
|
||||
}}
|
||||
onClick={() => {
|
||||
if (likeQuery) {
|
||||
setLikeQuery(false);
|
||||
setLikeImageLink(LikeRest);
|
||||
setCalloutVisible(false);
|
||||
} else {
|
||||
setLikeQuery(true);
|
||||
setDislikeQuery(false);
|
||||
setLikeImageLink(LikePressed);
|
||||
setDislikeImageLink(LikeRest);
|
||||
setCalloutVisible(true);
|
||||
}
|
||||
}}
|
||||
onMouseOver={() => setLikeImageLink(LikeHover)}
|
||||
onMouseLeave={() => setLikeImageLink(likeQuery ? LikePressed : LikeRest)}
|
||||
/>
|
||||
<IconButton
|
||||
id={dislikeBtnId}
|
||||
iconProps={{
|
||||
imageProps: { src: dislikeImageLink },
|
||||
style: { minHeight: "18px", transform: "rotate(180deg)" },
|
||||
}}
|
||||
onClick={() => {
|
||||
if (dislikeQuery) {
|
||||
setDislikeQuery(false);
|
||||
setDislikeImageLink(LikeRest);
|
||||
} else {
|
||||
setDislikeQuery(true);
|
||||
setLikeQuery(false);
|
||||
setDislikeImageLink(LikePressed);
|
||||
setLikeImageLink(LikeRest);
|
||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt);
|
||||
}
|
||||
}}
|
||||
onMouseOver={() => setDislikeImageLink(LikeHover)}
|
||||
onMouseLeave={() => setDislikeImageLink(dislikeQuery ? LikePressed : LikeRest)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,666 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Feedback buttons snapshot tests should click dislike and show popup 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeRest",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="likeBtn16"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikePressed",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
"transform": "rotate(180deg)",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="dislikeBtn17"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`Feedback buttons snapshot tests should click like and dismiss callout 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikePressed",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="likeBtn2"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeRest",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
"transform": "rotate(180deg)",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="dislikeBtn3"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`Feedback buttons snapshot tests should click like and show callout 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<Callout
|
||||
directionalHint={1}
|
||||
onDismiss={[Function]}
|
||||
role="dialog"
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": "4px",
|
||||
"padding": "5px 12px 5px 12px",
|
||||
}
|
||||
}
|
||||
styles={
|
||||
Object {
|
||||
"beakCurtain": Object {
|
||||
"borderRadius": "4px",
|
||||
},
|
||||
"root": Object {
|
||||
"borderRadius": "4px",
|
||||
},
|
||||
}
|
||||
}
|
||||
target="#likeBtn0"
|
||||
>
|
||||
<Text>
|
||||
|
||||
<Text>
|
||||
Thank you. Need to give
|
||||
|
||||
<StyledLinkBase
|
||||
onClick={[Function]}
|
||||
>
|
||||
more feedback?
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
</Text>
|
||||
</Callout>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikePressed",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="likeBtn0"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeRest",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
"transform": "rotate(180deg)",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="dislikeBtn1"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`Feedback buttons snapshot tests should click like and submit feedback 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikePressed",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="likeBtn4"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeRest",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
"transform": "rotate(180deg)",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="dislikeBtn5"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`Feedback buttons snapshot tests should dobule click on dislike 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeRest",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="likeBtn26"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeRest",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
"transform": "rotate(180deg)",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="dislikeBtn27"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`Feedback buttons snapshot tests should dobule click on like 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeRest",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="likeBtn14"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeRest",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
"transform": "rotate(180deg)",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="dislikeBtn15"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`Feedback buttons snapshot tests should hover over dislike 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeRest",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="likeBtn18"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeHover",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
"transform": "rotate(180deg)",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="dislikeBtn19"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`Feedback buttons snapshot tests should hover over dislike and click 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeRest",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="likeBtn24"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikePressed",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
"transform": "rotate(180deg)",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="dislikeBtn25"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`Feedback buttons snapshot tests should hover over like 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeHover",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="likeBtn6"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeRest",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
"transform": "rotate(180deg)",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="dislikeBtn7"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`Feedback buttons snapshot tests should hover over like and click 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<Callout
|
||||
directionalHint={1}
|
||||
onDismiss={[Function]}
|
||||
role="dialog"
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": "4px",
|
||||
"padding": "5px 12px 5px 12px",
|
||||
}
|
||||
}
|
||||
styles={
|
||||
Object {
|
||||
"beakCurtain": Object {
|
||||
"borderRadius": "4px",
|
||||
},
|
||||
"root": Object {
|
||||
"borderRadius": "4px",
|
||||
},
|
||||
}
|
||||
}
|
||||
target="#likeBtn12"
|
||||
>
|
||||
<Text>
|
||||
|
||||
<Text>
|
||||
Thank you. Need to give
|
||||
|
||||
<StyledLinkBase
|
||||
onClick={[Function]}
|
||||
>
|
||||
more feedback?
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
</Text>
|
||||
</Callout>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikePressed",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="likeBtn12"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeRest",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
"transform": "rotate(180deg)",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="dislikeBtn13"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`Feedback buttons snapshot tests should hover over pressed dislike and leave 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeRest",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="likeBtn22"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikePressed",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
"transform": "rotate(180deg)",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="dislikeBtn23"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`Feedback buttons snapshot tests should hover over pressed like and leave 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<Callout
|
||||
directionalHint={1}
|
||||
onDismiss={[Function]}
|
||||
role="dialog"
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": "4px",
|
||||
"padding": "5px 12px 5px 12px",
|
||||
}
|
||||
}
|
||||
styles={
|
||||
Object {
|
||||
"beakCurtain": Object {
|
||||
"borderRadius": "4px",
|
||||
},
|
||||
"root": Object {
|
||||
"borderRadius": "4px",
|
||||
},
|
||||
}
|
||||
}
|
||||
target="#likeBtn10"
|
||||
>
|
||||
<Text>
|
||||
|
||||
<Text>
|
||||
Thank you. Need to give
|
||||
|
||||
<StyledLinkBase
|
||||
onClick={[Function]}
|
||||
>
|
||||
more feedback?
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
</Text>
|
||||
</Callout>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikePressed",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="likeBtn10"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeRest",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
"transform": "rotate(180deg)",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="dislikeBtn11"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`Feedback buttons snapshot tests should hover over rest dislike and leave 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeRest",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="likeBtn20"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeRest",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
"transform": "rotate(180deg)",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="dislikeBtn21"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`Feedback buttons snapshot tests should hover over rest like and leave 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeRest",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="likeBtn8"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": "LikeRest",
|
||||
},
|
||||
"style": Object {
|
||||
"minHeight": "18px",
|
||||
"transform": "rotate(180deg)",
|
||||
},
|
||||
}
|
||||
}
|
||||
id="dislikeBtn9"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ActionButton } from "@fluentui/react";
|
||||
import { InsertButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Insert/InsertButton";
|
||||
import { shallow } from "enzyme";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React from "react";
|
||||
|
||||
describe("Insert button snapshot tests", () => {
|
||||
it("should click and update state", () => {
|
||||
const testQuery = "test query";
|
||||
useQueryCopilot.getState().setGeneratedQuery(testQuery);
|
||||
const wrapper = shallow(<InsertButton />);
|
||||
|
||||
const button = wrapper.find(ActionButton).first();
|
||||
button.simulate("click");
|
||||
|
||||
expect(useQueryCopilot.getState().query).toEqual(testQuery);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ActionButton } from "@fluentui/react";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React from "react";
|
||||
import CopilotInsert from "../../../../../../../../images/CopilotInsert.svg";
|
||||
|
||||
export const InsertButton: React.FC = (): JSX.Element => {
|
||||
return (
|
||||
<ActionButton
|
||||
iconProps={{ imageProps: { src: CopilotInsert } }}
|
||||
style={{ borderRadius: "4px", borderWidth: "1px", borderColor: "#D1D1D1", height: "24px", paddingBottom: "2px" }}
|
||||
onClick={() => useQueryCopilot.getState().setQuery(useQueryCopilot.getState().generatedQuery)}
|
||||
>
|
||||
Insert
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Insert button snapshot tests should click and update state 1`] = `
|
||||
<CustomizedActionButton
|
||||
iconProps={
|
||||
Object {
|
||||
"imageProps": Object {
|
||||
"src": Object {},
|
||||
},
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"borderColor": "#D1D1D1",
|
||||
"borderRadius": "4px",
|
||||
"borderWidth": "1px",
|
||||
"height": "24px",
|
||||
"paddingBottom": "2px",
|
||||
}
|
||||
}
|
||||
>
|
||||
Insert
|
||||
</CustomizedActionButton>
|
||||
`;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { MoreButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/More/MoreButton";
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
describe("More button snapshot tests", () => {
|
||||
it("should render", () => {
|
||||
const wrapper = shallow(<MoreButton />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { DirectionalHint, IContextualMenuProps, IconButton } from "@fluentui/react";
|
||||
import React from "react";
|
||||
import ExplainIcon from "../../../../../../../../images/CopilotExplain.svg";
|
||||
import OptimizeIcon from "../../../../../../../../images/CopilotOptimize.svg";
|
||||
import RegenerateIcon from "../../../../../../../../images/CopilotRegenerate.svg";
|
||||
import SimplifyIcon from "../../../../../../../../images/CopilotSimplify.svg";
|
||||
|
||||
export const MoreButton: React.FC = (): JSX.Element => {
|
||||
const menuProps: IContextualMenuProps = {
|
||||
items: [
|
||||
{
|
||||
key: "regenerate",
|
||||
text: "Regenerate code",
|
||||
iconProps: { imageProps: { src: RegenerateIcon } },
|
||||
},
|
||||
{
|
||||
key: "explain",
|
||||
text: "Explain code",
|
||||
iconProps: { imageProps: { src: ExplainIcon } },
|
||||
},
|
||||
{
|
||||
key: "optimize",
|
||||
text: "Optimize",
|
||||
iconProps: { imageProps: { src: OptimizeIcon } },
|
||||
},
|
||||
{
|
||||
key: "simplify",
|
||||
text: "Simplify",
|
||||
iconProps: { imageProps: { src: SimplifyIcon } },
|
||||
},
|
||||
],
|
||||
directionalHint: DirectionalHint.topRightEdge,
|
||||
calloutProps: {
|
||||
styles: { calloutMain: { borderRadius: "4px" }, root: { borderRadius: "4px" } },
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton iconProps={{ iconName: "More" }} menuProps={menuProps} menuIconProps={{ hidden: true }}></IconButton>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`More button snapshot tests should render 1`] = `
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "More",
|
||||
}
|
||||
}
|
||||
menuIconProps={
|
||||
Object {
|
||||
"hidden": true,
|
||||
}
|
||||
}
|
||||
menuProps={
|
||||
Object {
|
||||
"calloutProps": Object {
|
||||
"styles": Object {
|
||||
"calloutMain": Object {
|
||||
"borderRadius": "4px",
|
||||
},
|
||||
"root": Object {
|
||||
"borderRadius": "4px",
|
||||
},
|
||||
},
|
||||
},
|
||||
"directionalHint": 2,
|
||||
"items": Array [
|
||||
Object {
|
||||
"iconProps": Object {
|
||||
"imageProps": Object {
|
||||
"src": Object {},
|
||||
},
|
||||
},
|
||||
"key": "regenerate",
|
||||
"text": "Regenerate code",
|
||||
},
|
||||
Object {
|
||||
"iconProps": Object {
|
||||
"imageProps": Object {
|
||||
"src": Object {},
|
||||
},
|
||||
},
|
||||
"key": "explain",
|
||||
"text": "Explain code",
|
||||
},
|
||||
Object {
|
||||
"iconProps": Object {
|
||||
"imageProps": Object {
|
||||
"src": Object {},
|
||||
},
|
||||
},
|
||||
"key": "optimize",
|
||||
"text": "Optimize",
|
||||
},
|
||||
Object {
|
||||
"iconProps": Object {
|
||||
"imageProps": Object {
|
||||
"src": Object {},
|
||||
},
|
||||
},
|
||||
"key": "simplify",
|
||||
"text": "Simplify",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { OutputBubbleButtons } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/OutputBubbleButtons";
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
describe("Output Bubble Buttons snapshot tests", () => {
|
||||
it("should render", () => {
|
||||
const wrapper = shallow(<OutputBubbleButtons />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Stack } from "@fluentui/react";
|
||||
import { CopyButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Copy/CopyButton";
|
||||
import { FeedbackButtons } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Feedback/FeedbackButtons";
|
||||
import { InsertButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Insert/InsertButton";
|
||||
import { MoreButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/More/MoreButton";
|
||||
import React from "react";
|
||||
|
||||
export const OutputBubbleButtons: React.FC = (): JSX.Element => {
|
||||
return (
|
||||
<Stack horizontal>
|
||||
<Stack.Item style={{ paddingTop: "5px" }}>
|
||||
<InsertButton />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<CopyButton />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<FeedbackButtons />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<MoreButton />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Output Bubble Buttons snapshot tests should render 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<StackItem
|
||||
style={
|
||||
Object {
|
||||
"paddingTop": "5px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<InsertButton />
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<CopyButton />
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<FeedbackButtons />
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<MoreButton />
|
||||
</StackItem>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
import { OutputBubble } from "Explorer/QueryCopilot/V2/Bubbles/Output/OutputBubble";
|
||||
import { shallow } from "enzyme";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { withHooks } from "jest-react-hooks-shallow";
|
||||
import React from "react";
|
||||
|
||||
describe("Output Bubble snapshot tests", () => {
|
||||
it("should render and update height", () => {
|
||||
withHooks(() => {
|
||||
useQueryCopilot.getState().setGeneratedQuery("test query");
|
||||
useQueryCopilot.getState().setGeneratedQueryComments("test comments");
|
||||
const wrapper = shallow(<OutputBubble />);
|
||||
|
||||
const editor = wrapper.find(EditorReact).first();
|
||||
|
||||
expect(editor.props().monacoContainerStyles).not.toHaveProperty("height", undefined);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
60
src/Explorer/QueryCopilot/V2/Bubbles/Output/OutputBubble.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Stack, Text } from "@fluentui/react";
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
import { OutputBubbleButtons } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/OutputBubbleButtons";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React, { useState } from "react";
|
||||
|
||||
export const OutputBubble: React.FC = (): JSX.Element => {
|
||||
const [windowHeight, setWindowHeight] = useState<string>();
|
||||
|
||||
const calculateQueryWindowHeight = (): string => {
|
||||
const calculatedHeight = document.getElementById("outputBubble")?.clientHeight * (3 / 5);
|
||||
return `${calculatedHeight}px`;
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
setWindowHeight(calculateQueryWindowHeight());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
id="outputBubble"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "10px",
|
||||
margin: "10px",
|
||||
backgroundColor: "white",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
tokens={{ padding: 8, childrenGap: 8 }}
|
||||
>
|
||||
<Stack.Item style={{ alignSelf: "flex-start", paddingLeft: "2px" }}>
|
||||
{useQueryCopilot.getState()?.generatedQueryComments}
|
||||
</Stack.Item>
|
||||
<Stack.Item style={{ alignSelf: "stretch", flexGrow: 4 }}>
|
||||
<EditorReact
|
||||
language={"sql"}
|
||||
content={useQueryCopilot.getState()?.generatedQuery}
|
||||
isReadOnly={true}
|
||||
ariaLabel={"AI Response"}
|
||||
wordWrap="on"
|
||||
lineNumbers="on"
|
||||
lineNumbersMinChars={2}
|
||||
lineDecorationsWidth={0}
|
||||
minimap={{ enabled: false }}
|
||||
scrollBeyondLastLine={false}
|
||||
monacoContainerStyles={{ height: windowHeight, borderRadius: "4px" }}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item style={{ alignSelf: "flex-start" }}>
|
||||
<OutputBubbleButtons />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Text style={{ fontWeight: 400, fontSize: "10px", lineHeight: "14px" }}>
|
||||
AI-generated content may be incorrect
|
||||
</Text>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||