Compare commits
28 Commits
users/sind
...
users/v-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
863fdb4f7e | ||
|
|
260c99e15c | ||
|
|
c1c12019da | ||
|
|
4e5358185f | ||
|
|
b4bc93ac03 | ||
|
|
c61788198f | ||
|
|
379395567c | ||
|
|
c2d2ff3dee | ||
|
|
53fd738982 | ||
|
|
c07000a5c2 | ||
|
|
135a409f0c | ||
|
|
93b0101d4c | ||
|
|
0408a53121 | ||
|
|
12ed591634 | ||
|
|
76408e2f98 | ||
|
|
8c2ca4ab8e | ||
|
|
c9bd0b6c7f | ||
|
|
8ce7aaa728 | ||
|
|
98bf84d09d | ||
|
|
d4c831ff91 | ||
|
|
1eb566ab57 | ||
|
|
d155407b58 | ||
|
|
f8ff0626d9 | ||
|
|
c8e7e69aa5 | ||
|
|
b992742e20 | ||
|
|
0207f3cc04 | ||
|
|
6a8e87f45f | ||
|
|
f7370fd341 |
2
.github/workflows/ci.yml
vendored
@@ -85,6 +85,8 @@ jobs:
|
||||
path: .cache
|
||||
key: ${{ runner.os }}-build-cache
|
||||
- run: npm run pack:prod
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
- run: cp -r ./Contracts ./dist/contracts
|
||||
- run: cp -r ./configs ./dist/configs
|
||||
- uses: actions/upload-artifact@v2
|
||||
|
||||
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 |
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 |
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 |
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 |
20
images/Table.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.11108 12.8623H38.8889V33.9245C38.8889 34.2593 38.7559 34.5803 38.5192 34.8171C38.2825 35.0538 37.9614 35.1867 37.6266 35.1867H2.37331C2.03854 35.1867 1.71749 35.0538 1.48078 34.8171C1.24407 34.5803 1.11108 34.2593 1.11108 33.9245V12.8623Z" fill="url(#paint0_linear_307_12833)"/>
|
||||
<path d="M2.37997 4.81349H37.62C37.7857 4.81349 37.9499 4.84614 38.103 4.90958C38.2561 4.97301 38.3953 5.06598 38.5125 5.18319C38.6297 5.3004 38.7227 5.43955 38.7861 5.59269C38.8495 5.74582 38.8822 5.90996 38.8822 6.07572V12.8624H1.11108V6.07572C1.11108 5.9094 1.14395 5.74472 1.2078 5.59114C1.27165 5.43756 1.36522 5.29812 1.48313 5.18083C1.60105 5.06353 1.74098 4.97069 1.89489 4.90766C2.0488 4.84462 2.21366 4.81262 2.37997 4.81349V4.81349Z" fill="#0078D4"/>
|
||||
<path opacity="0.9" d="M12.6021 15.8242H5.74876C5.40144 15.8242 5.11987 16.1058 5.11987 16.4531V19.3064C5.11987 19.6538 5.40144 19.9353 5.74876 19.9353H12.6021C12.9494 19.9353 13.231 19.6538 13.231 19.3064V16.4531C13.231 16.1058 12.9494 15.8242 12.6021 15.8242Z" fill="white"/>
|
||||
<path opacity="0.9" d="M23.4821 15.7666H16.6288C16.2814 15.7666 15.9999 16.0482 15.9999 16.3955V19.2488C15.9999 19.5961 16.2814 19.8777 16.6288 19.8777H23.4821C23.8294 19.8777 24.111 19.5961 24.111 19.2488V16.3955C24.111 16.0482 23.8294 15.7666 23.4821 15.7666Z" fill="white"/>
|
||||
<path opacity="0.9" d="M34.3621 15.7666H27.5088C27.1614 15.7666 26.8799 16.0482 26.8799 16.3955V19.2488C26.8799 19.5961 27.1614 19.8777 27.5088 19.8777H34.3621C34.7094 19.8777 34.991 19.5961 34.991 19.2488V16.3955C34.991 16.0482 34.7094 15.7666 34.3621 15.7666Z" fill="white"/>
|
||||
<path opacity="0.9" d="M12.7221 21.7051H5.86876C5.52143 21.7051 5.23987 21.9866 5.23987 22.334V25.1873C5.23987 25.5346 5.52143 25.8162 5.86876 25.8162H12.7221C13.0694 25.8162 13.351 25.5346 13.351 25.1873V22.334C13.351 21.9866 13.0694 21.7051 12.7221 21.7051Z" fill="white"/>
|
||||
<path d="M23.6021 21.6465H16.7488C16.4014 21.6465 16.1199 21.928 16.1199 22.2754V25.1287C16.1199 25.476 16.4014 25.7576 16.7488 25.7576H23.6021C23.9494 25.7576 24.231 25.476 24.231 25.1287V22.2754C24.231 21.928 23.9494 21.6465 23.6021 21.6465Z" fill="#ECF4FD"/>
|
||||
<path d="M34.4821 21.6465H27.6288C27.2814 21.6465 26.9999 21.928 26.9999 22.2754V25.1287C26.9999 25.476 27.2814 25.7576 27.6288 25.7576H34.4821C34.8294 25.7576 35.111 25.476 35.111 25.1287V22.2754C35.111 21.928 34.8294 21.6465 34.4821 21.6465Z" fill="#ECF4FD"/>
|
||||
<path d="M12.7221 27.6426H5.86876C5.52143 27.6426 5.23987 27.9241 5.23987 28.2715V31.1248C5.23987 31.4721 5.52143 31.7537 5.86876 31.7537H12.7221C13.0694 31.7537 13.351 31.4721 13.351 31.1248V28.2715C13.351 27.9241 13.0694 27.6426 12.7221 27.6426Z" fill="#ECF4FD"/>
|
||||
<path d="M23.6021 27.585H16.7488C16.4014 27.585 16.1199 27.8665 16.1199 28.2139V31.0672C16.1199 31.4145 16.4014 31.6961 16.7488 31.6961H23.6021C23.9494 31.6961 24.231 31.4145 24.231 31.0672V28.2139C24.231 27.8665 23.9494 27.585 23.6021 27.585Z" fill="#ECF4FD"/>
|
||||
<path d="M34.4821 27.585H27.6288C27.2814 27.585 26.9999 27.8665 26.9999 28.2139V31.0672C26.9999 31.4145 27.2814 31.6961 27.6288 31.6961H34.4821C34.8294 31.6961 35.111 31.4145 35.111 31.0672V28.2139C35.111 27.8665 34.8294 27.585 34.4821 27.585Z" fill="#ECF4FD"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_307_12833" x1="20" y1="35.1867" x2="20" y2="12.8623" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0078D4"/>
|
||||
<stop offset="0.502" stop-color="#4093E6"/>
|
||||
<stop offset="0.775" stop-color="#5EA0EF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
BIN
images/vcoreMongoFirewallRule.png
Normal file
|
After Width: | Height: | Size: 13 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
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
@DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
|
||||
@SemiboldFont: "Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
|
||||
@GrayScale: "grayscale()";
|
||||
@NoColor: "brightness(0) saturate(100%)";
|
||||
|
||||
@xSmallFontSize: 4px;
|
||||
@smallFontSize: 8px;
|
||||
@@ -147,14 +148,41 @@
|
||||
// CommandBar
|
||||
@CommandBarButtonHeight: 40px;
|
||||
|
||||
/**********************************************************************************
|
||||
Portal Consts
|
||||
/**********************************************************************************/
|
||||
|
||||
@PortalAccentMediumHigh: #0058ad;
|
||||
@PortalAccentMedium: #004e87;
|
||||
@PortalAccentLight: #eef7ff;
|
||||
@PortalAccentAccentExtra: #ddf0ff;
|
||||
|
||||
/**********************************************************************************
|
||||
Fabric Consts
|
||||
/**********************************************************************************/
|
||||
|
||||
@FabricFont: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
|
||||
|
||||
@FabricBoxBorderRadius: 8px;
|
||||
@FabricBoxBorderShadow: 0 0 2px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14);
|
||||
@FabricBoxMargin: 4px 3px 4px 3px;
|
||||
|
||||
@FabricAccentMediumHigh: #0c695a;
|
||||
@FabricAccentMedium: #117865;
|
||||
@FabricAccentLight: #f5f5f5;
|
||||
@FabricAccentExtra: #ebebeb;
|
||||
|
||||
@FabricButtonBorderRadius: 4px;
|
||||
|
||||
|
||||
/**********************************************************************************
|
||||
Common Flex Property
|
||||
/**********************************************************************************/
|
||||
|
||||
.flex-display(@display: flex) {
|
||||
display: ~"-webkit-@{display}";
|
||||
display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox
|
||||
display: ~"-ms-@{display}"; // IE11
|
||||
display:~"-webkit-@{display}";
|
||||
display:~"-ms-@{display}box"; // IE10 uses -ms-flexbox
|
||||
display:~"-ms-@{display}"; // IE11
|
||||
display: @display;
|
||||
}
|
||||
|
||||
@@ -168,13 +196,15 @@
|
||||
High contrast mode active
|
||||
**************************************************************************************/
|
||||
|
||||
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
||||
@media all and (-ms-high-contrast: none),
|
||||
(-ms-high-contrast: active) {
|
||||
|
||||
.selectedRadio,
|
||||
.selectedRadio:hover,
|
||||
.selectedRadio:active,
|
||||
.selectedRadio.dirty,
|
||||
.tab [type="radio"]:checked ~ label,
|
||||
.tab [type="radio"]:checked ~ label:hover {
|
||||
.tab [type="radio"]:checked~label,
|
||||
.tab [type="radio"]:checked~label:hover {
|
||||
-ms-high-contrast-adjust: none;
|
||||
-webkit-text-fill-color: HighlightText;
|
||||
color: HighlightText;
|
||||
@@ -183,6 +213,7 @@
|
||||
}
|
||||
|
||||
.queryMetricsSummaryTuple {
|
||||
|
||||
th,
|
||||
td {
|
||||
&:nth-child(2) {
|
||||
@@ -302,4 +333,4 @@
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: @InfoPointerColor transparent;
|
||||
}
|
||||
}
|
||||
@@ -2646,6 +2646,11 @@ a:link {
|
||||
width: @ActiveTabWidth;
|
||||
}
|
||||
|
||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .tabNavText {
|
||||
font-weight: bolder;
|
||||
border-bottom: 2px solid rgba(0,120,212,1);
|
||||
}
|
||||
|
||||
.nav-tabs > li.active:focus > .tabNavContentContainer {
|
||||
.focus();
|
||||
}
|
||||
|
||||
211
less/documentDBFabric.less
Normal file
@@ -0,0 +1,211 @@
|
||||
@import "./Common/Constants";
|
||||
|
||||
html {
|
||||
font-family: @FabricFont;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: @FabricFont;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
a {
|
||||
color: @FabricAccentMedium;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:focus {
|
||||
color: @FabricAccentMediumHigh;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#divExplorer {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.resourceTreeAndTabs {
|
||||
border-radius: @FabricBoxBorderRadius;
|
||||
box-shadow: @FabricBoxBorderShadow;
|
||||
margin: @FabricBoxMargin;
|
||||
margin-top: 4px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.tabsManagerContainer {
|
||||
background-color: #fafafa
|
||||
}
|
||||
|
||||
.nav-tabs-margin {
|
||||
padding-top: 8px;
|
||||
background-color: #fafafa
|
||||
}
|
||||
|
||||
.commandBarContainer {
|
||||
background-color: #ffffff;
|
||||
border-bottom: none;
|
||||
border-radius: @FabricBoxBorderRadius;
|
||||
box-shadow: @FabricBoxBorderShadow;
|
||||
margin: @FabricBoxMargin;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.dividerContainer {
|
||||
padding: @SmallSpace 0px @SmallSpace 0px;
|
||||
.flex-display();
|
||||
|
||||
span {
|
||||
border-left: @ButtonBorderWidth solid @BaseMedium;
|
||||
margin: 0 10px 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.nav-tabs>li>.tabNavContentContainer>.tab_Content:hover {
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content,
|
||||
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content:hover {
|
||||
border-bottom: 2px solid @FabricAccentMedium;
|
||||
}
|
||||
|
||||
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content>.tabNavText {
|
||||
border-bottom: 0px none transparent;
|
||||
}
|
||||
|
||||
.tabNavContentContainer {
|
||||
padding: @SmallSpace 0px @SmallSpace 0px;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.tab_Content {
|
||||
border-right: 0px none transparent;
|
||||
margin: 0px @SmallSpace 0px @SmallSpace;
|
||||
width: calc(@TabsWidth - (@SmallSpace * 2));
|
||||
padding-bottom: @SmallSpace;
|
||||
|
||||
.statusIconContainer {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.tabIconSection {
|
||||
.cancelButton {
|
||||
padding: 0px 0px 0px @SmallSpace;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.resourceTree {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.accordion {
|
||||
.accordionItemContainer {
|
||||
.accordionItemHeader {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.treeComponent {
|
||||
.nodeItem {
|
||||
&:focus {
|
||||
outline: 2px @FabricAccentMedium;
|
||||
}
|
||||
|
||||
.treeNodeHeader {
|
||||
padding: 5px 5px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: @FabricAccentLight;
|
||||
|
||||
.treeMenuEllipsis {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.showingMenu {
|
||||
background-color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
&>.treeNodeHeader {
|
||||
background-color: @FabricAccentExtra;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.dataExplorerErrorConsoleContainer {
|
||||
border-radius: @FabricBoxBorderRadius;
|
||||
box-shadow: @FabricBoxBorderShadow;
|
||||
margin: @FabricBoxMargin;
|
||||
width: auto;
|
||||
align-self: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.filterbtnstyle {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
border: solid 1px #d1d1d1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filterbtnstyle:hover {
|
||||
background: @FabricAccentLight;
|
||||
color: #000;
|
||||
border: solid 1px #d1d1d1;
|
||||
}
|
||||
|
||||
.filterbtnstyle:active {
|
||||
background: @FabricAccentLight;
|
||||
color: #000;
|
||||
border: solid 1px #d1d1d1;
|
||||
}
|
||||
|
||||
.filterbtnstyle:focus {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
border: solid 1px #d1d1d1;
|
||||
}
|
||||
|
||||
|
||||
.gridRowSelected .tabdocumentsGridElement:hover {
|
||||
background-color: @FabricAccentLight !important;
|
||||
}
|
||||
|
||||
|
||||
.refreshcol {
|
||||
filter: brightness(0) saturate(100%);
|
||||
}
|
||||
|
||||
.refreshcol1 {
|
||||
filter: brightness(0) saturate(100%);
|
||||
}
|
||||
|
||||
.fileImportImg img {
|
||||
filter: brightness(0) saturate(100%);
|
||||
}
|
||||
33010
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",
|
||||
|
||||
@@ -365,9 +365,6 @@ export const EmulatorMasterKey =
|
||||
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
|
||||
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
|
||||
|
||||
// A variable @MyVariable defined in Constants.less is accessible as StyleConstants.MyVariable
|
||||
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
|
||||
|
||||
export class Notebook {
|
||||
public static readonly defaultBasePath = "./notebooks";
|
||||
public static readonly heartbeatDelayMs = 60000;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { ResourceTree } from "Explorer/Tree/ResourceTree";
|
||||
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||
import refreshImg from "../../images/refresh-cosmos.svg";
|
||||
import { AuthType } from "../AuthType";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
|
||||
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
|
||||
import { ResourceTree2 } from "../Explorer/Tree2/ResourceTree";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getApiShortDisplayName } from "../Utils/APITypeUtils";
|
||||
import { Platform, configContext } from "./../ConfigContext";
|
||||
import { NormalizedEventKey } from "./Constants";
|
||||
|
||||
export interface ResourceTreeContainerProps {
|
||||
@@ -76,6 +78,8 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
|
||||
<ResourceTokenTree />
|
||||
) : userContext.features.enableKoResourceTree ? (
|
||||
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
|
||||
) : configContext.platform === Platform.Fabric ? (
|
||||
<ResourceTree2 container={container} />
|
||||
) : (
|
||||
<ResourceTree container={container} />
|
||||
)}
|
||||
|
||||
18
src/Common/StyleConstants.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Platform, configContext } from "../ConfigContext";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
|
||||
|
||||
export function updateStyles(): void {
|
||||
if (configContext.platform === Platform.Fabric) {
|
||||
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
|
||||
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
|
||||
StyleConstants.AccentLight = StyleConstants.FabricAccentLight;
|
||||
StyleConstants.AccentAccentExtra = StyleConstants.FabricAccentMediumHigh;
|
||||
} else {
|
||||
StyleConstants.AccentMediumHigh = StyleConstants.PortalAccentMediumHigh;
|
||||
StyleConstants.AccentMedium = StyleConstants.PortalAccentMedium;
|
||||
StyleConstants.AccentLight = StyleConstants.PortalAccentLight;
|
||||
StyleConstants.AccentAccentExtra = StyleConstants.PortalAccentMediumHigh;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ export enum Platform {
|
||||
Portal = "Portal",
|
||||
Hosted = "Hosted",
|
||||
Emulator = "Emulator",
|
||||
Fabric = "Fabric",
|
||||
}
|
||||
|
||||
export interface ConfigContext {
|
||||
@@ -187,6 +188,7 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
|
||||
console.error(`Invalid platform query parameter: ${platform}`);
|
||||
break;
|
||||
case Platform.Portal:
|
||||
case Platform.Fabric:
|
||||
case Platform.Hosted:
|
||||
case Platform.Emulator:
|
||||
updateConfigContext({ platform });
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { ConnectionStatusType, ContainerStatusType } from "../Common/Constants";
|
||||
|
||||
export interface DatabaseAccount {
|
||||
export interface ArmEntity {
|
||||
id: string;
|
||||
name: string;
|
||||
location: string;
|
||||
type: string;
|
||||
kind: string;
|
||||
}
|
||||
|
||||
export interface DatabaseAccount extends ArmEntity {
|
||||
properties: DatabaseAccountExtendedProperties;
|
||||
systemData?: DatabaseAccountSystemData;
|
||||
}
|
||||
@@ -35,6 +38,7 @@ export interface DatabaseAccountExtendedProperties {
|
||||
locations?: DatabaseAccountResponseLocation[];
|
||||
postgresqlEndpoint?: string;
|
||||
publicNetworkAccess?: string;
|
||||
vcoreMongoEndpoint?: string;
|
||||
}
|
||||
|
||||
export interface DatabaseAccountResponseLocation {
|
||||
@@ -575,7 +579,7 @@ export interface ContainerConnectionInfo {
|
||||
//need to add ram and rom info
|
||||
}
|
||||
|
||||
export interface PostgresFirewallRule {
|
||||
export interface FirewallRule {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
|
||||
@@ -39,6 +39,8 @@ export enum MessageTypes {
|
||||
OpenPostgresNetworkingBlade,
|
||||
OpenCosmosDBNetworkingBlade,
|
||||
DisplayNPSSurvey,
|
||||
OpenVCoreMongoNetworkingBlade,
|
||||
OpenVCoreMongoConnectionStringsBlade,
|
||||
}
|
||||
|
||||
export { Versions, ActionContracts, Diagnostics };
|
||||
export { ActionContracts, Diagnostics, Versions };
|
||||
|
||||
@@ -372,6 +372,7 @@ export enum TerminalKind {
|
||||
Mongo = 1,
|
||||
Cassandra = 2,
|
||||
Postgres = 3,
|
||||
VCoreMongo = 4,
|
||||
}
|
||||
|
||||
export interface DataExplorerInputsFrame {
|
||||
@@ -397,6 +398,7 @@ export interface DataExplorerInputsFrame {
|
||||
defaultCollectionThroughput?: CollectionCreationDefaults;
|
||||
isPostgresAccount?: boolean;
|
||||
isReplica?: boolean;
|
||||
isVCoreMongoAccount?: boolean;
|
||||
clientIpAddress?: string;
|
||||
// TODO: Update this param in the OSS extension to remove isFreeTier, isMarlinServerGroup, and make nodes a flat array instead of an nested array
|
||||
connectionStringParams?: any;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import dayjs from "dayjs";
|
||||
import * as Plotly from "plotly.js-cartesian-dist-min";
|
||||
import { StyleConstants } from "../../Common/Constants";
|
||||
import { sendCachedDataMessage, sendReadyMessage } from "../../Common/MessageHandler";
|
||||
import { StyleConstants } from "../../Common/StyleConstants";
|
||||
import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
||||
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
|
||||
import "./Heatmap.less";
|
||||
|
||||
@@ -18,6 +18,7 @@ import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { Platform, configContext } from "./../ConfigContext";
|
||||
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
|
||||
import Explorer from "./Explorer";
|
||||
import { useNotebook } from "./Notebook/useNotebook";
|
||||
@@ -99,11 +100,13 @@ export const createCollectionContextMenuButton = (
|
||||
});
|
||||
}
|
||||
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
|
||||
if (
|
||||
configContext.platform !== Platform.Fabric &&
|
||||
(userContext.apiType === "SQL" || userContext.apiType === "Gremlin")
|
||||
) {
|
||||
items.push({
|
||||
iconSrc: AddStoredProcedureIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
|
||||
},
|
||||
label: "New Stored Procedure",
|
||||
@@ -112,7 +115,6 @@ export const createCollectionContextMenuButton = (
|
||||
items.push({
|
||||
iconSrc: AddUdfIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
|
||||
},
|
||||
label: "New UDF",
|
||||
@@ -121,7 +123,6 @@ export const createCollectionContextMenuButton = (
|
||||
items.push({
|
||||
iconSrc: AddTriggerIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined);
|
||||
},
|
||||
label: "New Trigger",
|
||||
@@ -130,13 +131,15 @@ export const createCollectionContextMenuButton = (
|
||||
|
||||
items.push({
|
||||
iconSrc: DeleteCollectionIcon,
|
||||
onClick: () =>
|
||||
onClick: () => {
|
||||
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getCollectionName(),
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />
|
||||
),
|
||||
);
|
||||
},
|
||||
label: `Delete ${getCollectionName()}`,
|
||||
styleClass: "deleteCollectionMenuItem",
|
||||
});
|
||||
|
||||
@@ -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> {
|
||||
@@ -54,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>
|
||||
);
|
||||
}
|
||||
@@ -84,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 = "";
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface NotebookTerminalComponentProps {
|
||||
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||
databaseAccount: DataModels.DatabaseAccount;
|
||||
tabId: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
|
||||
@@ -50,7 +51,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
||||
return;
|
||||
}
|
||||
|
||||
const props: TerminalProps = {
|
||||
let props: TerminalProps = {
|
||||
terminalEndpoint: this.tryGetTerminalEndpoint(),
|
||||
notebookServerEndpoint: this.props.notebookServerInfo?.notebookServerEndpoint,
|
||||
authToken: this.props.notebookServerInfo?.authToken,
|
||||
@@ -61,6 +62,13 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
||||
tabId: this.props.tabId,
|
||||
};
|
||||
|
||||
if (this.props.username) {
|
||||
props = {
|
||||
...props,
|
||||
username: this.props.username,
|
||||
};
|
||||
}
|
||||
|
||||
postRobot.send(this.terminalWindow, "props", props, {
|
||||
domain: window.location.origin,
|
||||
});
|
||||
@@ -78,6 +86,8 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
||||
terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint;
|
||||
} else if (StringUtils.endsWith(notebookServerEndpoint, "postgresql")) {
|
||||
return this.props.databaseAccount?.properties.postgresqlEndpoint;
|
||||
} else if (StringUtils.endsWith(notebookServerEndpoint, "mongovcore")) {
|
||||
return this.props.databaseAccount?.properties.vcoreMongoEndpoint;
|
||||
}
|
||||
|
||||
if (terminalEndpoint) {
|
||||
|
||||
@@ -23,9 +23,9 @@ import * as React from "react";
|
||||
import * as _ from "underscore";
|
||||
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
import { QueriesClient } from "../../../Common/QueriesClient";
|
||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
@@ -5,18 +5,18 @@ import DiscardIcon from "../../../../images/discard.svg";
|
||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||
import { AuthType } from "../../../AuthType";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
|
||||
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
|
||||
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
||||
@@ -37,15 +37,15 @@ import {
|
||||
AddMongoIndexProps,
|
||||
ChangeFeedPolicyState,
|
||||
GeospatialConfigType,
|
||||
MongoIndexTypes,
|
||||
SettingsV2TabTypes,
|
||||
TtlType,
|
||||
getMongoNotification,
|
||||
getTabTitle,
|
||||
hasDatabaseSharedThroughput,
|
||||
isDirty,
|
||||
MongoIndexTypes,
|
||||
parseConflictResolutionMode,
|
||||
parseConflictResolutionProcedure,
|
||||
SettingsV2TabTypes,
|
||||
TtlType,
|
||||
} from "./SettingsUtils";
|
||||
|
||||
interface SettingsV2TabInfo {
|
||||
@@ -78,6 +78,8 @@ export interface SettingsComponentState {
|
||||
timeToLiveBaseline: TtlType;
|
||||
timeToLiveSeconds: number;
|
||||
timeToLiveSecondsBaseline: number;
|
||||
displayedTtlSeconds: string;
|
||||
displayedTtlSecondsBaseline: string;
|
||||
geospatialConfigType: GeospatialConfigType;
|
||||
geospatialConfigTypeBaseline: GeospatialConfigType;
|
||||
analyticalStorageTtlSelection: TtlType;
|
||||
@@ -164,6 +166,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
timeToLiveBaseline: undefined,
|
||||
timeToLiveSeconds: undefined,
|
||||
timeToLiveSecondsBaseline: undefined,
|
||||
displayedTtlSeconds: undefined,
|
||||
displayedTtlSecondsBaseline: undefined,
|
||||
geospatialConfigType: undefined,
|
||||
geospatialConfigTypeBaseline: undefined,
|
||||
analyticalStorageTtlSelection: undefined,
|
||||
@@ -369,6 +373,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
throughput: this.state.throughputBaseline,
|
||||
timeToLive: this.state.timeToLiveBaseline,
|
||||
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
|
||||
displayedTtlSeconds: this.state.displayedTtlSecondsBaseline,
|
||||
geospatialConfigType: this.state.geospatialConfigTypeBaseline,
|
||||
indexingPolicyContent: this.state.indexingPolicyContentBaseline,
|
||||
indexesToAdd: [],
|
||||
@@ -479,6 +484,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private onTimeToLiveSecondsChange = (newTimeToLiveSeconds: number): void =>
|
||||
this.setState({ timeToLiveSeconds: newTimeToLiveSeconds });
|
||||
|
||||
private onDisplayedTtlChange = (newDisplayedTtlSeconds: string): void =>
|
||||
this.setState({ displayedTtlSeconds: newDisplayedTtlSeconds });
|
||||
|
||||
private onGeoSpatialConfigTypeChange = (newGeoSpatialConfigType: GeospatialConfigType): void =>
|
||||
this.setState({ geospatialConfigType: newGeoSpatialConfigType });
|
||||
|
||||
@@ -608,6 +616,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
break;
|
||||
}
|
||||
|
||||
const displayedTtlSeconds: string = timeToLive === TtlType.On ? timeToLiveSeconds.toString() : "";
|
||||
|
||||
let analyticalStorageTtlSelection: TtlType;
|
||||
let analyticalStorageTtlSeconds: number;
|
||||
if (this.isAnalyticalStorageEnabled) {
|
||||
@@ -645,6 +655,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
timeToLiveBaseline: timeToLive,
|
||||
timeToLiveSeconds: timeToLiveSeconds,
|
||||
timeToLiveSecondsBaseline: timeToLiveSeconds,
|
||||
displayedTtlSeconds: displayedTtlSeconds,
|
||||
displayedTtlSecondsBaseline: displayedTtlSeconds,
|
||||
analyticalStorageTtlSelection: analyticalStorageTtlSelection,
|
||||
analyticalStorageTtlSelectionBaseline: analyticalStorageTtlSelection,
|
||||
analyticalStorageTtlSeconds: analyticalStorageTtlSeconds,
|
||||
@@ -986,6 +998,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
timeToLiveSeconds: this.state.timeToLiveSeconds,
|
||||
timeToLiveSecondsBaseline: this.state.timeToLiveSecondsBaseline,
|
||||
onTimeToLiveSecondsChange: this.onTimeToLiveSecondsChange,
|
||||
displayedTtlSeconds: this.state.displayedTtlSeconds,
|
||||
onDisplayedTtlSecondsChange: this.onDisplayedTtlChange,
|
||||
geospatialConfigType: this.state.geospatialConfigType,
|
||||
geospatialConfigTypeBaseline: this.state.geospatialConfigTypeBaseline,
|
||||
onGeoSpatialConfigTypeChange: this.onGeoSpatialConfigTypeChange,
|
||||
|
||||
@@ -23,7 +23,8 @@ import {
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { StyleConstants, Urls } from "../../../Common/Constants";
|
||||
import { Urls } from "../../../Common/Constants";
|
||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||
import { hoursInAMonth } from "../../../Shared/Constants";
|
||||
import {
|
||||
computeRUUsagePriceHourly,
|
||||
|
||||
@@ -20,6 +20,10 @@ describe("SubSettingsComponent", () => {
|
||||
onTimeToLiveSecondsChange: () => {
|
||||
return;
|
||||
},
|
||||
displayedTtlSeconds: "1000",
|
||||
onDisplayedTtlSecondsChange: () => {
|
||||
return;
|
||||
},
|
||||
|
||||
geospatialConfigType: GeospatialConfigType.Geography,
|
||||
geospatialConfigTypeBaseline: GeospatialConfigType.Geography,
|
||||
|
||||
@@ -15,13 +15,13 @@ import {
|
||||
import {
|
||||
ChangeFeedPolicyState,
|
||||
GeospatialConfigType,
|
||||
getSanitizedInputValue,
|
||||
IsComponentDirtyResult,
|
||||
isDirty,
|
||||
TtlOff,
|
||||
TtlOn,
|
||||
TtlOnNoDefault,
|
||||
TtlType,
|
||||
getSanitizedInputValue,
|
||||
isDirty,
|
||||
} from "../SettingsUtils";
|
||||
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
|
||||
|
||||
@@ -34,6 +34,8 @@ export interface SubSettingsComponentProps {
|
||||
timeToLiveSeconds: number;
|
||||
timeToLiveSecondsBaseline: number;
|
||||
onTimeToLiveSecondsChange: (newTimeToLiveSeconds: number) => void;
|
||||
displayedTtlSeconds: string;
|
||||
onDisplayedTtlSecondsChange: (newDisplayedTtlSeconds: string) => void;
|
||||
|
||||
geospatialConfigType: GeospatialConfigType;
|
||||
geospatialConfigTypeBaseline: GeospatialConfigType;
|
||||
@@ -73,7 +75,14 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
this.onComponentUpdate();
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
componentDidUpdate(prevProps: SubSettingsComponentProps): void {
|
||||
if (
|
||||
(prevProps.timeToLive === TtlType.Off || prevProps.timeToLive === TtlType.OnNoDefault) &&
|
||||
this.props.timeToLive === TtlType.On &&
|
||||
this.props.timeToLiveBaseline !== TtlType.On
|
||||
) {
|
||||
this.props.onDisplayedTtlSecondsChange("");
|
||||
}
|
||||
this.onComponentUpdate();
|
||||
}
|
||||
|
||||
@@ -93,7 +102,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
public IsComponentDirty = (): IsComponentDirtyResult => {
|
||||
if (
|
||||
(this.props.timeToLive === TtlType.On && !this.props.timeToLiveSeconds) ||
|
||||
(this.props.analyticalStorageTtlSelection === TtlType.On && !this.props.analyticalStorageTtlSeconds)
|
||||
(this.props.analyticalStorageTtlSelection === TtlType.On && !this.props.analyticalStorageTtlSeconds) ||
|
||||
(this.props.timeToLive === TtlType.On && this.props.displayedTtlSeconds === "")
|
||||
) {
|
||||
return { isSaveable: false, isDiscardable: true };
|
||||
} else if (
|
||||
@@ -138,6 +148,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
newValue?: string
|
||||
): void => {
|
||||
const newTimeToLiveSeconds = getSanitizedInputValue(newValue, Int32.Max);
|
||||
this.props.onDisplayedTtlSecondsChange(newTimeToLiveSeconds.toString());
|
||||
this.props.onTimeToLiveSecondsChange(newTimeToLiveSeconds);
|
||||
};
|
||||
|
||||
@@ -204,7 +215,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
required
|
||||
min={1}
|
||||
max={Int32.Max}
|
||||
value={this.props.timeToLiveSeconds?.toString()}
|
||||
value={this.props.displayedTtlSeconds}
|
||||
onChange={this.onTimeToLiveSecondsChange}
|
||||
suffix="second(s)"
|
||||
/>
|
||||
|
||||
@@ -150,12 +150,14 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"usageSizeInKB": [Function],
|
||||
}
|
||||
}
|
||||
displayedTtlSeconds="5"
|
||||
geospatialConfigType="Geometry"
|
||||
geospatialConfigTypeBaseline="Geometry"
|
||||
isAnalyticalStorageEnabled={false}
|
||||
onAnalyticalStorageTtlSecondsChange={[Function]}
|
||||
onAnalyticalStorageTtlSelectionChange={[Function]}
|
||||
onChangeFeedPolicyChange={[Function]}
|
||||
onDisplayedTtlSecondsChange={[Function]}
|
||||
onGeoSpatialConfigTypeChange={[Function]}
|
||||
onSubSettingsDiscardableChange={[Function]}
|
||||
onSubSettingsSaveableChange={[Function]}
|
||||
|
||||
@@ -18,6 +18,7 @@ import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squa
|
||||
import TriangleDownIcon from "../../../../images/Triangle-down.svg";
|
||||
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
@@ -237,7 +238,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
private renderContextMenuButton(node: TreeNode): JSX.Element {
|
||||
const menuItemLabel = "More";
|
||||
const buttonStyles: Partial<IButtonStyles> = {
|
||||
rootFocused: { outline: `1px dashed ${Constants.StyleConstants.FocusColor}` },
|
||||
rootFocused: { outline: `1px dashed ${StyleConstants.FocusColor}` },
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -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
|
||||
|
||||
142
src/Explorer/Controls/TreeComponent2/TreeNode2Component.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
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";
|
||||
|
||||
export interface TreeNode2MenuItem {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
iconSrc?: string;
|
||||
isDisabled?: boolean;
|
||||
styleClass?: string;
|
||||
}
|
||||
|
||||
export interface TreeNode2 {
|
||||
label: string;
|
||||
id?: string;
|
||||
children?: TreeNode2[];
|
||||
contextMenu?: TreeNode2MenuItem[];
|
||||
iconSrc?: string;
|
||||
// isExpanded?: boolean;
|
||||
className?: string;
|
||||
isAlphaSorted?: boolean;
|
||||
// data?: any; // Piece of data corresponding to this node
|
||||
timestamp?: number;
|
||||
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
|
||||
isLoading?: boolean;
|
||||
isScrollable?: boolean;
|
||||
isSelected?: () => boolean;
|
||||
onClick?: () => void; // Only if a leaf, other click will expand/collapse
|
||||
onExpanded?: () => void;
|
||||
onCollapsed?: () => void;
|
||||
onContextMenuOpen?: () => void;
|
||||
}
|
||||
|
||||
export interface TreeNode2ComponentProps {
|
||||
node: TreeNode2;
|
||||
className?: string;
|
||||
treeNodeId: string;
|
||||
globalOpenIds: string[];
|
||||
}
|
||||
|
||||
const getTreeIcon = (iconSrc: string): JSX.Element => <img src={iconSrc} alt="" style={{ width: 20, height: 20 }} />;
|
||||
|
||||
export const TreeNode2Component: React.FC<TreeNode2ComponentProps> = ({
|
||||
node,
|
||||
treeNodeId,
|
||||
globalOpenIds,
|
||||
}: TreeNode2ComponentProps): JSX.Element => {
|
||||
// const defaultOpenItems = node.isExpanded ? children?.map((child: TreeNode2) => child.label) : undefined;
|
||||
const [isExpanded, setIsExpanded] = React.useState<boolean>(false);
|
||||
|
||||
// Compute whether node is expanded
|
||||
React.useEffect(() => {
|
||||
const isNowExpanded = globalOpenIds && globalOpenIds.includes(treeNodeId);
|
||||
if (!isExpanded && isNowExpanded) {
|
||||
// Catch the transition non-expanded to expanded
|
||||
node.onExpanded?.();
|
||||
}
|
||||
setIsExpanded(isNowExpanded);
|
||||
}, [globalOpenIds, treeNodeId, node, isExpanded]);
|
||||
|
||||
const getSortedChildren = (treeNode: TreeNode2): TreeNode2[] => {
|
||||
if (!treeNode || !treeNode.children) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const compareFct = (a: TreeNode2, b: TreeNode2) => a.label.localeCompare(b.label);
|
||||
|
||||
let unsortedChildren;
|
||||
if (treeNode.isLeavesParentsSeparate) {
|
||||
// Separate parents and leave
|
||||
const parents: TreeNode2[] = treeNode.children.filter((node) => node.children);
|
||||
const leaves: TreeNode2[] = treeNode.children.filter((node) => !node.children);
|
||||
|
||||
if (treeNode.isAlphaSorted) {
|
||||
parents.sort(compareFct);
|
||||
leaves.sort(compareFct);
|
||||
}
|
||||
|
||||
unsortedChildren = parents.concat(leaves);
|
||||
} else {
|
||||
unsortedChildren = treeNode.isAlphaSorted ? treeNode.children.sort(compareFct) : treeNode.children;
|
||||
}
|
||||
|
||||
return unsortedChildren;
|
||||
};
|
||||
|
||||
return (
|
||||
<TreeItem value={treeNodeId} itemType={node.children !== undefined ? "branch" : "leaf"} style={{ height: "100%" }}>
|
||||
<TreeItemLayout
|
||||
className={node.className}
|
||||
actions={
|
||||
node.contextMenu && (
|
||||
<Menu>
|
||||
<MenuTrigger disableButtonEnhancement>
|
||||
<Button aria-label="More options" appearance="subtle" icon={<MoreHorizontal20Regular />} />
|
||||
</MenuTrigger>
|
||||
<MenuPopover>
|
||||
<MenuList>
|
||||
{node.contextMenu.map((menuItem) => (
|
||||
<MenuItem disabled={menuItem.isDisabled} key={menuItem.label} onClick={menuItem.onClick}>
|
||||
{menuItem.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</MenuPopover>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
expandIcon={node.isLoading ? <Spinner size="extra-tiny" /> : undefined}
|
||||
iconBefore={node.iconSrc && getTreeIcon(node.iconSrc)}
|
||||
>
|
||||
<span onClick={() => node.onClick?.()}>{node.label}</span>
|
||||
</TreeItemLayout>
|
||||
{!node.isLoading && node.children?.length > 0 && (
|
||||
<Tree
|
||||
// defaultOpenItems={defaultOpenItems}
|
||||
style={{ overflow: node.isScrollable ? "auto" : undefined }}
|
||||
>
|
||||
{getSortedChildren(node).map((childNode: TreeNode2) => (
|
||||
<TreeNode2Component
|
||||
key={childNode.label}
|
||||
node={childNode}
|
||||
treeNodeId={`${treeNodeId}/${childNode.label}`}
|
||||
globalOpenIds={globalOpenIds}
|
||||
/>
|
||||
))}
|
||||
</Tree>
|
||||
)}
|
||||
</TreeItem>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Link } from "@fluentui/react/lib/Link";
|
||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { Platform } from "ConfigContext";
|
||||
import { Platform, configContext } 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";
|
||||
@@ -276,6 +277,20 @@ export default class Explorer {
|
||||
userContext.databaseAccount?.systemData?.createdAt || "",
|
||||
NINETY_DAYS_IN_MS
|
||||
);
|
||||
const lastSubmitted: string = localStorage.getItem("lastSubmitted");
|
||||
|
||||
if (lastSubmitted !== null) {
|
||||
let lastSubmittedDate: number = parseInt(lastSubmitted);
|
||||
if (isNaN(lastSubmittedDate)) {
|
||||
lastSubmittedDate = 0;
|
||||
}
|
||||
|
||||
const nowMs: number = Date.now();
|
||||
const millisecsSinceLastSubmitted = nowMs - lastSubmittedDate;
|
||||
if (millisecsSinceLastSubmitted < NINETY_DAYS_IN_MS) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Try Cosmos DB subscription - survey shown to random 25% of users at day 1 in Data Explorer.
|
||||
if (userContext.isTryCosmosDBSubscription) {
|
||||
@@ -284,17 +299,20 @@ export default class Explorer {
|
||||
this.getRandomInt(100) < 25
|
||||
) {
|
||||
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||
localStorage.setItem("lastSubmitted", Date.now().toString());
|
||||
}
|
||||
} 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 });
|
||||
localStorage.setItem("lastSubmitted", Date.now().toString());
|
||||
}
|
||||
} 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 });
|
||||
localStorage.setItem("lastSubmitted", Date.now().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -386,8 +404,14 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public async allocateContainer(poolId: PoolIdType): Promise<void> {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
const isAllocating = useNotebook.getState().isAllocating;
|
||||
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 ||
|
||||
@@ -395,23 +419,28 @@ export default class Explorer {
|
||||
) {
|
||||
const provisionData: IProvisionData = {
|
||||
cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint,
|
||||
poolId: poolId === PoolIdType.DefaultPoolId ? undefined : poolId,
|
||||
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,
|
||||
});
|
||||
@@ -423,7 +452,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 {
|
||||
@@ -436,7 +467,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;
|
||||
@@ -445,6 +478,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
private async setNotebookInfo(
|
||||
shouldUseNotebookStates: boolean,
|
||||
connectionInfo: IResponse<IPhoenixServiceInfo>,
|
||||
connectionStatus: DataModels.ContainerConnectionInfo
|
||||
) {
|
||||
@@ -452,21 +486,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 {
|
||||
@@ -540,7 +579,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");
|
||||
@@ -1105,6 +1144,10 @@ export default class Explorer {
|
||||
title = "PSQL Shell";
|
||||
break;
|
||||
|
||||
case ViewModels.TerminalKind.VCoreMongo:
|
||||
title = "VCoreMongo Shell";
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Terminal kind: ${kind} not supported");
|
||||
}
|
||||
@@ -1291,7 +1334,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public async refreshExplorer(): Promise<void> {
|
||||
if (userContext.apiType !== "Postgres") {
|
||||
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
||||
userContext.authType === AuthType.ResourceToken
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: this.refreshAllDatabases();
|
||||
@@ -1300,9 +1343,10 @@ export default class Explorer {
|
||||
|
||||
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
||||
const isNotebookEnabled =
|
||||
userContext.features.notebooksDownBanner ||
|
||||
useNotebook.getState().isPhoenixNotebooks ||
|
||||
useNotebook.getState().isPhoenixFeatures;
|
||||
configContext.platform !== Platform.Fabric &&
|
||||
(userContext.features.notebooksDownBanner ||
|
||||
useNotebook.getState().isPhoenixNotebooks ||
|
||||
useNotebook.getState().isPhoenixFeatures);
|
||||
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
|
||||
useNotebook
|
||||
.getState()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -8,7 +8,9 @@ import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { userContext } from "UserContext";
|
||||
import * as React from "react";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { ConnectionStatusType, PoolIdType, StyleConstants } from "../../../Common/Constants";
|
||||
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
|
||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||
import { Platform, configContext } from "../../../ConfigContext";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
@@ -34,8 +36,11 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
const buttons = useCommandBar((state) => state.contextButtons);
|
||||
const backgroundColor = StyleConstants.BaseLight;
|
||||
|
||||
if (userContext.apiType === "Postgres") {
|
||||
const buttons = CommandBarComponentButtonFactory.createPostgreButtons(container);
|
||||
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||
const buttons =
|
||||
userContext.apiType === "Postgres"
|
||||
? CommandBarComponentButtonFactory.createPostgreButtons(container)
|
||||
: CommandBarComponentButtonFactory.createVCoreMongoButtons(container);
|
||||
return (
|
||||
<div className="commandBarContainer">
|
||||
<FluentCommandBar
|
||||
@@ -81,15 +86,27 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
const rootStyle =
|
||||
configContext.platform === Platform.Fabric
|
||||
? {
|
||||
root: {
|
||||
backgroundColor: "transparent",
|
||||
padding: "0px 14px 0px 14px",
|
||||
},
|
||||
}
|
||||
: {
|
||||
root: {
|
||||
backgroundColor: backgroundColor,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="commandBarContainer">
|
||||
<FluentCommandBar
|
||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
||||
farItems={uiFabricControlButtons}
|
||||
styles={{
|
||||
root: { backgroundColor: backgroundColor },
|
||||
}}
|
||||
styles={rootStyle}
|
||||
overflowButtonProps={{ ariaLabel: "More commands" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -142,8 +142,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Open Mongo Shell button", () => {
|
||||
const openMongoShellBtnLabel = "Open Mongo Shell";
|
||||
describe("Open Mongo shell button", () => {
|
||||
const openMongoShellBtnLabel = "Open Mongo shell";
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
|
||||
beforeAll(() => {
|
||||
@@ -247,8 +247,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Open Cassandra Shell button", () => {
|
||||
const openCassandraShellBtnLabel = "Open Cassandra Shell";
|
||||
describe("Open Cassandra shell button", () => {
|
||||
const openCassandraShellBtnLabel = "Open Cassandra shell";
|
||||
const selectedNodeState = useSelectedNode.getState();
|
||||
|
||||
beforeAll(() => {
|
||||
|
||||
@@ -54,7 +54,11 @@ export function createStaticCommandBarButtons(
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
|
||||
buttons.push(newCollectionBtn);
|
||||
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
|
||||
if (
|
||||
configContext.platform !== Platform.Fabric &&
|
||||
userContext.apiType !== "Tables" &&
|
||||
userContext.apiType !== "Cassandra"
|
||||
) {
|
||||
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
||||
|
||||
if (addSynapseLink) {
|
||||
@@ -94,9 +98,9 @@ export function createStaticCommandBarButtons(
|
||||
) {
|
||||
notebookButtons.push(createDivider());
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
notebookButtons.push(createOpenCassandraTerminalButton(container));
|
||||
notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Cassandra));
|
||||
} else {
|
||||
notebookButtons.push(createOpenMongoTerminalButton(container));
|
||||
notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Mongo));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +261,9 @@ export function createDivider(): CommandButtonComponentProps {
|
||||
}
|
||||
|
||||
function areScriptsSupported(): boolean {
|
||||
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||
return (
|
||||
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin")
|
||||
);
|
||||
}
|
||||
|
||||
function createNewCollectionGroup(container: Explorer): CommandButtonComponentProps {
|
||||
@@ -499,8 +505,25 @@ function createOpenTerminalButton(container: Explorer): CommandButtonComponentPr
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenMongoTerminalButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Open Mongo Shell";
|
||||
function createOpenTerminalButtonByKind(
|
||||
container: Explorer,
|
||||
terminalKind: ViewModels.TerminalKind
|
||||
): CommandButtonComponentProps {
|
||||
const terminalFriendlyName = (): string => {
|
||||
switch (terminalKind) {
|
||||
case ViewModels.TerminalKind.Cassandra:
|
||||
return "Cassandra";
|
||||
case ViewModels.TerminalKind.Mongo:
|
||||
return "Mongo";
|
||||
case ViewModels.TerminalKind.Postgres:
|
||||
return "PSQL";
|
||||
case ViewModels.TerminalKind.VCoreMongo:
|
||||
return "MongoDB (vcore)";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
const label = `Open ${terminalFriendlyName()} shell`;
|
||||
const tooltip =
|
||||
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||
const disableButton =
|
||||
@@ -510,7 +533,7 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
container.openNotebookTerminal(terminalKind);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
@@ -521,51 +544,6 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenCassandraTerminalButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Open Cassandra Shell";
|
||||
const tooltip =
|
||||
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||
const disableButton =
|
||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
||||
return {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: disableButton,
|
||||
ariaLabel: label,
|
||||
tooltipText: !disableButton ? "" : tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenPsqlTerminalButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Open PSQL Shell";
|
||||
const disableButton =
|
||||
(!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled) ||
|
||||
useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
||||
return {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Postgres);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: disableButton,
|
||||
ariaLabel: label,
|
||||
tooltipText: !disableButton
|
||||
? ""
|
||||
: "This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.",
|
||||
};
|
||||
}
|
||||
|
||||
function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Reset Workspace";
|
||||
return {
|
||||
@@ -630,7 +608,13 @@ function createStaticCommandBarButtonsForResourceToken(
|
||||
}
|
||||
|
||||
export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
const openPostgreShellBtn = createOpenPsqlTerminalButton(container);
|
||||
const openPostgreShellBtn = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Postgres);
|
||||
|
||||
return [openPostgreShellBtn];
|
||||
}
|
||||
|
||||
export function createVCoreMongoButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.VCoreMongo);
|
||||
|
||||
return [openVCoreMongoTerminalButton];
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
import * as React from "react";
|
||||
import _ from "underscore";
|
||||
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
||||
import { PoolIdType, StyleConstants } from "../../../Common/Constants";
|
||||
import { PoolIdType } from "../../../Common/Constants";
|
||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||
import { configContext, Platform } from "../../../ConfigContext";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
@@ -24,11 +26,14 @@ import { MemoryTracker } from "./MemoryTrackerComponent";
|
||||
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
|
||||
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
|
||||
|
||||
const hoverColor =
|
||||
configContext.platform == Platform.Fabric ? StyleConstants.FabricAccentLight : StyleConstants.AccentLight;
|
||||
|
||||
const getFilter = (isDisabled: boolean): string => {
|
||||
if (isDisabled) {
|
||||
return StyleConstants.GrayScale;
|
||||
}
|
||||
return undefined;
|
||||
return configContext.platform == Platform.Fabric ? StyleConstants.NoColor : undefined;
|
||||
};
|
||||
|
||||
return btns
|
||||
@@ -68,6 +73,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
||||
height: buttonHeightPx,
|
||||
paddingRight: 0,
|
||||
paddingLeft: 0,
|
||||
borderRadius: configContext.platform == Platform.Fabric ? StyleConstants.FabricButtonBorderRadius : "0px",
|
||||
minWidth: 24,
|
||||
marginLeft: isSplit ? 0 : 5,
|
||||
marginRight: isSplit ? 0 : 5,
|
||||
@@ -79,17 +85,17 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
||||
splitButtonMenuButton: {
|
||||
backgroundColor: backgroundColor,
|
||||
selectors: {
|
||||
":hover": { backgroundColor: StyleConstants.AccentLight },
|
||||
":hover": { backgroundColor: hoverColor },
|
||||
},
|
||||
width: 16,
|
||||
},
|
||||
label: { fontSize: StyleConstants.mediumFontSize },
|
||||
rootHovered: { backgroundColor: StyleConstants.AccentLight },
|
||||
rootPressed: { backgroundColor: StyleConstants.AccentLight },
|
||||
rootHovered: { backgroundColor: hoverColor },
|
||||
rootPressed: { backgroundColor: hoverColor },
|
||||
splitButtonMenuButtonExpanded: {
|
||||
backgroundColor: StyleConstants.AccentExtra,
|
||||
selectors: {
|
||||
":hover": { backgroundColor: StyleConstants.AccentLight },
|
||||
":hover": { backgroundColor: hoverColor },
|
||||
},
|
||||
},
|
||||
splitButtonDivider: {
|
||||
@@ -120,7 +126,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
||||
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
|
||||
selectors: {
|
||||
".ms-ContextualMenu-itemText": { fontSize: StyleConstants.mediumFontSize },
|
||||
".ms-ContextualMenu-link:hover": { backgroundColor: StyleConstants.AccentLight },
|
||||
".ms-ContextualMenu-link:hover": { backgroundColor: hoverColor },
|
||||
".ms-ContextualMenu-icon": { width: 16, height: 16 },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
@@ -150,7 +150,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
>
|
||||
<img
|
||||
alt="clear notifications image"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
Clear Notifications
|
||||
</span>
|
||||
@@ -185,7 +185,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
>
|
||||
<img
|
||||
alt="in progress items"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
<span
|
||||
className="numInProgress"
|
||||
@@ -198,7 +198,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
>
|
||||
<img
|
||||
alt="error items"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
<span
|
||||
className="numErroredItems"
|
||||
@@ -211,7 +211,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
>
|
||||
<img
|
||||
alt="info items"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
<span
|
||||
className="numInfoItems"
|
||||
@@ -315,7 +315,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
>
|
||||
<img
|
||||
alt="clear notifications image"
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
Clear Notifications
|
||||
</span>
|
||||
@@ -330,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);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import styled from "styled-components";
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||
|
||||
interface Props {
|
||||
lastSaved?: Date | null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||
import { cloneDeep } from "lodash";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
@@ -10,13 +10,13 @@ import * as Logger from "../../Common/Logger";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { ContainerConnectionInfo, ContainerInfo, PhoenixErrorType } from "../../Contracts/DataModels";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||
import NotebookManager from "./NotebookManager";
|
||||
|
||||
@@ -124,7 +124,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
}
|
||||
|
||||
const firstWriteLocation =
|
||||
userContext.apiType === "Postgres"
|
||||
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
|
||||
? databaseAccount?.location
|
||||
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
||||
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
|
||||
@@ -316,8 +316,10 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
isPhoenixNotebooks = isPublicInternetAllowed && userContext.features.phoenixNotebooks === true;
|
||||
isPhoenixFeatures =
|
||||
isPublicInternetAllowed &&
|
||||
// phoenix needs to be enabled for Postgres accounts since the PSQL shell requires phoenix containers
|
||||
(userContext.features.phoenixFeatures === true || userContext.apiType === "Postgres");
|
||||
// phoenix needs to be enabled for Postgres and VCoreMongo accounts since the PSQL and mongo shell requires phoenix containers
|
||||
(userContext.features.phoenixFeatures === true ||
|
||||
userContext.apiType === "Postgres" ||
|
||||
userContext.apiType === "VCoreMongo");
|
||||
} else {
|
||||
isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed;
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import { GitHubReposTitle } from "Explorer/Tree/ResourceTree";
|
||||
import React, { FormEvent, FunctionComponent } from "react";
|
||||
import { IPinnedRepo } from "../../../Juno/JunoClient";
|
||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
|
||||
|
||||
interface Location {
|
||||
type: "MyNotebooks" | "GitHub";
|
||||
@@ -65,7 +65,7 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
|
||||
|
||||
options.push({
|
||||
key: "GitHub-Header",
|
||||
text: ResourceTreeAdapter.GitHubReposTitle,
|
||||
text: GitHubReposTitle,
|
||||
itemType: SelectableOptionMenuItemType.Header,
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,8 +11,9 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import { QueryCopilotSampleDatabaseId, StyleConstants } from "Common/Constants";
|
||||
import { QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import { StyleConstants } from "Common/StyleConstants";
|
||||
import { createCollection } from "Common/dataAccess/createCollection";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
|
||||
|
||||
@@ -20,7 +20,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
|
||||
>
|
||||
<StackItem>
|
||||
<Image
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
@@ -84,7 +84,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
|
||||
className="imageTextPadding"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem
|
||||
@@ -120,7 +120,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
|
||||
className="imageTextPadding"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem
|
||||
@@ -156,7 +156,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
|
||||
className="imageTextPadding"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem
|
||||
|
||||
@@ -40,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,4 @@
|
||||
/* eslint-disable no-console */
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
import {
|
||||
Callout,
|
||||
CommandBarButton,
|
||||
@@ -18,34 +17,25 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import {
|
||||
ContainerStatusType,
|
||||
PoolIdType,
|
||||
QueryCopilotSampleContainerId,
|
||||
QueryCopilotSampleContainerSchema,
|
||||
ShortenedQueryCopilotSampleContainerSchema,
|
||||
} from "Common/Constants";
|
||||
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
|
||||
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
|
||||
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
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 { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
||||
import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal";
|
||||
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
|
||||
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
|
||||
import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||
import { OnExecuteQueryClick, SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
||||
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/SamplePrompts/SamplePrompts";
|
||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import React, { useRef, useState } from "react";
|
||||
@@ -83,8 +73,6 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
setSelectedQuery,
|
||||
isGeneratingQuery,
|
||||
setIsGeneratingQuery,
|
||||
isExecuting,
|
||||
setIsExecuting,
|
||||
likeQuery,
|
||||
setLikeQuery,
|
||||
dislikeQuery,
|
||||
@@ -93,12 +81,6 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
setShowCallout,
|
||||
showSamplePrompts,
|
||||
setShowSamplePrompts,
|
||||
queryIterator,
|
||||
setQueryIterator,
|
||||
queryResults,
|
||||
setQueryResults,
|
||||
errorMessage,
|
||||
setErrorMessage,
|
||||
isSamplePromptsOpen,
|
||||
setIsSamplePromptsOpen,
|
||||
showDeletePopup,
|
||||
@@ -109,10 +91,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
setshowCopyPopup,
|
||||
showErrorMessageBar,
|
||||
setShowErrorMessageBar,
|
||||
generatedQueryComments,
|
||||
setGeneratedQueryComments,
|
||||
shouldAllocateContainer,
|
||||
setShouldAllocateContainer,
|
||||
} = useQueryCopilot();
|
||||
|
||||
const sampleProps: SamplePromptsProps = {
|
||||
@@ -180,26 +159,27 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
|
||||
const generateSQLQuery = async (): Promise<void> => {
|
||||
try {
|
||||
if (shouldAllocateContainer && userContext.features.enableCopilotPhoenixGateaway) {
|
||||
await explorer.allocateContainer(PoolIdType.QueryCopilot);
|
||||
setShouldAllocateContainer(false);
|
||||
}
|
||||
|
||||
setIsGeneratingQuery(true);
|
||||
setShowDeletePopup(false);
|
||||
useTabs.getState().setIsTabExecuting(true);
|
||||
useTabs.getState().setIsQueryErrorThrown(false);
|
||||
if (
|
||||
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
|
||||
!userContext.features.disableCopilotPhoenixGateaway
|
||||
) {
|
||||
await explorer.allocateContainer(PoolIdType.QueryCopilot);
|
||||
}
|
||||
const payload = {
|
||||
containerSchema: userContext.features.enableCopilotFullSchema
|
||||
? QueryCopilotSampleContainerSchema
|
||||
: ShortenedQueryCopilotSampleContainerSchema,
|
||||
userPrompt: userPrompt,
|
||||
};
|
||||
setShowDeletePopup(false);
|
||||
useQueryCopilot.getState().refreshCorrelationId();
|
||||
const serverInfo = useNotebook.getState().notebookServerInfo;
|
||||
const queryUri = userContext.features.enableCopilotPhoenixGateaway
|
||||
? createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery")
|
||||
: createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery");
|
||||
const serverInfo = useQueryCopilot.getState().notebookServerInfo;
|
||||
const queryUri = userContext.features.disableCopilotPhoenixGateaway
|
||||
? createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery")
|
||||
: createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery");
|
||||
const response = await fetch(queryUri, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -210,9 +190,6 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
});
|
||||
|
||||
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
||||
if (response.status === 404) {
|
||||
setShouldAllocateContainer(true);
|
||||
}
|
||||
if (response.ok) {
|
||||
if (generateSQLQueryResponse?.sql) {
|
||||
let query = `-- **Prompt:** ${userPrompt}\r\n`;
|
||||
@@ -242,64 +219,12 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
}
|
||||
};
|
||||
|
||||
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(),
|
||||
} as FeedOptions);
|
||||
setQueryIterator(queryIterator);
|
||||
|
||||
setTimeout(async () => {
|
||||
await queryDocumentsPerPage(0, queryIterator);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const queryDocumentsPerPage = async (firstItemIndex: number, queryIterator: MinimalQueryIterator): Promise<void> => {
|
||||
try {
|
||||
setIsExecuting(true);
|
||||
useTabs.getState().setIsTabExecuting(true);
|
||||
useTabs.getState().setIsQueryErrorThrown(false);
|
||||
const queryResults: QueryResults = await queryPagesUntilContentPresent(
|
||||
firstItemIndex,
|
||||
async (firstItemIndex: number) =>
|
||||
queryDocumentsPage(QueryCopilotSampleContainerId, queryIterator, firstItemIndex)
|
||||
);
|
||||
|
||||
setQueryResults(queryResults);
|
||||
setErrorMessage("");
|
||||
setShowErrorMessageBar(false);
|
||||
traceSuccess(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||
correlationId: useQueryCopilot.getState().correlationId,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||
correlationId: useQueryCopilot.getState().correlationId,
|
||||
errorMessage: errorMessage,
|
||||
});
|
||||
setErrorMessage(errorMessage);
|
||||
handleError(errorMessage, "executeQueryCopilotTab");
|
||||
useTabs.getState().setIsQueryErrorThrown(true);
|
||||
setShowErrorMessageBar(true);
|
||||
} finally {
|
||||
setIsExecuting(false);
|
||||
useTabs.getState().setIsTabExecuting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
||||
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
||||
const executeQueryBtn = {
|
||||
iconSrc: ExecuteQueryIcon,
|
||||
iconAlt: executeQueryBtnLabel,
|
||||
onCommandClick: () => onExecuteQueryClick(),
|
||||
onCommandClick: () => OnExecuteQueryClick(),
|
||||
commandButtonLabel: executeQueryBtnLabel,
|
||||
ariaLabel: executeQueryBtnLabel,
|
||||
hasPopup: false,
|
||||
@@ -330,10 +255,9 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
return [executeQueryBtn, saveQueryBtn];
|
||||
};
|
||||
const showTeachingBubble = (): void => {
|
||||
const shouldShowTeachingBubble = !inputEdited.current && userPrompt.trim() === "";
|
||||
if (shouldShowTeachingBubble) {
|
||||
if (!inputEdited.current) {
|
||||
setTimeout(() => {
|
||||
if (shouldShowTeachingBubble) {
|
||||
if (!inputEdited.current) {
|
||||
toggleCopilotTeachingBubbleVisible();
|
||||
inputEdited.current = true;
|
||||
}
|
||||
@@ -380,6 +304,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
inputEdited.current = true;
|
||||
startGenerateQueryProcess();
|
||||
}
|
||||
}}
|
||||
@@ -450,6 +375,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
onClick={() => {
|
||||
setUserPrompt(history);
|
||||
setShowSamplePrompts(false);
|
||||
inputEdited.current = true;
|
||||
}}
|
||||
onRenderIcon={() => <Image src={RecentIcon} />}
|
||||
styles={promptStyles}
|
||||
@@ -479,6 +405,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
onClick={() => {
|
||||
setUserPrompt(prompt.text);
|
||||
setShowSamplePrompts(false);
|
||||
inputEdited.current = true;
|
||||
}}
|
||||
onRenderIcon={() => <Image src={HintIcon} />}
|
||||
styles={promptStyles}
|
||||
@@ -624,16 +551,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
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)
|
||||
}
|
||||
/>
|
||||
<QueryCopilotResults />
|
||||
</SplitterLayout>
|
||||
</Stack>
|
||||
<WelcomeModal visible={localStorage.getItem("hideWelcomeModal") !== "true"} />
|
||||
|
||||
@@ -2,9 +2,9 @@ import { QueryCopilotSampleContainerSchema, ShortenedQueryCopilotSampleContainer
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import { createUri } from "Common/UrlUtility";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||
import { userContext } from "UserContext";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
|
||||
jest.mock("@azure/cosmos", () => ({
|
||||
Constants: {
|
||||
@@ -27,22 +27,10 @@ jest.mock("Explorer/Explorer", () => {
|
||||
return MockExplorer;
|
||||
});
|
||||
|
||||
jest.mock("hooks/useQueryCopilot", () => {
|
||||
const mockQueryCopilotStore = {
|
||||
shouldAllocateContainer: true,
|
||||
setShouldAllocateContainer: jest.fn(),
|
||||
correlationId: "mocked-correlation-id",
|
||||
};
|
||||
|
||||
return {
|
||||
useQueryCopilot: jest.fn(() => mockQueryCopilotStore),
|
||||
};
|
||||
});
|
||||
|
||||
describe("Query Copilot Client", () => {
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
describe("submitFeedback", () => {
|
||||
describe("SubmitFeedback", () => {
|
||||
const payload = {
|
||||
like: "like",
|
||||
generatedSql: "GeneratedQuery",
|
||||
@@ -54,18 +42,17 @@ describe("Query Copilot Client", () => {
|
||||
: ShortenedQueryCopilotSampleContainerSchema,
|
||||
};
|
||||
|
||||
const mockStore = useNotebook.getState();
|
||||
beforeEach(() => {
|
||||
mockStore.notebookServerInfo = {
|
||||
notebookServerEndpoint: "mocked-endpoint",
|
||||
authToken: "mocked-token",
|
||||
forwardingId: "mocked-forwarding-id",
|
||||
};
|
||||
});
|
||||
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(useNotebook.getState().notebookServerInfo.notebookServerEndpoint, "feedback")
|
||||
: createUri("https://copilotorchestrater.azurewebsites.net/", "feedback");
|
||||
const feedbackUri = userContext.features.disableCopilotPhoenixGateaway
|
||||
? createUri("https://copilotorchestrater.azurewebsites.net/", "feedback")
|
||||
: createUri(useQueryCopilot.getState().notebookServerInfo.notebookServerEndpoint, "feedback");
|
||||
|
||||
it("should call fetch with the payload with like", async () => {
|
||||
const mockFetch = jest.fn().mockResolvedValueOnce({});
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
import {
|
||||
ContainerStatusType,
|
||||
PoolIdType,
|
||||
QueryCopilotSampleContainerId,
|
||||
QueryCopilotSampleContainerSchema,
|
||||
ShortenedQueryCopilotSampleContainerSchema,
|
||||
} from "Common/Constants";
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
|
||||
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
|
||||
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
||||
import { createUri } from "Common/UrlUtility";
|
||||
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
|
||||
import { QueryResults } from "Contracts/ViewModels";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
|
||||
@@ -20,24 +30,27 @@ export const SendQueryRequest = async ({
|
||||
explorer: Explorer;
|
||||
}): Promise<void> => {
|
||||
if (userPrompt.trim() !== "") {
|
||||
useQueryCopilot.getState().setIsGeneratingQuery(true);
|
||||
useTabs.getState().setIsTabExecuting(true);
|
||||
useTabs.getState().setIsQueryErrorThrown(false);
|
||||
useQueryCopilot
|
||||
.getState()
|
||||
.setChatMessages([...useQueryCopilot.getState().chatMessages, { source: 0, message: userPrompt }]);
|
||||
useQueryCopilot.getState().setIsGeneratingQuery(true);
|
||||
useQueryCopilot.getState().setShouldIncludeInMessages(true);
|
||||
useTabs.getState().setIsTabExecuting(true);
|
||||
useTabs.getState().setIsQueryErrorThrown(false);
|
||||
try {
|
||||
if (useQueryCopilot.getState().shouldAllocateContainer && userContext.features.enableCopilotPhoenixGateaway) {
|
||||
await explorer.allocateContainer(PoolIdType.DefaultPoolId);
|
||||
useQueryCopilot.getState().setShouldAllocateContainer(false);
|
||||
if (
|
||||
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
|
||||
!userContext.features.disableCopilotPhoenixGateaway
|
||||
) {
|
||||
await explorer.allocateContainer(PoolIdType.QueryCopilot);
|
||||
}
|
||||
|
||||
useQueryCopilot.getState().refreshCorrelationId();
|
||||
const serverInfo = useNotebook.getState().notebookServerInfo;
|
||||
const serverInfo = useQueryCopilot.getState().notebookServerInfo;
|
||||
|
||||
const queryUri = userContext.features.enableCopilotPhoenixGateaway
|
||||
? createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery")
|
||||
: createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery");
|
||||
const queryUri = userContext.features.disableCopilotPhoenixGateaway
|
||||
? createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery")
|
||||
: createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery");
|
||||
|
||||
const payload = {
|
||||
containerSchema: userContext.features.enableCopilotFullSchema
|
||||
@@ -55,21 +68,23 @@ export const SendQueryRequest = async ({
|
||||
});
|
||||
|
||||
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
||||
if (response.status === 404) {
|
||||
useQueryCopilot.getState().setShouldAllocateContainer(true);
|
||||
}
|
||||
if (response.ok) {
|
||||
if (generateSQLQueryResponse?.sql) {
|
||||
let query = `Here is a query which will help you with provided prompt.\r\n **Prompt:** ${userPrompt}`;
|
||||
query += `\r\n${generateSQLQueryResponse.sql}`;
|
||||
useQueryCopilot
|
||||
.getState()
|
||||
.setChatMessages([
|
||||
const bubbleMessage = `Here is a query which will help you with provided prompt.\r\n **Prompt:** "${userPrompt}"`;
|
||||
if (useQueryCopilot.getState().shouldIncludeInMessages) {
|
||||
useQueryCopilot.getState().setChatMessages([
|
||||
...useQueryCopilot.getState().chatMessages,
|
||||
{ source: 1, message: query, explanation: generateSQLQueryResponse.explanation },
|
||||
{
|
||||
source: 1,
|
||||
message: bubbleMessage,
|
||||
sqlQuery: generateSQLQueryResponse.sql,
|
||||
explanation: generateSQLQueryResponse.explanation,
|
||||
},
|
||||
]);
|
||||
useQueryCopilot.getState().setGeneratedQuery(generateSQLQueryResponse.sql);
|
||||
useQueryCopilot.getState().setGeneratedQueryComments(generateSQLQueryResponse.explanation);
|
||||
useQueryCopilot.getState().setShowExplanationBubble(true);
|
||||
useQueryCopilot.getState().setGeneratedQuery(generateSQLQueryResponse.sql);
|
||||
useQueryCopilot.getState().setGeneratedQueryComments(generateSQLQueryResponse.explanation);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
|
||||
@@ -96,7 +111,6 @@ export const SubmitFeedback = async ({
|
||||
}): Promise<void> => {
|
||||
try {
|
||||
const { likeQuery, generatedQuery, userPrompt, description, contact } = params;
|
||||
const { correlationId, shouldAllocateContainer, setShouldAllocateContainer } = useQueryCopilot();
|
||||
const payload = {
|
||||
containerSchema: userContext.features.enableCopilotFullSchema
|
||||
? QueryCopilotSampleContainerSchema
|
||||
@@ -107,26 +121,79 @@ export const SubmitFeedback = async ({
|
||||
description: description || "",
|
||||
contact: contact || "",
|
||||
};
|
||||
if (shouldAllocateContainer && userContext.features.enableCopilotPhoenixGateaway) {
|
||||
await explorer.allocateContainer(PoolIdType.DefaultPoolId);
|
||||
setShouldAllocateContainer(false);
|
||||
if (
|
||||
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
|
||||
!userContext.features.disableCopilotPhoenixGateaway
|
||||
) {
|
||||
await explorer.allocateContainer(PoolIdType.QueryCopilot);
|
||||
}
|
||||
const serverInfo = useNotebook.getState().notebookServerInfo;
|
||||
const feedbackUri = userContext.features.enableCopilotPhoenixGateaway
|
||||
? createUri(serverInfo.notebookServerEndpoint, "feedback")
|
||||
: createUri("https://copilotorchestrater.azurewebsites.net/", "feedback");
|
||||
const response = await fetch(feedbackUri, {
|
||||
const serverInfo = useQueryCopilot.getState().notebookServerInfo;
|
||||
const feedbackUri = userContext.features.disableCopilotPhoenixGateaway
|
||||
? createUri("https://copilotorchestrater.azurewebsites.net/", "feedback")
|
||||
: createUri(serverInfo.notebookServerEndpoint, "feedback");
|
||||
await fetch(feedbackUri, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-ms-correlationid": correlationId,
|
||||
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (response.status === 404) {
|
||||
setShouldAllocateContainer(true);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "copilotSubmitFeedback");
|
||||
}
|
||||
};
|
||||
|
||||
export const OnExecuteQueryClick = async (): Promise<void> => {
|
||||
traceStart(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||
correlationId: useQueryCopilot.getState().correlationId,
|
||||
userPrompt: useQueryCopilot.getState().userPrompt,
|
||||
generatedQuery: useQueryCopilot.getState().generatedQuery,
|
||||
generatedQueryComments: useQueryCopilot.getState().generatedQueryComments,
|
||||
executedQuery: useQueryCopilot.getState().selectedQuery || useQueryCopilot.getState().query,
|
||||
});
|
||||
const queryToExecute = useQueryCopilot.getState().selectedQuery || useQueryCopilot.getState().query;
|
||||
const queryIterator = querySampleDocuments(queryToExecute, {
|
||||
enableCrossPartitionQuery: shouldEnableCrossPartitionKey(),
|
||||
} as FeedOptions);
|
||||
useQueryCopilot.getState().setQueryIterator(queryIterator);
|
||||
|
||||
setTimeout(async () => {
|
||||
await QueryDocumentsPerPage(0, queryIterator);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
export const QueryDocumentsPerPage = async (
|
||||
firstItemIndex: number,
|
||||
queryIterator: MinimalQueryIterator
|
||||
): Promise<void> => {
|
||||
try {
|
||||
useQueryCopilot.getState().setIsExecuting(true);
|
||||
useTabs.getState().setIsTabExecuting(true);
|
||||
useTabs.getState().setIsQueryErrorThrown(false);
|
||||
const queryResults: QueryResults = await queryPagesUntilContentPresent(
|
||||
firstItemIndex,
|
||||
async (firstItemIndex: number) => queryDocumentsPage(QueryCopilotSampleContainerId, queryIterator, firstItemIndex)
|
||||
);
|
||||
|
||||
useQueryCopilot.getState().setQueryResults(queryResults);
|
||||
useQueryCopilot.getState().setErrorMessage("");
|
||||
useQueryCopilot.getState().setShowErrorMessageBar(false);
|
||||
traceSuccess(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||
correlationId: useQueryCopilot.getState().correlationId,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||
correlationId: useQueryCopilot.getState().correlationId,
|
||||
errorMessage: errorMessage,
|
||||
});
|
||||
useQueryCopilot.getState().setErrorMessage(errorMessage);
|
||||
handleError(errorMessage, "executeQueryCopilotTab");
|
||||
useTabs.getState().setIsQueryErrorThrown(true);
|
||||
useQueryCopilot.getState().setShowErrorMessageBar(true);
|
||||
} finally {
|
||||
useQueryCopilot.getState().setIsExecuting(false);
|
||||
useTabs.getState().setIsTabExecuting(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,11 +11,13 @@ export interface GenerateSQLQueryResponse {
|
||||
enum MessageSource {
|
||||
User,
|
||||
AI,
|
||||
AIExplanation,
|
||||
}
|
||||
|
||||
export interface CopilotMessage {
|
||||
source: MessageSource;
|
||||
message: string;
|
||||
sqlQuery?: string;
|
||||
explanation?: string;
|
||||
}
|
||||
|
||||
|
||||
19
src/Explorer/QueryCopilot/Shared/QueryCopilotResults.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React from "react";
|
||||
|
||||
export const QueryCopilotResults: React.FC = (): JSX.Element => {
|
||||
return (
|
||||
<QueryResultSection
|
||||
isMongoDB={false}
|
||||
queryEditorContent={useQueryCopilot.getState().selectedQuery || useQueryCopilot.getState().query}
|
||||
error={useQueryCopilot.getState().errorMessage}
|
||||
queryResults={useQueryCopilot.getState().queryResults}
|
||||
isExecuting={useQueryCopilot.getState().isExecuting}
|
||||
executeQueryDocumentsPage={(firstItemIndex: number) =>
|
||||
QueryDocumentsPerPage(firstItemIndex, useQueryCopilot.getState().queryIterator)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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 { ExplanationButton } from "./ExplanationButton";
|
||||
|
||||
describe("Explanation Button", () => {
|
||||
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().shouldIncludeInMessages = true;
|
||||
|
||||
const wrapper = shallow(<ExplanationButton />);
|
||||
|
||||
expect(wrapper.find("Stack")).toHaveLength(1);
|
||||
expect(wrapper.find("Text")).toHaveLength(1);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render 'Explain this query' link", () => {
|
||||
useQueryCopilot.getState().shouldIncludeInMessages = true;
|
||||
const mockSetChatMessages = jest.fn();
|
||||
const mockSetIsGeneratingExplanation = jest.fn();
|
||||
const mockSetShouldIncludeInMessages = jest.fn();
|
||||
const mockSetShowExplanationBubble = jest.fn();
|
||||
useQueryCopilot.getState().setChatMessages = mockSetChatMessages;
|
||||
useQueryCopilot.getState().setIsGeneratingExplanation = mockSetIsGeneratingExplanation;
|
||||
useQueryCopilot.getState().setShouldIncludeInMessages = mockSetShouldIncludeInMessages;
|
||||
useQueryCopilot.getState().setShowExplanationBubble = mockSetShowExplanationBubble;
|
||||
|
||||
const wrapper = shallow(<ExplanationButton />);
|
||||
|
||||
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(mockSetShowExplanationBubble).toHaveBeenCalledWith(false);
|
||||
|
||||
jest.advanceTimersByTime(3000);
|
||||
|
||||
expect(mockSetIsGeneratingExplanation).toHaveBeenCalledWith(false);
|
||||
expect(mockSetChatMessages).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should render nothing when conditions are not met", () => {
|
||||
useQueryCopilot.getState().showExplanationBubble = false;
|
||||
|
||||
const wrapper = shallow(<ExplanationButton />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Stack, Text } from "@fluentui/react";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React from "react";
|
||||
|
||||
export const ExplanationButton: React.FC = (): JSX.Element => {
|
||||
const {
|
||||
showExplanationBubble,
|
||||
isGeneratingQuery,
|
||||
chatMessages,
|
||||
setChatMessages,
|
||||
generatedQuery,
|
||||
generatedQueryComments,
|
||||
isGeneratingExplanation,
|
||||
setIsGeneratingExplanation,
|
||||
setShouldIncludeInMessages,
|
||||
setShowExplanationBubble,
|
||||
} = useQueryCopilot();
|
||||
|
||||
const showExplanation = () => {
|
||||
setChatMessages([...chatMessages, { source: 0, message: "Explain this query to me" }]);
|
||||
setIsGeneratingExplanation(true);
|
||||
setShouldIncludeInMessages(true);
|
||||
setShowExplanationBubble(false);
|
||||
|
||||
setTimeout(() => {
|
||||
if (useQueryCopilot.getState().shouldIncludeInMessages) {
|
||||
setIsGeneratingExplanation(false);
|
||||
setChatMessages([...chatMessages, { source: 2, message: generatedQueryComments, sqlQuery: generatedQuery }]);
|
||||
}
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
showExplanationBubble &&
|
||||
!isGeneratingQuery &&
|
||||
!isGeneratingExplanation && (
|
||||
<Stack
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "5px 5px 5px 50px",
|
||||
margin: "5px",
|
||||
}}
|
||||
>
|
||||
<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,32 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Explanation Button should render explanation bubble with generated comments 1`] = `
|
||||
<Stack
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"display": "flex",
|
||||
"margin": "5px",
|
||||
"padding": "5px 5px 5px 50px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"border": "1.5px solid #B0BEFF",
|
||||
"borderRadius": "4px",
|
||||
"cursor": "pointer",
|
||||
"marginBottom": "5px",
|
||||
"padding": "2px",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
Explain this query to me
|
||||
</Text>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`Explanation Button should render nothing when conditions are not met 1`] = `""`;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Stack, Text } from "@fluentui/react";
|
||||
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||
import { FeedbackButtons } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Feedback/FeedbackButtons";
|
||||
import React from "react";
|
||||
|
||||
export const ExplanationBubble = ({ copilotMessage }: { copilotMessage: CopilotMessage }): JSX.Element => {
|
||||
return (
|
||||
<Stack
|
||||
horizontalAlign="start"
|
||||
verticalAlign="start"
|
||||
tokens={{ padding: 8, childrenGap: 8 }}
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
borderRadius: "8px",
|
||||
margin: "5px 10px",
|
||||
textAlign: "start",
|
||||
}}
|
||||
>
|
||||
<Text>{copilotMessage.message}</Text>
|
||||
<FeedbackButtons sqlQuery={copilotMessage.sqlQuery} />
|
||||
<Text style={{ fontWeight: 400, fontSize: "10px", lineHeight: "14px" }}>
|
||||
AI-generated content may be incorrect
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||
import { ExplanationBubble } from "Explorer/QueryCopilot/V2/Bubbles/Explanation/ExplainationBubble";
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
describe("Explanation Bubble snapshot tests", () => {
|
||||
it("should render", () => {
|
||||
const mockCopilotMessage: CopilotMessage = {
|
||||
source: 2,
|
||||
message: "Mock message",
|
||||
};
|
||||
|
||||
const wrapper = shallow(<ExplanationBubble copilotMessage={mockCopilotMessage} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Explanation Bubble snapshot tests should render 1`] = `
|
||||
<Stack
|
||||
horizontalAlign="start"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "white",
|
||||
"borderRadius": "8px",
|
||||
"margin": "5px 10px",
|
||||
"textAlign": "start",
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 8,
|
||||
"padding": 8,
|
||||
}
|
||||
}
|
||||
verticalAlign="start"
|
||||
>
|
||||
<Text>
|
||||
Mock message
|
||||
</Text>
|
||||
<FeedbackButtons />
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": "10px",
|
||||
"fontWeight": 400,
|
||||
"lineHeight": "14px",
|
||||
}
|
||||
}
|
||||
>
|
||||
AI-generated content may be incorrect
|
||||
</Text>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -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 sqlQuery={""} />);
|
||||
|
||||
const button = wrapper.find(IconButton).first();
|
||||
button.simulate("click", {});
|
||||
|
||||
expect(document.execCommand).toHaveBeenCalledWith("copy");
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { IconButton } from "@fluentui/react";
|
||||
import React from "react";
|
||||
import CopilotCopy from "../../../../../../../../images/CopilotCopy.svg";
|
||||
|
||||
export const CopyButton = ({ sqlQuery }: { sqlQuery: string }): JSX.Element => {
|
||||
const copyGeneratedCode = (): void => {
|
||||
const queryElement = document.createElement("textarea");
|
||||
queryElement.value = sqlQuery;
|
||||
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 sqlQuery={""} />);
|
||||
|
||||
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 sqlQuery={""} />);
|
||||
|
||||
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 sqlQuery={""} />);
|
||||
|
||||
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 sqlQuery={""} />);
|
||||
|
||||
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 sqlQuery={""} />);
|
||||
|
||||
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 sqlQuery={""} />);
|
||||
|
||||
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 sqlQuery={""} />);
|
||||
|
||||
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 sqlQuery={""} />);
|
||||
|
||||
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 sqlQuery={""} />);
|
||||
|
||||
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 sqlQuery={""} />);
|
||||
|
||||
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 sqlQuery={""} />);
|
||||
|
||||
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 sqlQuery={""} />);
|
||||
|
||||
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 sqlQuery={""} />);
|
||||
|
||||
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 sqlQuery={""} />);
|
||||
|
||||
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 = ({ sqlQuery }: { sqlQuery: string }): JSX.Element => {
|
||||
const { 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(sqlQuery, 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(sqlQuery, 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,10 @@
|
||||
import { InsertButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Insert/InsertButton";
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
describe("Insert button snapshot tests", () => {
|
||||
it("should click and update state", () => {
|
||||
const wrapper = shallow(<InsertButton sqlQuery={""} />);
|
||||
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 = ({ sqlQuery }: { sqlQuery: string }): JSX.Element => {
|
||||
return (
|
||||
<ActionButton
|
||||
iconProps={{ imageProps: { src: CopilotInsert } }}
|
||||
style={{ borderRadius: "4px", borderWidth: "1px", borderColor: "#D1D1D1", height: "24px", paddingBottom: "2px" }}
|
||||
onClick={() => useQueryCopilot.getState().setQuery(sqlQuery)}
|
||||
>
|
||||
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 sqlQuery={""} />);
|
||||
|
||||
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 = ({ sqlQuery }: { sqlQuery: string }): JSX.Element => {
|
||||
return (
|
||||
<Stack horizontal>
|
||||
<Stack.Item style={{ paddingTop: "5px" }}>
|
||||
<InsertButton sqlQuery={sqlQuery} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<CopyButton sqlQuery={sqlQuery} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<FeedbackButtons sqlQuery={sqlQuery} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<MoreButton />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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
|
||||
sqlQuery=""
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<CopyButton
|
||||
sqlQuery=""
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<FeedbackButtons
|
||||
sqlQuery=""
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<MoreButton />
|
||||
</StackItem>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
import { OutputBubble } from "Explorer/QueryCopilot/V2/Bubbles/Output/OutputBubble";
|
||||
import { shallow } from "enzyme";
|
||||
import { withHooks } from "jest-react-hooks-shallow";
|
||||
import React from "react";
|
||||
|
||||
describe("Output Bubble snapshot tests", () => {
|
||||
it("should render and update height", () => {
|
||||
withHooks(() => {
|
||||
const wrapper = shallow(
|
||||
<OutputBubble
|
||||
copilotMessage={{ message: "testMessage", source: 1, explanation: "testExplanation", sqlQuery: "testSQL" }}
|
||||
/>
|
||||
);
|
||||
|
||||
const editor = wrapper.find(EditorReact).first();
|
||||
|
||||
expect(editor.props().monacoContainerStyles).not.toHaveProperty("height", undefined);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
71
src/Explorer/QueryCopilot/V2/Bubbles/Output/OutputBubble.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Stack, Text } from "@fluentui/react";
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||
import { OutputBubbleButtons } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/OutputBubbleButtons";
|
||||
import { userContext } from "UserContext";
|
||||
import React, { useState } from "react";
|
||||
|
||||
export const OutputBubble = ({ copilotMessage }: { copilotMessage: CopilotMessage }): JSX.Element => {
|
||||
const [windowHeight, setWindowHeight] = useState<string>();
|
||||
const textHeightWithPadding = 16;
|
||||
|
||||
const calculateQueryWindowHeight = (): string => {
|
||||
const outputWidth = document.getElementById("outputBubble")?.clientWidth;
|
||||
const responseLength = copilotMessage.sqlQuery.length;
|
||||
|
||||
if (outputWidth > responseLength) {
|
||||
return `${textHeightWithPadding * 3}px`;
|
||||
} else {
|
||||
const neededLines = Math.ceil(responseLength / outputWidth);
|
||||
return `${neededLines * textHeightWithPadding}px`;
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (userContext.features.copilotChatFixedMonacoEditorHeight) {
|
||||
setWindowHeight(`${textHeightWithPadding * 5}px`);
|
||||
} else {
|
||||
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" }}>{copilotMessage.message}</Stack.Item>
|
||||
<Stack.Item style={{ alignSelf: "stretch", flexGrow: 4 }}>
|
||||
<EditorReact
|
||||
language={"sql"}
|
||||
content={copilotMessage.sqlQuery}
|
||||
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 sqlQuery={copilotMessage.sqlQuery} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Text style={{ fontWeight: 400, fontSize: "10px", lineHeight: "14px" }}>
|
||||
AI-generated content may be incorrect
|
||||
</Text>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Output Bubble snapshot tests should render and update height 1`] = `
|
||||
<Stack
|
||||
id="outputBubble"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "white",
|
||||
"borderRadius": "8px",
|
||||
"display": "flex",
|
||||
"margin": "10px",
|
||||
"padding": "10px",
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 8,
|
||||
"padding": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StackItem
|
||||
style={
|
||||
Object {
|
||||
"alignSelf": "flex-start",
|
||||
"paddingLeft": "2px",
|
||||
}
|
||||
}
|
||||
>
|
||||
testMessage
|
||||
</StackItem>
|
||||
<StackItem
|
||||
style={
|
||||
Object {
|
||||
"alignSelf": "stretch",
|
||||
"flexGrow": 4,
|
||||
}
|
||||
}
|
||||
>
|
||||
<EditorReact
|
||||
ariaLabel="AI Response"
|
||||
content="testSQL"
|
||||
isReadOnly={true}
|
||||
language="sql"
|
||||
lineDecorationsWidth={0}
|
||||
lineNumbers="on"
|
||||
lineNumbersMinChars={2}
|
||||
minimap={
|
||||
Object {
|
||||
"enabled": false,
|
||||
}
|
||||
}
|
||||
monacoContainerStyles={
|
||||
Object {
|
||||
"borderRadius": "4px",
|
||||
"height": "NaNpx",
|
||||
}
|
||||
}
|
||||
scrollBeyondLastLine={false}
|
||||
wordWrap="on"
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem
|
||||
style={
|
||||
Object {
|
||||
"alignSelf": "flex-start",
|
||||
}
|
||||
}
|
||||
>
|
||||
<OutputBubbleButtons
|
||||
sqlQuery="testSQL"
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": "10px",
|
||||
"fontWeight": 400,
|
||||
"lineHeight": "14px",
|
||||
}
|
||||
}
|
||||
>
|
||||
AI-generated content may be incorrect
|
||||
</Text>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -19,7 +19,7 @@ exports[`Footer snapshot test should not pass if no text 1`] = `
|
||||
<Stack>
|
||||
<Image
|
||||
onClick={[Function]}
|
||||
src=""
|
||||
src={Object {}}
|
||||
styles={
|
||||
Object {
|
||||
"label": Object {
|
||||
@@ -104,7 +104,7 @@ exports[`Footer snapshot test should not pass text with non enter key 1`] = `
|
||||
<Stack>
|
||||
<Image
|
||||
onClick={[Function]}
|
||||
src=""
|
||||
src={Object {}}
|
||||
styles={
|
||||
Object {
|
||||
"label": Object {
|
||||
@@ -189,7 +189,7 @@ exports[`Footer snapshot test should open sample prompts on button click 1`] = `
|
||||
<Stack>
|
||||
<Image
|
||||
onClick={[Function]}
|
||||
src=""
|
||||
src={Object {}}
|
||||
styles={
|
||||
Object {
|
||||
"label": Object {
|
||||
@@ -274,7 +274,7 @@ exports[`Footer snapshot test should pass text with enter key 1`] = `
|
||||
<Stack>
|
||||
<Image
|
||||
onClick={[Function]}
|
||||
src=""
|
||||
src={Object {}}
|
||||
styles={
|
||||
Object {
|
||||
"label": Object {
|
||||
@@ -359,7 +359,7 @@ exports[`Footer snapshot test should pass text with icon button 1`] = `
|
||||
<Stack>
|
||||
<Image
|
||||
onClick={[Function]}
|
||||
src=""
|
||||
src={Object {}}
|
||||
styles={
|
||||
Object {
|
||||
"label": Object {
|
||||
@@ -444,7 +444,7 @@ exports[`Footer snapshot test should update user input 1`] = `
|
||||
<Stack>
|
||||
<Image
|
||||
onClick={[Function]}
|
||||
src=""
|
||||
src={Object {}}
|
||||
styles={
|
||||
Object {
|
||||
"label": Object {
|
||||
|
||||
@@ -1,39 +1,81 @@
|
||||
import { IconButton, Image, Stack, Text } from "@fluentui/react";
|
||||
import { DefaultButton, IconButton, Image, Modal, PrimaryButton, Stack, Text } from "@fluentui/react";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React from "react";
|
||||
import CopilotIcon from "../../../../../images/CopilotSidebarLogo.svg";
|
||||
|
||||
export const Header: React.FC = (): JSX.Element => {
|
||||
const { setShowCopilotSidebar } = useQueryCopilot();
|
||||
const { setShowCopilotSidebar, chatMessages, setChatMessages, setShowExplanationBubble } = useQueryCopilot();
|
||||
const [showDeleteHistoryModal, setShowDeleteHistoryModal] = React.useState(false);
|
||||
|
||||
const getDeleteHistoryModal = () => {
|
||||
return (
|
||||
<Modal isOpen={showDeleteHistoryModal} styles={{ main: { minHeight: "122px", minWidth: "480px" } }}>
|
||||
<Stack style={{ padding: "16px 24px", height: "auto" }}>
|
||||
<Text style={{ height: 24, fontSize: "18px" }}>
|
||||
<b>Delete chat history?</b>
|
||||
</Text>
|
||||
<Text style={{ marginTop: 10, marginBottom: 20 }}>
|
||||
This action will clear all chat history. Are you sure you want to continue?
|
||||
</Text>
|
||||
<Stack horizontal tokens={{ childrenGap: 10 }} horizontalAlign="start">
|
||||
<PrimaryButton
|
||||
style={{ padding: "0px 20px", height: 24 }}
|
||||
onClick={() => {
|
||||
setChatMessages([]);
|
||||
setShowExplanationBubble(false);
|
||||
setShowDeleteHistoryModal(false);
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</PrimaryButton>
|
||||
<DefaultButton style={{ padding: "0px 20px", height: 24 }} onClick={() => setShowDeleteHistoryModal(false)}>
|
||||
Close
|
||||
</DefaultButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
style={{ margin: "15px 0px 0px 0px", padding: "5px", display: "flex", justifyContent: "space-between" }}
|
||||
horizontal
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Image src={CopilotIcon} />
|
||||
<Text style={{ marginLeft: "5px", fontWeight: "bold" }}>Copilot</Text>
|
||||
<Text
|
||||
style={{
|
||||
background: "#f0f0f0",
|
||||
fontSize: "10px",
|
||||
padding: "2px 4px",
|
||||
marginLeft: "5px",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
>
|
||||
Preview
|
||||
</Text>
|
||||
<>
|
||||
<Stack
|
||||
style={{ margin: "15px 0px 0px 0px", padding: "5px", display: "flex", justifyContent: "space-between" }}
|
||||
horizontal
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Image src={CopilotIcon} />
|
||||
<Text style={{ marginLeft: "5px", fontWeight: "bold" }}>Copilot</Text>
|
||||
<Text
|
||||
style={{
|
||||
background: "#f0f0f0",
|
||||
fontSize: "10px",
|
||||
padding: "2px 4px",
|
||||
marginLeft: "5px",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
>
|
||||
Preview
|
||||
</Text>
|
||||
</Stack>
|
||||
<IconButton
|
||||
onClick={() => setShowDeleteHistoryModal(true)}
|
||||
iconProps={{ iconName: "History" }}
|
||||
title="Delete history"
|
||||
ariaLabel="Delete history"
|
||||
style={{ color: "#424242", verticalAlign: "middle" }}
|
||||
disabled={chatMessages.length === 0}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => setShowCopilotSidebar(false)}
|
||||
iconProps={{ iconName: "Cancel" }}
|
||||
title="Exit"
|
||||
ariaLabel="Exit"
|
||||
style={{ color: "#424242", verticalAlign: "middle" }}
|
||||
/>
|
||||
</Stack>
|
||||
<IconButton
|
||||
onClick={() => setShowCopilotSidebar(false)}
|
||||
iconProps={{ iconName: "Cancel" }}
|
||||
title="Exit"
|
||||
ariaLabel="Exit"
|
||||
style={{ color: "#424242", verticalAlign: "middle" }}
|
||||
/>
|
||||
</Stack>
|
||||
{getDeleteHistoryModal()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,64 +1,158 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Header snapshot test should close on button click 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"display": "flex",
|
||||
"justifyContent": "space-between",
|
||||
"margin": "15px 0px 0px 0px",
|
||||
"padding": "5px",
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Fragment>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontWeight": "bold",
|
||||
"marginLeft": "5px",
|
||||
}
|
||||
}
|
||||
>
|
||||
Copilot
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"background": "#f0f0f0",
|
||||
"borderRadius": "8px",
|
||||
"fontSize": "10px",
|
||||
"marginLeft": "5px",
|
||||
"padding": "2px 4px",
|
||||
}
|
||||
}
|
||||
>
|
||||
Preview
|
||||
</Text>
|
||||
</Stack>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Exit"
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Cancel",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#424242",
|
||||
"verticalAlign": "middle",
|
||||
"display": "flex",
|
||||
"justifyContent": "space-between",
|
||||
"margin": "15px 0px 0px 0px",
|
||||
"padding": "5px",
|
||||
}
|
||||
}
|
||||
title="Exit"
|
||||
/>
|
||||
</Stack>
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src={Object {}}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontWeight": "bold",
|
||||
"marginLeft": "5px",
|
||||
}
|
||||
}
|
||||
>
|
||||
Copilot
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"background": "#f0f0f0",
|
||||
"borderRadius": "8px",
|
||||
"fontSize": "10px",
|
||||
"marginLeft": "5px",
|
||||
"padding": "2px 4px",
|
||||
}
|
||||
}
|
||||
>
|
||||
Preview
|
||||
</Text>
|
||||
</Stack>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Delete history"
|
||||
disabled={true}
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "History",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#424242",
|
||||
"verticalAlign": "middle",
|
||||
}
|
||||
}
|
||||
title="Delete history"
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Exit"
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Cancel",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#424242",
|
||||
"verticalAlign": "middle",
|
||||
}
|
||||
}
|
||||
title="Exit"
|
||||
/>
|
||||
</Stack>
|
||||
<Modal
|
||||
isOpen={true}
|
||||
styles={
|
||||
Object {
|
||||
"main": Object {
|
||||
"minHeight": "122px",
|
||||
"minWidth": "480px",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
style={
|
||||
Object {
|
||||
"height": "auto",
|
||||
"padding": "16px 24px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": "18px",
|
||||
"height": 24,
|
||||
}
|
||||
}
|
||||
>
|
||||
<b>
|
||||
Delete chat history?
|
||||
</b>
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": 20,
|
||||
"marginTop": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
This action will clear all chat history. Are you sure you want to continue?
|
||||
</Text>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
horizontalAlign="start"
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedPrimaryButton
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": 24,
|
||||
"padding": "0px 20px",
|
||||
}
|
||||
}
|
||||
>
|
||||
Continue
|
||||
</CustomizedPrimaryButton>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": 24,
|
||||
"padding": "0px 20px",
|
||||
}
|
||||
}
|
||||
>
|
||||
Close
|
||||
</CustomizedDefaultButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Stack } from "@fluentui/react";
|
||||
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||
import { ExplanationButton } from "Explorer/QueryCopilot/V2/Bubbles/Explanation/Button/ExplanationButton";
|
||||
import { ExplanationBubble } from "Explorer/QueryCopilot/V2/Bubbles/Explanation/ExplainationBubble";
|
||||
import { OutputBubble } from "Explorer/QueryCopilot/V2/Bubbles/Output/OutputBubble";
|
||||
import { RetrievingBubble } from "Explorer/QueryCopilot/V2/Bubbles/Retriveing/RetrievingBubble";
|
||||
import { SampleBubble } from "Explorer/QueryCopilot/V2/Bubbles/Sample/SampleBubble";
|
||||
import { WelcomeBubble } from "Explorer/QueryCopilot/V2/Bubbles/Welcome/WelcomeBubble";
|
||||
@@ -14,8 +17,8 @@ export const QueryCopilotSidebar: React.FC<QueryCopilotProps> = ({ explorer }: Q
|
||||
setWasCopilotUsed,
|
||||
showCopilotSidebar,
|
||||
chatMessages,
|
||||
showWelcomeSidebar,
|
||||
isGeneratingQuery,
|
||||
showWelcomeSidebar,
|
||||
} = useQueryCopilot();
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -40,39 +43,34 @@ export const QueryCopilotSidebar: React.FC<QueryCopilotProps> = ({ explorer }: Q
|
||||
}}
|
||||
>
|
||||
<WelcomeBubble />
|
||||
{chatMessages.map((message, index) =>
|
||||
message.source === 0 ? (
|
||||
<Stack
|
||||
key={index}
|
||||
horizontalAlign="center"
|
||||
tokens={{ padding: 8, childrenGap: 8 }}
|
||||
style={{
|
||||
backgroundColor: "#E0E7FF",
|
||||
borderRadius: "8px",
|
||||
margin: "5px 10px",
|
||||
textAlign: "start",
|
||||
}}
|
||||
>
|
||||
{message.message}
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack
|
||||
key={index}
|
||||
horizontalAlign="center"
|
||||
tokens={{ padding: 8, childrenGap: 8 }}
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
borderRadius: "8px",
|
||||
margin: "5px 10px",
|
||||
textAlign: "start",
|
||||
}}
|
||||
>
|
||||
{message.message}
|
||||
</Stack>
|
||||
)
|
||||
)}
|
||||
|
||||
{chatMessages.map((message, index) => {
|
||||
switch (message.source) {
|
||||
case 0:
|
||||
return (
|
||||
<Stack
|
||||
key={index}
|
||||
horizontalAlign="center"
|
||||
tokens={{ padding: 8, childrenGap: 8 }}
|
||||
style={{
|
||||
backgroundColor: "#E0E7FF",
|
||||
borderRadius: "8px",
|
||||
margin: "5px 10px",
|
||||
textAlign: "start",
|
||||
}}
|
||||
>
|
||||
{message.message}
|
||||
</Stack>
|
||||
);
|
||||
case 1:
|
||||
return <OutputBubble key={index} copilotMessage={message} />;
|
||||
case 2:
|
||||
return <ExplanationBubble key={index} copilotMessage={message} />;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
})}
|
||||
<RetrievingBubble />
|
||||
<ExplanationButton />
|
||||
|
||||
{chatMessages.length === 0 && !isGeneratingQuery && <SampleBubble />}
|
||||
</Stack>
|
||||
|
||||
@@ -74,7 +74,7 @@ exports[`Query Copilot Carousel snapshot test should render when isOpen is true
|
||||
To generate queries , just describe the query you want and copilot will generate the query for you.Watch this video to learn more about how to use copilot.
|
||||
</Text>
|
||||
<Image
|
||||
src=""
|
||||
src={Object {}}
|
||||
style={
|
||||
Object {
|
||||
"margin": "16px auto",
|
||||
|
||||
@@ -23,7 +23,7 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
src={Object {}}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
@@ -125,25 +125,26 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
||||
>
|
||||
<EditorReact
|
||||
ariaLabel="Editing Query"
|
||||
content=""
|
||||
content="SELECT * FROM c"
|
||||
isReadOnly={false}
|
||||
language="sql"
|
||||
lineNumbers="on"
|
||||
onContentChanged={[Function]}
|
||||
onContentSelected={[Function]}
|
||||
/>
|
||||
<QueryResultSection
|
||||
error=""
|
||||
executeQueryDocumentsPage={[Function]}
|
||||
isExecuting={false}
|
||||
isMongoDB={false}
|
||||
queryEditorContent=""
|
||||
/>
|
||||
<QueryCopilotResults />
|
||||
</t>
|
||||
</Stack>
|
||||
<WelcomeModal
|
||||
visible={true}
|
||||
/>
|
||||
<DeletePopup
|
||||
clearFeedback={[Function]}
|
||||
setQuery={[Function]}
|
||||
setShowDeletePopup={[Function]}
|
||||
showDeletePopup={false}
|
||||
showFeedbackBar={[Function]}
|
||||
/>
|
||||
<CopyPopup
|
||||
setShowCopyPopup={[Function]}
|
||||
showCopyPopup={false}
|
||||
|
||||
@@ -2,20 +2,26 @@ import { Image, PrimaryButton, Stack, Text } from "@fluentui/react";
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import React from "react";
|
||||
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
|
||||
|
||||
export const QuickstartFirewallNotification: React.FC = (): JSX.Element => (
|
||||
export interface QuickstartFirewallNotificationProps {
|
||||
shellName: string;
|
||||
screenshot: string;
|
||||
messageType: MessageTypes;
|
||||
}
|
||||
|
||||
export const QuickstartFirewallNotification: React.FC<QuickstartFirewallNotificationProps> = ({
|
||||
shellName,
|
||||
screenshot,
|
||||
messageType,
|
||||
}: QuickstartFirewallNotificationProps): JSX.Element => (
|
||||
<Stack style={{ padding: "16px 20px" }}>
|
||||
<Text block>
|
||||
To use the PostgreSQL shell, you need to add a firewall rule to allow access from all IP addresses
|
||||
To use the {shellName} shell, you need to add a firewall rule to allow access from all IP addresses
|
||||
(0.0.0.0-255.255.255).
|
||||
</Text>
|
||||
<Text block>We strongly recommend removing this rule once you finish using the PostgreSQL shell.</Text>
|
||||
<Image style={{ margin: "20px 0" }} src={FirewallRuleScreenshot} />
|
||||
<PrimaryButton
|
||||
style={{ width: 150 }}
|
||||
onClick={() => sendMessage({ type: MessageTypes.OpenPostgresNetworkingBlade })}
|
||||
>
|
||||
<Text block>We strongly recommend removing this rule once you finish using the {shellName} shell.</Text>
|
||||
<Image style={{ margin: "20px 0" }} src={screenshot} />
|
||||
<PrimaryButton style={{ width: 150 }} onClick={() => sendMessage({ type: messageType })}>
|
||||
Add firewall rule
|
||||
</PrimaryButton>
|
||||
</Stack>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import {
|
||||
DefaultButton,
|
||||
Icon,
|
||||
IconButton,
|
||||
Image,
|
||||
IPivotItemProps,
|
||||
Pivot,
|
||||
PivotItem,
|
||||
PrimaryButton,
|
||||
@@ -21,18 +19,10 @@ import {
|
||||
queryCommand,
|
||||
queryCommandForDisplay,
|
||||
} from "Explorer/Quickstart/PostgreQuickstartCommands";
|
||||
import { customPivotHeaderRenderer } from "Explorer/Quickstart/Shared/QuickstartRenderUtilities";
|
||||
import { useTerminal } from "hooks/useTerminal";
|
||||
import React, { useState } from "react";
|
||||
import Youtube from "react-youtube";
|
||||
import Pivot1SelectedIcon from "../../../images/Pivot1_selected.svg";
|
||||
import Pivot2Icon from "../../../images/Pivot2.svg";
|
||||
import Pivot2SelectedIcon from "../../../images/Pivot2_selected.svg";
|
||||
import Pivot3Icon from "../../../images/Pivot3.svg";
|
||||
import Pivot3SelectedIcon from "../../../images/Pivot3_selected.svg";
|
||||
import Pivot4Icon from "../../../images/Pivot4.svg";
|
||||
import Pivot4SelectedIcon from "../../../images/Pivot4_selected.svg";
|
||||
import Pivot5Icon from "../../../images/Pivot5.svg";
|
||||
import Pivot5SelectedIcon from "../../../images/Pivot5_selected.svg";
|
||||
import CompleteIcon from "../../../images/QuickstartComplete.svg";
|
||||
import { ReactTabKind, useTabs } from "../../hooks/useTabs";
|
||||
|
||||
@@ -53,44 +43,6 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
||||
document.execCommand("copy");
|
||||
};
|
||||
|
||||
const getPivotHeaderIcon = (step: number): string => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return Pivot1SelectedIcon;
|
||||
case 1:
|
||||
return step === currentStep ? Pivot2SelectedIcon : Pivot2Icon;
|
||||
case 2:
|
||||
return step === currentStep ? Pivot3SelectedIcon : Pivot3Icon;
|
||||
case 3:
|
||||
return step === currentStep ? Pivot4SelectedIcon : Pivot4Icon;
|
||||
case 4:
|
||||
return step === currentStep ? Pivot5SelectedIcon : Pivot5Icon;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const customPivotHeaderRenderer = (
|
||||
link: IPivotItemProps,
|
||||
defaultRenderer: (link?: IPivotItemProps) => JSX.Element | null,
|
||||
step: number
|
||||
): JSX.Element | null => {
|
||||
if (!link || !defaultRenderer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
{currentStep > step ? (
|
||||
<Icon iconName="CompletedSolid" style={{ color: "#57A300", marginRight: 8 }} />
|
||||
) : (
|
||||
<Image style={{ marginRight: 8 }} src={getPivotHeaderIcon(step)} />
|
||||
)}
|
||||
{defaultRenderer({ ...link, itemIcon: undefined })}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack style={{ paddingTop: 8, height: "100%", width: "100%" }}>
|
||||
<Stack style={{ flexGrow: 1, padding: "0 20px", overflow: "auto" }}>
|
||||
@@ -103,7 +55,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
||||
>
|
||||
<PivotItem
|
||||
headerText="Login"
|
||||
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 0)}
|
||||
onRenderItemLink={(props, defaultRenderer) =>
|
||||
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 0)
|
||||
}
|
||||
itemKey={GuideSteps[0]}
|
||||
onClick={() => {
|
||||
setCurrentStep(0);
|
||||
@@ -125,7 +79,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="New table"
|
||||
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 1)}
|
||||
onRenderItemLink={(props, defaultRenderer) =>
|
||||
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 1)
|
||||
}
|
||||
itemKey={GuideSteps[1]}
|
||||
onClick={() => setCurrentStep(1)}
|
||||
>
|
||||
@@ -165,7 +121,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Distribute table"
|
||||
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 2)}
|
||||
onRenderItemLink={(props, defaultRenderer) =>
|
||||
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 2)
|
||||
}
|
||||
itemKey={GuideSteps[2]}
|
||||
onClick={() => setCurrentStep(2)}
|
||||
>
|
||||
@@ -210,7 +168,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Load data"
|
||||
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 3)}
|
||||
onRenderItemLink={(props, defaultRenderer) =>
|
||||
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 3)
|
||||
}
|
||||
itemKey={GuideSteps[3]}
|
||||
onClick={() => setCurrentStep(3)}
|
||||
>
|
||||
@@ -250,7 +210,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Query"
|
||||
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 4)}
|
||||
onRenderItemLink={(props, defaultRenderer) =>
|
||||
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 4)
|
||||
}
|
||||
itemKey={GuideSteps[4]}
|
||||
onClick={() => setCurrentStep(4)}
|
||||
>
|
||||
|
||||
50
src/Explorer/Quickstart/Shared/QuickstartRenderUtilities.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Icon, Image, IPivotItemProps, Stack } from "@fluentui/react";
|
||||
import React from "react";
|
||||
import Pivot1SelectedIcon from "../../../../images/Pivot1_selected.svg";
|
||||
import Pivot2Icon from "../../../../images/Pivot2.svg";
|
||||
import Pivot2SelectedIcon from "../../../../images/Pivot2_selected.svg";
|
||||
import Pivot3Icon from "../../../../images/Pivot3.svg";
|
||||
import Pivot3SelectedIcon from "../../../../images/Pivot3_selected.svg";
|
||||
import Pivot4Icon from "../../../../images/Pivot4.svg";
|
||||
import Pivot4SelectedIcon from "../../../../images/Pivot4_selected.svg";
|
||||
import Pivot5Icon from "../../../../images/Pivot5.svg";
|
||||
import Pivot5SelectedIcon from "../../../../images/Pivot5_selected.svg";
|
||||
|
||||
const getPivotHeaderIcon = (currentStep: number, newStep: number): string => {
|
||||
switch (newStep) {
|
||||
case 0:
|
||||
return Pivot1SelectedIcon;
|
||||
case 1:
|
||||
return newStep === currentStep ? Pivot2SelectedIcon : Pivot2Icon;
|
||||
case 2:
|
||||
return newStep === currentStep ? Pivot3SelectedIcon : Pivot3Icon;
|
||||
case 3:
|
||||
return newStep === currentStep ? Pivot4SelectedIcon : Pivot4Icon;
|
||||
case 4:
|
||||
return newStep === currentStep ? Pivot5SelectedIcon : Pivot5Icon;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
export const customPivotHeaderRenderer = (
|
||||
link: IPivotItemProps,
|
||||
defaultRenderer: (link?: IPivotItemProps) => JSX.Element | null,
|
||||
currentStep: number,
|
||||
newStep: number
|
||||
): JSX.Element | null => {
|
||||
if (!link || !defaultRenderer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
{currentStep > newStep ? (
|
||||
<Icon iconName="CompletedSolid" style={{ color: "#57A300", marginRight: 8 }} />
|
||||
) : (
|
||||
<Image style={{ marginRight: 8 }} src={getPivotHeaderIcon(currentStep, newStep)} />
|
||||
)}
|
||||
{defaultRenderer({ ...link, itemIcon: undefined })}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
34
src/Explorer/Quickstart/VCoreMongoQuickstartCommands.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export const newDbAndCollectionCommand = `use quickstartDB
|
||||
db.createCollection('sampleCollection')`;
|
||||
|
||||
export const newDbAndCollectionCommandForDisplay = `use quickstartDB // Create new database named 'quickstartDB' or switch to it if it already exists
|
||||
|
||||
db.createCollection('sampleCollection') // Create new collection named 'sampleCollection'`;
|
||||
|
||||
export const loadDataCommand = `db.sampleCollection.insertMany([
|
||||
{title: "The Great Gatsby", author: "F. Scott Fitzgerald", pages: 180},
|
||||
{title: "To Kill a Mockingbird", author: "Harper Lee", pages: 324},
|
||||
{title: "1984", author: "George Orwell", pages: 328},
|
||||
{title: "The Catcher in the Rye", author: "J.D. Salinger", pages: 277},
|
||||
{title: "Moby-Dick", author: "Herman Melville", pages: 720},
|
||||
{title: "Pride and Prejudice", author: "Jane Austen", pages: 279},
|
||||
{title: "The Hobbit", author: "J.R.R. Tolkien", pages: 310},
|
||||
{title: "War and Peace", author: "Leo Tolstoy", pages: 1392},
|
||||
{title: "The Odyssey", author: "Homer", pages: 374},
|
||||
{title: "Ulysses", author: "James Joyce", pages: 730}
|
||||
])`;
|
||||
|
||||
export const queriesCommand = `db.sampleCollection.find({author: "George Orwell"})
|
||||
|
||||
db.sampleCollection.find({pages: {$gt: 500}})
|
||||
|
||||
db.sampleCollection.find({}).sort({pages: 1})`;
|
||||
|
||||
export const queriesCommandForDisplay = `// Query to find all books written by "George Orwell"
|
||||
db.sampleCollection.find({author: "George Orwell"})
|
||||
|
||||
// Query to find all books with more than 500 pages
|
||||
db.sampleCollection.find({pages: {$gt: 500}})
|
||||
|
||||
// Query to find all books and sort them by the number of pages in ascending order
|
||||
db.sampleCollection.find({}).sort({pages: 1})`;
|
||||
310
src/Explorer/Quickstart/VCoreMongoQuickstartGuide.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
import {
|
||||
DefaultButton,
|
||||
IconButton,
|
||||
Link,
|
||||
Pivot,
|
||||
PivotItem,
|
||||
PrimaryButton,
|
||||
Stack,
|
||||
Text,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { customPivotHeaderRenderer } from "Explorer/Quickstart/Shared/QuickstartRenderUtilities";
|
||||
import {
|
||||
loadDataCommand,
|
||||
newDbAndCollectionCommand,
|
||||
newDbAndCollectionCommandForDisplay,
|
||||
queriesCommand,
|
||||
queriesCommandForDisplay,
|
||||
} from "Explorer/Quickstart/VCoreMongoQuickstartCommands";
|
||||
import { useTerminal } from "hooks/useTerminal";
|
||||
import React, { useState } from "react";
|
||||
import { ReactTabKind, useTabs } from "../../hooks/useTabs";
|
||||
|
||||
enum GuideSteps {
|
||||
Login,
|
||||
NewTable,
|
||||
DistributeTable,
|
||||
LoadData,
|
||||
Query,
|
||||
}
|
||||
|
||||
export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
|
||||
const [currentStep, setCurrentStep] = useState<number>(0);
|
||||
|
||||
const onCopyBtnClicked = (selector: string): void => {
|
||||
const textfield: HTMLInputElement = document.querySelector(selector);
|
||||
textfield.select();
|
||||
document.execCommand("copy");
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack style={{ paddingTop: 8, height: "100%", width: "100%" }}>
|
||||
<Stack style={{ flexGrow: 1, padding: "0 20px", overflow: "auto" }}>
|
||||
<Text variant="xxLarge">Quick start guide</Text>
|
||||
<Text variant="small">Getting started in Cosmos DB Mongo DB (vCore)</Text>
|
||||
{currentStep < 5 && (
|
||||
<Pivot
|
||||
style={{ marginTop: 10, width: "100%" }}
|
||||
selectedKey={GuideSteps[currentStep]}
|
||||
onLinkClick={(item?: PivotItem) => setCurrentStep(Object.values(GuideSteps).indexOf(item.props.itemKey))}
|
||||
>
|
||||
<PivotItem
|
||||
headerText="Connect"
|
||||
onRenderItemLink={(props, defaultRenderer) =>
|
||||
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 0)
|
||||
}
|
||||
itemKey={GuideSteps[0]}
|
||||
onClick={() => {
|
||||
setCurrentStep(0);
|
||||
}}
|
||||
>
|
||||
<Stack style={{ marginTop: 20 }}>
|
||||
<Text>
|
||||
A hosted mongosh (mongo shell) is provided for this quick start. You are automatically logged in to
|
||||
mongosh, allowing you to interact with your database directly.
|
||||
<br />
|
||||
<br />
|
||||
When not in the quick start guide, connecting to Azure Cosmos DB for MongoDB vCore is straightforward
|
||||
using your connection string.
|
||||
<br />
|
||||
<br />
|
||||
<Link
|
||||
aria-label="View connection string"
|
||||
href=""
|
||||
onClick={() => sendMessage({ type: MessageTypes.OpenVCoreMongoConnectionStringsBlade })}
|
||||
>
|
||||
View connection string
|
||||
</Link>
|
||||
<br />
|
||||
<br />
|
||||
This string contains placeholders for <user> and <password>. Replace them with your chosen
|
||||
username and password to establish a secure connection to your cluster. Depending on your environment,
|
||||
you may need to adjust firewall rules or configure private endpoints in the ‘Networking’
|
||||
tab of your database settings, or modify your own network's firewall settings, to successfully
|
||||
connect.
|
||||
</Text>
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="New collection"
|
||||
onRenderItemLink={(props, defaultRenderer) =>
|
||||
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 1)
|
||||
}
|
||||
itemKey={GuideSteps[1]}
|
||||
onClick={() => setCurrentStep(1)}
|
||||
>
|
||||
<Stack style={{ marginTop: 20 }}>
|
||||
<Text>
|
||||
In MongoDB, data is stored in collections, which are analogous to tables in relational databases.
|
||||
Collections contain documents, each of which consists of field and value pairs. The fields in
|
||||
documents are similar to the columns in a relational database table. One key advantage of MongoDB is
|
||||
that these documents within a collection can have different fields.
|
||||
<br />
|
||||
You're now going to create a new database and a collection within that database using the Mongo
|
||||
shell. In MongoDB, creating a database or a collection is implicit. This means that databases and
|
||||
collections are created when you first reference them in a command, so no explicit creation command is
|
||||
necessary.
|
||||
</Text>
|
||||
<DefaultButton
|
||||
style={{ marginTop: 16, width: 270 }}
|
||||
onClick={() => useTerminal.getState().sendMessage(newDbAndCollectionCommand)}
|
||||
>
|
||||
Create new database and collection
|
||||
</DefaultButton>
|
||||
<Stack horizontal style={{ marginTop: 16 }}>
|
||||
<TextField
|
||||
id="newDbAndCollectionCommand"
|
||||
multiline
|
||||
rows={5}
|
||||
readOnly
|
||||
defaultValue={newDbAndCollectionCommandForDisplay}
|
||||
styles={{
|
||||
root: { width: "90%" },
|
||||
field: {
|
||||
backgroundColor: "#EEEEEE",
|
||||
fontFamily:
|
||||
"Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{
|
||||
iconName: "Copy",
|
||||
}}
|
||||
onClick={() => onCopyBtnClicked("#newDbAndCollectionCommand")}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Load data"
|
||||
onRenderItemLink={(props, defaultRenderer) =>
|
||||
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 2)
|
||||
}
|
||||
itemKey={GuideSteps[2]}
|
||||
onClick={() => setCurrentStep(2)}
|
||||
>
|
||||
<Stack style={{ marginTop: 20 }}>
|
||||
<Text>
|
||||
Now that you've created your database and collection, it's time to populate your collection
|
||||
with data. In MongoDB, data is stored as documents, which are structured as field and value pairs.
|
||||
<br />
|
||||
<br />
|
||||
Let's populate your sampleCollection with data. We'll add 10 documents representing books,
|
||||
each with a title, author, and number of pages, to your sampleCollection in the quickstartDB database.
|
||||
</Text>
|
||||
<DefaultButton
|
||||
style={{ marginTop: 16, width: 200 }}
|
||||
onClick={() => useTerminal.getState().sendMessage(loadDataCommand)}
|
||||
>
|
||||
Create distributed table
|
||||
</DefaultButton>
|
||||
<Stack horizontal style={{ marginTop: 16 }}>
|
||||
<TextField
|
||||
id="loadDataCommand"
|
||||
multiline
|
||||
rows={5}
|
||||
readOnly
|
||||
defaultValue={loadDataCommand}
|
||||
styles={{
|
||||
root: { width: "90%" },
|
||||
field: {
|
||||
backgroundColor: "#EEEEEE",
|
||||
fontFamily:
|
||||
"Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{
|
||||
iconName: "Copy",
|
||||
}}
|
||||
onClick={() => onCopyBtnClicked("#loadDataCommand")}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Queries"
|
||||
onRenderItemLink={(props, defaultRenderer) =>
|
||||
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 3)
|
||||
}
|
||||
itemKey={GuideSteps[3]}
|
||||
onClick={() => setCurrentStep(3)}
|
||||
>
|
||||
<Stack style={{ marginTop: 20 }}>
|
||||
<Text>
|
||||
Once you’ve inserted data into your sampleCollection, you can retrieve it using queries. MongoDB
|
||||
queries can be as simple or as complex as you need them to be, allowing you to filter, sort, and limit
|
||||
results.
|
||||
</Text>
|
||||
<DefaultButton
|
||||
style={{ marginTop: 16, width: 110 }}
|
||||
onClick={() => useTerminal.getState().sendMessage(queriesCommand)}
|
||||
>
|
||||
Load data
|
||||
</DefaultButton>
|
||||
<Stack horizontal style={{ marginTop: 16 }}>
|
||||
<TextField
|
||||
id="queriesCommand"
|
||||
multiline
|
||||
rows={5}
|
||||
readOnly
|
||||
defaultValue={queriesCommandForDisplay}
|
||||
styles={{
|
||||
root: { width: "90%" },
|
||||
field: {
|
||||
backgroundColor: "#EEEEEE",
|
||||
fontFamily:
|
||||
"Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{
|
||||
iconName: "Copy",
|
||||
}}
|
||||
onClick={() => onCopyBtnClicked("#queriesCommand")}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Integrations"
|
||||
onRenderItemLink={(props, defaultRenderer) =>
|
||||
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 4)
|
||||
}
|
||||
itemKey={GuideSteps[4]}
|
||||
onClick={() => setCurrentStep(4)}
|
||||
>
|
||||
<Stack>
|
||||
<Text>
|
||||
Cosmos DB for MongoDB vCore seamlessly integrates with Azure services. These integrations enable
|
||||
Cosmos DB for MongoDB and its partner products to directly interoperate, ensuring a smooth and unified
|
||||
experience, that just works.
|
||||
</Text>
|
||||
<Stack horizontal>
|
||||
<Stack style={{ marginTop: 20, marginRight: 20 }}>
|
||||
<Text>
|
||||
<b>First party integrations</b>
|
||||
<br />
|
||||
<br />
|
||||
<b>Azure Monitor</b>
|
||||
<br />
|
||||
Azure monitor provides comprehensive monitoring and diagnostics for Cosmos DB for Mongo DB. Learn
|
||||
more
|
||||
<br />
|
||||
<br />
|
||||
<b>Azure Networking</b>
|
||||
<br />
|
||||
Azure Networking seamlessly integrates with Azure Cosmos DB for Mongo DB for fast and secure data
|
||||
access. Learn more
|
||||
<br />
|
||||
<br />
|
||||
<b>PowerShell/CLI/ARM</b>
|
||||
<br />
|
||||
PowerShell/CLI/ARM seamlessly integrates with Azure Cosmos DB for Mongo DB for efficient
|
||||
management and automation. Learn more
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack style={{ marginTop: 20, marginLeft: 20 }}>
|
||||
<Text>
|
||||
<b>Application platforms integrations</b>
|
||||
<br />
|
||||
<br />
|
||||
<b>Vercel</b>
|
||||
<br />
|
||||
Vercel is a cloud platform for hosting static front ends and serverless functions, with instant
|
||||
deployments, automated scaling, and Next.js integration. Learn more
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack horizontal style={{ padding: "16px 20px", boxShadow: "inset 0px 1px 0px rgba(204, 204, 204, 0.8)" }}>
|
||||
<DefaultButton disabled={currentStep === 0} onClick={() => setCurrentStep(currentStep - 1)}>
|
||||
Previous
|
||||
</DefaultButton>
|
||||
{currentStep < 4 && (
|
||||
<PrimaryButton onClick={() => setCurrentStep(currentStep + 1)} style={{ marginLeft: 8 }}>
|
||||
Next
|
||||
</PrimaryButton>
|
||||
)}
|
||||
{currentStep === 4 && (
|
||||
<PrimaryButton
|
||||
onClick={() => useTabs.getState().closeReactTab(ReactTabKind.Quickstart)}
|
||||
style={{ marginLeft: 8 }}
|
||||
>
|
||||
Done
|
||||
</PrimaryButton>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -260,30 +260,33 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
let title: string;
|
||||
let subtitle: string;
|
||||
|
||||
switch (userContext.apiType) {
|
||||
case "Postgres":
|
||||
title = "Welcome to Azure Cosmos DB for PostgreSQL";
|
||||
subtitle = "Get started with our sample datasets, documentation, and additional tools.";
|
||||
break;
|
||||
case "VCoreMongo":
|
||||
title = "Welcome to Azure Cosmos DB for MongoDB (vCore)";
|
||||
subtitle = "Get started with our sample datasets, documentation, and additional tools.";
|
||||
break;
|
||||
default:
|
||||
title = "Welcome to Azure Cosmos DB";
|
||||
subtitle = "Globally distributed, multi-model database service for any scale";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="connectExplorerContainer">
|
||||
<form className="connectExplorerFormContainer">
|
||||
<div className="splashScreenContainer">
|
||||
<div className="splashScreen">
|
||||
<h1
|
||||
className="title"
|
||||
role="heading"
|
||||
aria-label={
|
||||
userContext.apiType === "Postgres"
|
||||
? "Welcome to Azure Cosmos DB for PostgreSQL"
|
||||
: "Welcome to Azure Cosmos DB"
|
||||
}
|
||||
>
|
||||
{userContext.apiType === "Postgres"
|
||||
? "Welcome to Azure Cosmos DB for PostgreSQL"
|
||||
: "Welcome to Azure Cosmos DB"}
|
||||
<h1 className="title" role="heading" aria-label={title}>
|
||||
{title}
|
||||
<FeaturePanelLauncher />
|
||||
</h1>
|
||||
<div className="subtitle">
|
||||
{userContext.apiType === "Postgres"
|
||||
? "Get started with our sample datasets, documentation, and additional tools."
|
||||
: "Globally distributed, multi-model database service for any scale"}
|
||||
</div>
|
||||
<div className="subtitle">{subtitle}</div>
|
||||
{this.getSplashScreenButtons()}
|
||||
{useCarousel.getState().showCoachMark && (
|
||||
<Coachmark
|
||||
@@ -313,7 +316,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
</TeachingBubbleContent>
|
||||
</Coachmark>
|
||||
)}
|
||||
{userContext.apiType === "Postgres" ? (
|
||||
{userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" ? (
|
||||
<Stack horizontal style={{ margin: "0 auto", width: "84%" }} tokens={{ childrenGap: 16 }}>
|
||||
<Stack style={{ width: "33%" }}>
|
||||
<Text
|
||||
@@ -380,7 +383,8 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
if (
|
||||
userContext.apiType === "SQL" ||
|
||||
userContext.apiType === "Mongo" ||
|
||||
(userContext.apiType === "Postgres" && !userContext.isReplica)
|
||||
(userContext.apiType === "Postgres" && !userContext.isReplica) ||
|
||||
userContext.apiType === "VCoreMongo"
|
||||
) {
|
||||
const launchQuickstartBtn = {
|
||||
id: "quickstartDescription",
|
||||
@@ -388,9 +392,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
title: "Launch quick start",
|
||||
description: "Launch a quick start tutorial to get started with sample data",
|
||||
onClick: () => {
|
||||
userContext.apiType === "Postgres"
|
||||
? useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart)
|
||||
: this.container.onNewCollectionClicked({ isQuickstart: true });
|
||||
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart);
|
||||
} else {
|
||||
this.container.onNewCollectionClicked({ isQuickstart: true });
|
||||
}
|
||||
traceOpen(Action.LaunchQuickstart, { apiType: userContext.apiType });
|
||||
},
|
||||
};
|
||||
@@ -405,39 +411,65 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
heroes.push(newNotebookBtn);
|
||||
}
|
||||
|
||||
heroes.push(this.getShellCard());
|
||||
heroes.push(this.getThirdCard());
|
||||
return heroes;
|
||||
}
|
||||
|
||||
private getShellCard() {
|
||||
if (userContext.apiType === "Postgres") {
|
||||
const postgreShellBtn = {
|
||||
return {
|
||||
iconSrc: PowerShellIcon,
|
||||
title: "PostgreSQL Shell",
|
||||
description: "Create table and interact with data using PostgreSQL’s shell interface",
|
||||
onClick: () => this.container.openNotebookTerminal(TerminalKind.Postgres),
|
||||
};
|
||||
heroes.push(postgreShellBtn);
|
||||
} else {
|
||||
const newContainerBtn = {
|
||||
iconSrc: ContainersIcon,
|
||||
title: `New ${getCollectionName()}`,
|
||||
description: "Create a new container for storage and throughput",
|
||||
onClick: () => {
|
||||
this.container.onNewCollectionClicked();
|
||||
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
|
||||
},
|
||||
};
|
||||
heroes.push(newContainerBtn);
|
||||
}
|
||||
|
||||
const connectBtn = {
|
||||
iconSrc: ConnectIcon,
|
||||
title: userContext.apiType === "Postgres" ? "Connect with pgAdmin" : "Connect",
|
||||
description:
|
||||
userContext.apiType === "Postgres"
|
||||
? "Prefer pgAdmin? Find your connection strings here"
|
||||
: "Prefer using your own choice of tooling? Find the connection string you need to connect",
|
||||
onClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect),
|
||||
};
|
||||
heroes.push(connectBtn);
|
||||
if (userContext.apiType === "VCoreMongo") {
|
||||
return {
|
||||
iconSrc: PowerShellIcon,
|
||||
title: "Mongo Shell",
|
||||
description: "Create a collection and interact with data using MongoDB's shell interface",
|
||||
onClick: () => this.container.openNotebookTerminal(TerminalKind.VCoreMongo),
|
||||
};
|
||||
}
|
||||
|
||||
return heroes;
|
||||
return {
|
||||
iconSrc: ContainersIcon,
|
||||
title: `New ${getCollectionName()}`,
|
||||
description: "Create a new container for storage and throughput",
|
||||
onClick: () => {
|
||||
this.container.onNewCollectionClicked();
|
||||
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private getThirdCard() {
|
||||
let icon = ConnectIcon;
|
||||
let title = "Connect";
|
||||
let description = "Prefer using your own choice of tooling? Find the connection string you need to connect";
|
||||
let onClick = () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect);
|
||||
|
||||
if (userContext.apiType === "Postgres") {
|
||||
title = "Connect with pgAdmin";
|
||||
description = "Prefer pgAdmin? Find your connection strings here";
|
||||
}
|
||||
|
||||
if (userContext.apiType === "VCoreMongo") {
|
||||
icon = ContainersIcon;
|
||||
title = "Connect with Studio 3T";
|
||||
description = "Prefer Studio 3T? Find your connection strings here";
|
||||
onClick = () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect);
|
||||
}
|
||||
|
||||
return {
|
||||
iconSrc: icon,
|
||||
title: title,
|
||||
description: description,
|
||||
onClick: onClick,
|
||||
};
|
||||
}
|
||||
|
||||
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
|
||||
@@ -587,6 +619,8 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
},
|
||||
];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<Stack>
|
||||
@@ -724,6 +758,8 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
cdbLiveTv,
|
||||
];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<Stack>
|
||||
@@ -749,24 +785,46 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
);
|
||||
}
|
||||
|
||||
private postgresNextStepItems: { link: string; title: string; description: string }[] = [
|
||||
{
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2208312",
|
||||
title: "Data Modeling",
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: " https://go.microsoft.com/fwlink/?linkid=2206941 ",
|
||||
title: "How to choose a Distribution Column",
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2207425",
|
||||
title: "Build Apps with Python/Java/Django",
|
||||
description: "",
|
||||
},
|
||||
];
|
||||
|
||||
private vcoreMongoNextStepItems: { link: string; title: string; description: string }[] = [
|
||||
{
|
||||
link:
|
||||
"https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/how-to-migrate-native-tools?tabs=export-import",
|
||||
title: "Migrate Data",
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/vector-search-ai",
|
||||
title: "Build AI apps with Vector Search",
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link:
|
||||
"https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/tutorial-nodejs-web-app?tabs=github-codespaces",
|
||||
title: "Build Apps with Nodejs",
|
||||
description: "",
|
||||
},
|
||||
];
|
||||
|
||||
private getNextStepItems(): JSX.Element {
|
||||
const items: { link: string; title: string; description: string }[] = [
|
||||
{
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2208312",
|
||||
title: "Data Modeling",
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: " https://go.microsoft.com/fwlink/?linkid=2206941 ",
|
||||
title: "How to choose a Distribution Column",
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2207425",
|
||||
title: "Build Apps with Python/Java/Django",
|
||||
description: "",
|
||||
},
|
||||
];
|
||||
const items = userContext.apiType === "Postgres" ? this.postgresNextStepItems : this.vcoreMongoNextStepItems;
|
||||
|
||||
return (
|
||||
<Stack style={{ minWidth: 124, maxWidth: 296 }}>
|
||||
@@ -785,24 +843,44 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
);
|
||||
}
|
||||
|
||||
private postgresLearnMoreItems: { link: string; title: string; description: string }[] = [
|
||||
{
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2207226",
|
||||
title: "Performance Tuning",
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2208037",
|
||||
title: "Useful Diagnostic Queries",
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2205270",
|
||||
title: "Distributed SQL Reference",
|
||||
description: "",
|
||||
},
|
||||
];
|
||||
|
||||
private vcoreMongoLearnMoreItems: { link: string; title: string; description: string }[] = [
|
||||
{
|
||||
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/vector-search",
|
||||
title: "Vector Search",
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/how-to-create-text-index",
|
||||
title: "Text Indexing",
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/troubleshoot-common-issues",
|
||||
title: "Troubleshoot common issues",
|
||||
description: "",
|
||||
},
|
||||
];
|
||||
|
||||
private getTipsAndLearnMoreItems(): JSX.Element {
|
||||
const items: { link: string; title: string; description: string }[] = [
|
||||
{
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2207226",
|
||||
title: "Performance Tuning",
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2208037",
|
||||
title: "Useful Diagnostic Queries",
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2205270",
|
||||
title: "Distributed SQL Reference",
|
||||
description: "",
|
||||
},
|
||||
];
|
||||
const items = userContext.apiType === "Postgres" ? this.postgresLearnMoreItems : this.vcoreMongoLearnMoreItems;
|
||||
|
||||
return (
|
||||
<Stack style={{ minWidth: 124, maxWidth: 296 }}>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
||||
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
@@ -76,6 +78,7 @@ interface IQueryTabStates {
|
||||
isExecutionError: boolean;
|
||||
isExecuting: boolean;
|
||||
showCopilotSidebar: boolean;
|
||||
queryCopilotGeneratedQuery: string;
|
||||
}
|
||||
|
||||
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
|
||||
@@ -101,6 +104,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
isExecutionError: this.props.isExecutionError,
|
||||
isExecuting: false,
|
||||
showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar,
|
||||
queryCopilotGeneratedQuery: useQueryCopilot.getState().query,
|
||||
};
|
||||
this.isCloseClicked = false;
|
||||
this.splitterId = this.props.tabId + "_splitter";
|
||||
@@ -274,7 +278,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
buttons.push({
|
||||
iconSrc: ExecuteQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onExecuteQueryClick,
|
||||
onCommandClick: this.isCopilotTabActive ? () => OnExecuteQueryClick() : this.onExecuteQueryClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
@@ -334,6 +338,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
public onChangeContent(newContent: string): void {
|
||||
this.setState({
|
||||
sqlQueryEditorContent: newContent,
|
||||
queryCopilotGeneratedQuery: "",
|
||||
});
|
||||
if (this.isPreferredApiMongoDB) {
|
||||
if (newContent.length > 0) {
|
||||
@@ -362,9 +367,24 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
selectedContent: "",
|
||||
});
|
||||
}
|
||||
|
||||
if (this.isCopilotTabActive) {
|
||||
selectedContent.trim().length > 0
|
||||
? useQueryCopilot.getState().setSelectedQuery(selectedContent)
|
||||
: useQueryCopilot.getState().setSelectedQuery("");
|
||||
}
|
||||
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}
|
||||
|
||||
public setEditorContent(): string {
|
||||
if (this.isCopilotTabActive && this.state.queryCopilotGeneratedQuery) {
|
||||
return this.state.queryCopilotGeneratedQuery;
|
||||
}
|
||||
|
||||
return this.state.sqlQueryEditorContent;
|
||||
}
|
||||
|
||||
private unsubscribeCopilotSidebar: () => void;
|
||||
|
||||
componentDidMount(): void {
|
||||
@@ -372,6 +392,9 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
if (this.state.showCopilotSidebar !== state.showCopilotSidebar) {
|
||||
this.setState({ showCopilotSidebar: state.showCopilotSidebar });
|
||||
}
|
||||
if (this.state.queryCopilotGeneratedQuery !== state.query) {
|
||||
this.setState({ queryCopilotGeneratedQuery: state.query });
|
||||
}
|
||||
});
|
||||
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
@@ -393,7 +416,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
<div className="queryEditor" style={{ height: "100%" }}>
|
||||
<EditorReact
|
||||
language={"sql"}
|
||||
content={this.state.sqlQueryEditorContent}
|
||||
content={this.setEditorContent()}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"Editing Query"}
|
||||
lineNumbers={"on"}
|
||||
@@ -402,14 +425,20 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
<QueryResultSection
|
||||
isMongoDB={this.props.isPreferredApiMongoDB}
|
||||
queryEditorContent={this.state.sqlQueryEditorContent}
|
||||
error={this.state.error}
|
||||
queryResults={this.state.queryResults}
|
||||
isExecuting={this.state.isExecuting}
|
||||
executeQueryDocumentsPage={(firstItemIndex: number) => this._executeQueryDocumentsPage(firstItemIndex)}
|
||||
/>
|
||||
{this.isCopilotTabActive ? (
|
||||
<QueryCopilotResults />
|
||||
) : (
|
||||
<QueryResultSection
|
||||
isMongoDB={this.props.isPreferredApiMongoDB}
|
||||
queryEditorContent={this.state.sqlQueryEditorContent}
|
||||
error={this.state.error}
|
||||
queryResults={this.state.queryResults}
|
||||
isExecuting={this.state.isExecuting}
|
||||
executeQueryDocumentsPage={(firstItemIndex: number) =>
|
||||
this._executeQueryDocumentsPage(firstItemIndex)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</SplitterLayout>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
||||
import { PoolIdType } from "Common/Constants";
|
||||
import { configContext } from "ConfigContext";
|
||||
import { NotebookWorkspaceConnectionInfo, PostgresFirewallRule } from "Contracts/DataModels";
|
||||
import { NotebookWorkspaceConnectionInfo } from "Contracts/DataModels";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
|
||||
import { QuickstartGuide } from "Explorer/Quickstart/QuickstartGuide";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
||||
import { userContext } from "UserContext";
|
||||
import { armRequest } from "Utils/arm/request";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
|
||||
|
||||
interface QuickstartTabProps {
|
||||
explorer: Explorer;
|
||||
@@ -26,29 +26,12 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
|
||||
forwardingId: notebookServerInfo.forwardingId,
|
||||
});
|
||||
|
||||
const checkFirewallRules = async (): Promise<void> => {
|
||||
const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const response: any = await armRequest({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path: firewallRulesUri,
|
||||
method: "GET",
|
||||
apiVersion: "2022-11-08",
|
||||
});
|
||||
const firewallRules: PostgresFirewallRule[] = response?.data?.value || response?.value || [];
|
||||
const isEnabled = firewallRules.some(
|
||||
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"
|
||||
);
|
||||
setIsAllPublicIPAddressEnabled(isEnabled);
|
||||
|
||||
// If the firewall rule is not added, check every 30 seconds to see if the user has added the rule
|
||||
if (!isEnabled && useTabs.getState().activeReactTab === ReactTabKind.Quickstart) {
|
||||
setTimeout(checkFirewallRules, 30000);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkFirewallRules();
|
||||
checkFirewallRules(
|
||||
"2022-11-08",
|
||||
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255",
|
||||
setIsAllPublicIPAddressEnabled
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -61,7 +44,13 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
|
||||
<QuickstartGuide />
|
||||
</Stack>
|
||||
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
|
||||
{!isAllPublicIPAddressEnabled && <QuickstartFirewallNotification />}
|
||||
{!isAllPublicIPAddressEnabled && (
|
||||
<QuickstartFirewallNotification
|
||||
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
||||
screenshot={FirewallRuleScreenshot}
|
||||
shellName="PostgreSQL"
|
||||
/>
|
||||
)}
|
||||
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
|
||||
<NotebookTerminalComponent
|
||||
notebookServerInfo={getNotebookServerInfo()}
|
||||
|
||||